diff --git a/data/resources/fullscreenui/back-icon.png b/data/resources/fullscreenui/back-icon.png new file mode 100644 index 000000000..438aaa39b Binary files /dev/null and b/data/resources/fullscreenui/back-icon.png differ diff --git a/data/resources/fullscreenui/desktop-mode.png b/data/resources/fullscreenui/desktop-mode.png new file mode 100644 index 000000000..081e031b2 Binary files /dev/null and b/data/resources/fullscreenui/desktop-mode.png differ diff --git a/data/resources/fullscreenui/drive-cdrom.png b/data/resources/fullscreenui/drive-cdrom.png new file mode 100644 index 000000000..5fa911ccd Binary files /dev/null and b/data/resources/fullscreenui/drive-cdrom.png differ diff --git a/data/resources/fullscreenui/exit.png b/data/resources/fullscreenui/exit.png new file mode 100644 index 000000000..d0cec9db7 Binary files /dev/null and b/data/resources/fullscreenui/exit.png differ diff --git a/data/resources/fullscreenui/start-bios.png b/data/resources/fullscreenui/start-bios.png new file mode 100644 index 000000000..bd7405269 Binary files /dev/null and b/data/resources/fullscreenui/start-bios.png differ diff --git a/data/resources/fullscreenui/start-file.png b/data/resources/fullscreenui/start-file.png new file mode 100644 index 000000000..3e27c1933 Binary files /dev/null and b/data/resources/fullscreenui/start-file.png differ diff --git a/src/core/achievements.cpp b/src/core/achievements.cpp index 2098dc8d1..4e1982d23 100644 --- a/src/core/achievements.cpp +++ b/src/core/achievements.cpp @@ -1725,16 +1725,7 @@ void Achievements::ShowLoginNotification() if (g_settings.achievements_notifications && FullscreenUI::Initialize()) { - std::string badge_path = GetUserBadgePath(user->username); - if (!FileSystem::FileExists(badge_path.c_str())) - { - char url[512]; - const int res = rc_client_user_get_image_url(user, url, std::size(url)); - if (res == RC_OK) - DownloadImage(url, badge_path); - else - ReportRCError(res, "rc_client_user_get_image_url() failed: "); - } + std::string badge_path = GetLoggedInUserBadgePath(); //: Summary for login notification. std::string title = user->display_name; @@ -1746,6 +1737,37 @@ void Achievements::ShowLoginNotification() } } +const char* Achievements::GetLoggedInUserName() +{ + const rc_client_user_t* user = rc_client_get_user_info(s_client); + if (!user) [[unlikely]] + return nullptr; + + return user->username; +} + +std::string Achievements::GetLoggedInUserBadgePath() +{ + std::string badge_path; + + const rc_client_user_t* user = rc_client_get_user_info(s_client); + if (!user) [[unlikely]] + return badge_path; + + badge_path = GetUserBadgePath(user->username); + if (!FileSystem::FileExists(badge_path.c_str())) [[unlikely]] + { + char url[512]; + const int res = rc_client_user_get_image_url(user, url, std::size(url)); + if (res == RC_OK) + DownloadImage(url, badge_path); + else + ReportRCError(res, "rc_client_user_get_image_url() failed: "); + } + + return badge_path; +} + void Achievements::Logout() { if (IsActive()) @@ -2290,6 +2312,8 @@ void Achievements::DrawAchievementsWindow() ImGuiFullscreen::EndMenuButtons(); } ImGuiFullscreen::EndFullscreenWindow(); + + FullscreenUI::SetStandardSelectionFooterText(true); } void Achievements::DrawAchievement(const rc_client_achievement_t* cheevo) @@ -2705,6 +2729,7 @@ void Achievements::DrawLeaderboardsWindow() } } ImGuiFullscreen::EndFullscreenWindow(); + FullscreenUI::SetStandardSelectionFooterText(true); if (!is_leaderboard_open) { diff --git a/src/core/achievements.h b/src/core/achievements.h index fbfc8c728..504b92389 100644 --- a/src/core/achievements.h +++ b/src/core/achievements.h @@ -119,6 +119,13 @@ const std::string& GetRichPresenceString(); /// Should be called with the lock held. const std::string& GetGameTitle(); +/// Returns the logged-in user name. +const char* GetLoggedInUserName(); + +/// Returns the path to the user's profile avatar. +/// Should be called with the lock held. +std::string GetLoggedInUserBadgePath(); + /// Clears all cached state used to render the UI. void ClearUIState(); diff --git a/src/core/fullscreen_ui.cpp b/src/core/fullscreen_ui.cpp index dabddc034..3ed5baa5e 100644 --- a/src/core/fullscreen_ui.cpp +++ b/src/core/fullscreen_ui.cpp @@ -19,6 +19,7 @@ #include "scmversion/scmversion.h" +#include "util/cd_image.h" #include "util/gpu_device.h" #include "util/imgui_fullscreen.h" #include "util/imgui_manager.h" @@ -80,6 +81,7 @@ using ImGuiFullscreen::g_large_font; using ImGuiFullscreen::g_layout_padding_left; using ImGuiFullscreen::g_layout_padding_top; using ImGuiFullscreen::g_medium_font; +using ImGuiFullscreen::LAYOUT_FOOTER_HEIGHT; using ImGuiFullscreen::LAYOUT_LARGE_FONT_SIZE; using ImGuiFullscreen::LAYOUT_MEDIUM_FONT_SIZE; using ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT; @@ -109,6 +111,7 @@ using ImGuiFullscreen::AddNotification; using ImGuiFullscreen::BeginFullscreenColumns; using ImGuiFullscreen::BeginFullscreenColumnWindow; using ImGuiFullscreen::BeginFullscreenWindow; +using ImGuiFullscreen::BeginHorizontalMenu; using ImGuiFullscreen::BeginMenuButtons; using ImGuiFullscreen::BeginNavBar; using ImGuiFullscreen::CenterImage; @@ -119,13 +122,18 @@ using ImGuiFullscreen::DrawShadowedText; using ImGuiFullscreen::EndFullscreenColumns; using ImGuiFullscreen::EndFullscreenColumnWindow; using ImGuiFullscreen::EndFullscreenWindow; +using ImGuiFullscreen::EndHorizontalMenu; using ImGuiFullscreen::EndMenuButtons; using ImGuiFullscreen::EndNavBar; using ImGuiFullscreen::EnumChoiceButton; using ImGuiFullscreen::FloatingButton; +using ImGuiFullscreen::ForceKeyNavEnabled; using ImGuiFullscreen::GetCachedTexture; using ImGuiFullscreen::GetCachedTextureAsync; using ImGuiFullscreen::GetPlaceholderTexture; +using ImGuiFullscreen::HorizontalMenuItem; +using ImGuiFullscreen::IsFocusResetQueued; +using ImGuiFullscreen::IsGamepadInputSource; using ImGuiFullscreen::LayoutScale; using ImGuiFullscreen::LoadTexture; using ImGuiFullscreen::MenuButton; @@ -150,6 +158,7 @@ using ImGuiFullscreen::QueueResetFocus; using ImGuiFullscreen::RangeButton; using ImGuiFullscreen::ResetFocusHere; using ImGuiFullscreen::RightAlignNavButtons; +using ImGuiFullscreen::SetFullscreenFooterText; using ImGuiFullscreen::ShowToast; using ImGuiFullscreen::ThreeWayToggleButton; using ImGuiFullscreen::ToggleButton; @@ -161,7 +170,10 @@ enum class MainWindowType { None, Landing, + StartGame, + Exit, GameList, + GameListSettings, Settings, PauseMenu, Achievements, @@ -193,11 +205,10 @@ enum class SettingsPage Count }; -enum class GameListPage +enum class GameListView { Grid, List, - Settings, Count }; @@ -210,17 +221,21 @@ struct PostProcessingStageInfo ////////////////////////////////////////////////////////////////////////// // Main ////////////////////////////////////////////////////////////////////////// -static void ToggleTheme(); static void PauseForMenuOpen(bool set_pause_menu_open); +static bool AreAnyDialogsOpen(); static void ClosePauseMenu(); static void OpenPauseSubMenu(PauseSubMenu submenu); +static void DrawLandingTemplate(ImVec2* menu_pos, ImVec2* menu_size); static void DrawLandingWindow(); +static void DrawStartGameWindow(); +static void DrawExitWindow(); static void DrawPauseMenu(); static void ExitFullscreenAndOpenURL(const std::string_view& url); static void CopyTextToClipboard(std::string title, const std::string_view& text); static void DrawAboutWindow(); static void OpenAboutWindow(); static void FixStateIfPaused(); +static void GetStandardSelectionFooterText(SmallStringBase& dest, bool back_instead_of_cancel); static MainWindowType s_current_main_window = MainWindowType::None; static PauseSubMenu s_current_pause_submenu = PauseSubMenu::None; @@ -255,12 +270,15 @@ static void DoStartPath(std::string path, std::string state = std::string(), static void DoResume(); static void DoStartFile(); static void DoStartBIOS(); +static void DoStartDisc(std::string path); +static void DoStartDisc(); static void DoToggleFastForward(); static void DoShutdown(bool save_state); static void DoReset(); static void DoChangeDiscFromFile(); static void DoChangeDisc(); static void DoRequestExit(); +static void DoDesktopMode(); static void DoToggleFullscreen(); static void DoCheatsMenu(); static void DoToggleAnalogMode(); @@ -414,10 +432,11 @@ struct SaveStateListEntry bool global; }; -static void InitializePlaceholderSaveStateListEntry(SaveStateListEntry* li, const std::string& title, - const std::string& serial, s32 slot, bool global); -static bool InitializeSaveStateListEntry(SaveStateListEntry* li, const std::string& title, const std::string& serial, - s32 slot, bool global); +static void InitializePlaceholderSaveStateListEntry(SaveStateListEntry* li, const std::string& serial, s32 slot, + bool global); +static bool InitializeSaveStateListEntryFromSerial(SaveStateListEntry* li, const std::string& serial, s32 slot, + bool global); +static bool InitializeSaveStateListEntryFromPath(SaveStateListEntry* li, std::string path, s32 slot, bool global); static void PopulateSaveStateScreenshot(SaveStateListEntry* li, const ExtendedSaveStateInfo* ssi); static void ClearSaveStateEntryList(); static u32 PopulateSaveStateListEntries(const std::string& title, const std::string& serial); @@ -445,7 +464,7 @@ static void DrawGameList(const ImVec2& heading_size); static void DrawGameGrid(const ImVec2& heading_size); static void HandleGameListActivate(const GameList::Entry* entry); static void HandleGameListOptions(const GameList::Entry* entry); -static void DrawGameListSettingsPage(const ImVec2& heading_size); +static void DrawGameListSettingsWindow(); static void SwitchToGameList(); static void PopulateGameListEntryList(); static GPUTexture* GetTextureForGameListEntryType(GameList::EntryType type); @@ -455,7 +474,7 @@ static GPUTexture* GetCoverForCurrentGame(); // Lazily populated cover images. static std::unordered_map s_cover_image_map; static std::vector s_game_list_sorted_entries; -static GameListPage s_game_list_page = GameListPage::Grid; +static GameListView s_game_list_view = GameListView::Grid; } // namespace FullscreenUI ////////////////////////////////////////////////////////////////////////// @@ -476,6 +495,73 @@ void FullscreenUI::TimeToPrintableString(SmallStringBase* str, time_t t) str->assign(buf); } +void FullscreenUI::GetStandardSelectionFooterText(SmallStringBase& dest, bool back_instead_of_cancel) +{ + if (IsGamepadInputSource()) + { + ImGuiFullscreen::CreateFooterTextString( + dest, + std::array{std::make_pair(ICON_PF_XBOX_DPAD_UP_DOWN, FSUI_VSTR("Change Selection")), + std::make_pair(ICON_PF_BUTTON_A, FSUI_VSTR("Select")), + std::make_pair(ICON_PF_BUTTON_B, back_instead_of_cancel ? FSUI_VSTR("Back") : FSUI_VSTR("Cancel"))}); + } + else + { + ImGuiFullscreen::CreateFooterTextString( + dest, std::array{std::make_pair(ICON_PF_ARROW_UP ICON_PF_ARROW_DOWN, FSUI_VSTR("Change Selection")), + std::make_pair(ICON_PF_ENTER, FSUI_VSTR("Select")), + std::make_pair(ICON_PF_ESC, back_instead_of_cancel ? FSUI_VSTR("Back") : FSUI_VSTR("Cancel"))}); + } +} + +void FullscreenUI::SetStandardSelectionFooterText(bool back_instead_of_cancel) +{ + SmallString text; + GetStandardSelectionFooterText(text, back_instead_of_cancel); + ImGuiFullscreen::SetFullscreenFooterText(text); +} + +void ImGuiFullscreen::GetChoiceDialogHelpText(SmallStringBase& dest) +{ + FullscreenUI::GetStandardSelectionFooterText(dest, false); +} + +void ImGuiFullscreen::GetFileSelectorHelpText(SmallStringBase& dest) +{ + if (IsGamepadInputSource()) + { + ImGuiFullscreen::CreateFooterTextString( + dest, std::array{std::make_pair(ICON_PF_XBOX_DPAD_UP_DOWN, FSUI_VSTR("Change Selection")), + std::make_pair(ICON_PF_BUTTON_Y, FSUI_VSTR("Parent Directory")), + std::make_pair(ICON_PF_BUTTON_A, FSUI_VSTR("Select")), + std::make_pair(ICON_PF_BUTTON_B, FSUI_VSTR("Cancel"))}); + } + else + { + ImGuiFullscreen::CreateFooterTextString( + dest, + std::array{std::make_pair(ICON_PF_ARROW_UP ICON_PF_ARROW_DOWN, FSUI_VSTR("Change Selection")), + std::make_pair(ICON_PF_BACKSPACE, FSUI_VSTR("Parent Directory")), + std::make_pair(ICON_PF_ENTER, FSUI_VSTR("Select")), std::make_pair(ICON_PF_ESC, FSUI_VSTR("Cancel"))}); + } +} + +void ImGuiFullscreen::GetInputDialogHelpText(SmallStringBase& dest) +{ + if (IsGamepadInputSource()) + { + CreateFooterTextString(dest, std::array{std::make_pair(ICON_PF_KEYBOARD, FSUI_VSTR("Enter Value")), + std::make_pair(ICON_PF_BUTTON_A, FSUI_VSTR("Select")), + std::make_pair(ICON_PF_BUTTON_B, FSUI_VSTR("Cancel"))}); + } + else + { + CreateFooterTextString(dest, std::array{std::make_pair(ICON_PF_KEYBOARD, FSUI_VSTR("Enter Value")), + std::make_pair(ICON_PF_ENTER, FSUI_VSTR("Select")), + std::make_pair(ICON_PF_ESC, FSUI_VSTR("Cancel"))}); + } +} + ////////////////////////////////////////////////////////////////////////// // Main ////////////////////////////////////////////////////////////////////////// @@ -514,6 +600,7 @@ bool FullscreenUI::Initialize() if (!System::IsRunning()) Host::OnIdleStateChanged(); + ForceKeyNavEnabled(); return true; } @@ -524,8 +611,14 @@ bool FullscreenUI::IsInitialized() bool FullscreenUI::HasActiveWindow() { - return s_initialized && (s_current_main_window != MainWindowType::None || s_save_state_selector_open || - ImGuiFullscreen::IsChoiceDialogOpen() || ImGuiFullscreen::IsFileSelectorOpen()); + return s_initialized && (s_current_main_window != MainWindowType::None || AreAnyDialogsOpen()); +} + +bool FullscreenUI::AreAnyDialogsOpen() +{ + return (s_save_state_selector_open || s_about_window_open || + s_input_binding_type != InputBindingInfo::Type::Unknown || ImGuiFullscreen::IsChoiceDialogOpen() || + ImGuiFullscreen::IsFileSelectorOpen()); } void FullscreenUI::CheckForConfigChanges(const Settings& old_settings) @@ -587,14 +680,6 @@ void FullscreenUI::OnRunningGameChanged() s_current_game_subtitle = {}; } -void FullscreenUI::ToggleTheme() -{ - const bool new_light = !Host::GetBaseBoolSettingValue("Main", "UseLightFullscreenUITheme", false); - Host::SetBaseBoolSettingValue("Main", "UseLightFullscreenUITheme", new_light); - Host::CommitBaseSettingChanges(); - ImGuiFullscreen::SetTheme(new_light); -} - void FullscreenUI::PauseForMenuOpen(bool set_pause_menu_open) { s_was_paused_on_quick_menu_open = (System::GetState() == System::State::Paused); @@ -616,6 +701,7 @@ void FullscreenUI::OpenPauseMenu() s_current_main_window = MainWindowType::PauseMenu; s_current_pause_submenu = PauseSubMenu::None; QueueResetFocus(); + ForceKeyNavEnabled(); FixStateIfPaused(); } @@ -696,9 +782,18 @@ void FullscreenUI::Render() case MainWindowType::Landing: DrawLandingWindow(); break; + case MainWindowType::StartGame: + DrawStartGameWindow(); + break; + case MainWindowType::Exit: + DrawExitWindow(); + break; case MainWindowType::GameList: DrawGameListWindow(); break; + case MainWindowType::GameListSettings: + DrawGameListSettingsWindow(); + break; case MainWindowType::Settings: DrawSettingsWindow(); break; @@ -834,14 +929,23 @@ void FullscreenUI::DoStartPath(std::string path, std::string state, std::optiona void FullscreenUI::DoResume() { - std::string path(System::GetMostRecentResumeSaveStatePath()); + std::string path = System::GetMostRecentResumeSaveStatePath(); if (path.empty()) { ShowToast({}, FSUI_CSTR("No resume save state found.")); return; } - DoStartPath({}, std::move(path)); + SaveStateListEntry slentry; + if (!InitializeSaveStateListEntryFromPath(&slentry, std::move(path), -1, false)) + return; + + CloseSaveStateSelector(); + s_save_state_selector_slots.push_back(std::move(slentry)); + s_save_state_selector_game_path = {}; + s_save_state_selector_loading = true; + s_save_state_selector_open = true; + s_save_state_selector_resuming = true; } void FullscreenUI::DoStartFile() @@ -869,6 +973,56 @@ void FullscreenUI::DoStartBIOS() }); } +void FullscreenUI::DoStartDisc(std::string path) +{ + Host::RunOnCPUThread([path = std::move(path)]() mutable { + if (System::IsValid()) + return; + + SystemBootParameters params; + params.filename = std::move(path); + System::BootSystem(std::move(params)); + }); +} + +void FullscreenUI::DoStartDisc() +{ + std::vector> devices = CDImage::GetDeviceList(); + if (devices.empty()) + { + ShowToast(std::string(), + FSUI_STR("Could not find any CD/DVD-ROM devices. Please ensure you have a drive connected and sufficient " + "permissions to access it.")); + return; + } + + // if there's only one, select it automatically + if (devices.size() == 1) + { + DoStartDisc(std::move(devices.front().first)); + return; + } + + ImGuiFullscreen::ChoiceDialogOptions options; + std::vector paths; + options.reserve(devices.size()); + paths.reserve(paths.size()); + for (auto& [path, name] : devices) + { + options.emplace_back(std::move(name), false); + paths.push_back(std::move(path)); + } + OpenChoiceDialog(FSUI_ICONSTR(ICON_FA_COMPACT_DISC, "Select Disc Drive"), false, std::move(options), + [paths = std::move(paths)](s32 index, const std::string&, bool) mutable { + if (index < 0) + return; + + DoStartDisc(std::move(paths[index])); + CloseChoiceDialog(); + QueueResetFocus(); + }); +} + void FullscreenUI::DoShutdown(bool save_state) { Host::RunOnCPUThread([save_state]() { Host::RequestSystemShutdown(false, save_state); }); @@ -1066,7 +1220,12 @@ void FullscreenUI::DoToggleAnalogMode() void FullscreenUI::DoRequestExit() { - Host::RunOnCPUThread([]() { Host::RequestExit(true); }); + Host::RunOnCPUThread([]() { Host::RequestExitApplication(true); }); +} + +void FullscreenUI::DoDesktopMode() +{ + Host::RunOnCPUThread([]() { Host::RequestExitBigPicture(); }); } void FullscreenUI::DoToggleFullscreen() @@ -1084,103 +1243,250 @@ void FullscreenUI::SwitchToLanding() QueueResetFocus(); } +void FullscreenUI::DrawLandingTemplate(ImVec2* menu_pos, ImVec2* menu_size) +{ + const ImGuiIO& io = ImGui::GetIO(); + const ImVec2 heading_size = ImVec2( + io.DisplaySize.x, LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY + LAYOUT_MENU_BUTTON_Y_PADDING * 2.0f + 2.0f)); + *menu_pos = ImVec2(0.0f, heading_size.y); + *menu_size = ImVec2(io.DisplaySize.x, io.DisplaySize.y - heading_size.y - LayoutScale(LAYOUT_FOOTER_HEIGHT)); + + if (BeginFullscreenWindow(ImVec2(0.0f, 0.0f), heading_size, "landing_heading", UIPrimaryColor)) + { + ImFont* const heading_font = g_large_font; + ImDrawList* const dl = ImGui::GetWindowDrawList(); + SmallString heading_str; + + ImGui::PushFont(heading_font); + ImGui::PushStyleColor(ImGuiCol_Text, UIPrimaryTextColor); + + // draw branding + { + const ImVec2 logo_pos = LayoutScale(LAYOUT_MENU_BUTTON_X_PADDING, LAYOUT_MENU_BUTTON_Y_PADDING); + const ImVec2 logo_size = LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); + dl->AddImage(GetCachedTexture("fullscreenui/duck.png"), logo_pos, logo_pos + logo_size); + dl->AddText(heading_font, heading_font->FontSize, + ImVec2(logo_pos.x + logo_size.x + LayoutScale(LAYOUT_MENU_BUTTON_X_PADDING), logo_pos.y), + ImGui::GetColorU32(ImGuiCol_Text), "DuckStation"); + } + + // draw time + ImVec2 time_pos; + { + heading_str.format(FSUI_FSTR("{:%H:%M}"), fmt::localtime(std::time(nullptr))); + + const ImVec2 time_size = heading_font->CalcTextSizeA(heading_font->FontSize, FLT_MAX, 0.0f, "00:00"); + time_pos = ImVec2(heading_size.x - LayoutScale(LAYOUT_MENU_BUTTON_X_PADDING) - time_size.x, + LayoutScale(LAYOUT_MENU_BUTTON_Y_PADDING)); + ImGui::RenderTextClipped(time_pos, time_pos + time_size, heading_str.c_str(), heading_str.end_ptr(), &time_size); + } + + // draw achievements info + if (Achievements::IsActive()) + { + const auto lock = Achievements::GetLock(); + const char* username = Achievements::GetLoggedInUserName(); + if (username) + { + const ImVec2 name_size = heading_font->CalcTextSizeA(heading_font->FontSize, FLT_MAX, 0.0f, username); + const ImVec2 name_pos = + ImVec2(time_pos.x - name_size.x - LayoutScale(LAYOUT_MENU_BUTTON_X_PADDING), time_pos.y); + ImGui::RenderTextClipped(name_pos, name_pos + name_size, username, nullptr, &name_size); + + // TODO: should we cache this? heap allocations bad... + std::string badge_path = Achievements::GetLoggedInUserBadgePath(); + if (!badge_path.empty()) [[likely]] + { + const ImVec2 badge_size = + LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); + const ImVec2 badge_pos = + ImVec2(name_pos.x - badge_size.x - LayoutScale(LAYOUT_MENU_BUTTON_X_PADDING), time_pos.y); + + dl->AddImage(reinterpret_cast(GetCachedTextureAsync(badge_path)), badge_pos, + badge_pos + badge_size); + } + } + } + + ImGui::PopStyleColor(); + ImGui::PopFont(); + } + EndFullscreenWindow(); +} + void FullscreenUI::DrawLandingWindow() { - BeginFullscreenColumns(nullptr, 0.0f, true); + ImVec2 menu_pos, menu_size; + DrawLandingTemplate(&menu_pos, &menu_size); - if (BeginFullscreenColumnWindow(0.0f, -710.0f, "logo", UIPrimaryDarkColor)) - { - const float image_size = LayoutScale(380.f); - ImGui::SetCursorPos(ImVec2((ImGui::GetWindowWidth() * 0.5f) - (image_size * 0.5f), - (ImGui::GetWindowHeight() * 0.5f) - (image_size * 0.5f))); - ImGui::Image(s_app_icon_texture.get(), ImVec2(image_size, image_size)); - } - EndFullscreenColumnWindow(); + ImGui::PushStyleColor(ImGuiCol_Text, UIBackgroundTextColor); - if (BeginFullscreenColumnWindow(-710.0f, 0.0f, "menu", UIBackgroundColor)) + if (BeginHorizontalMenu("landing_window", menu_pos, menu_size, 4)) { ResetFocusHere(); - BeginMenuButtons(7, 0.5f); - - if (MenuButton(FSUI_ICONSTR(ICON_FA_LIST, "Game List"), - FSUI_CSTR("Launch a game from images scanned from your game directories."))) + if (HorizontalMenuItem(GetCachedTexture("fullscreenui/address-book-new.png"), FSUI_CSTR("Game List"), + FSUI_CSTR("Launch a game from images scanned from your game directories."))) { SwitchToGameList(); } - if (MenuButton(FSUI_ICONSTR(ICON_FA_PLAY_CIRCLE, "Resume"), - FSUI_CSTR("Starts the console from where it was before it was last closed."))) + if (HorizontalMenuItem( + GetCachedTexture("fullscreenui/media-cdrom.png"), FSUI_CSTR("Start Game"), + FSUI_CSTR("Launch a game from a file, disc, or starts the console without any disc inserted."))) { - System::GetMostRecentResumeSaveStatePath(); - DoResume(); + s_current_main_window = MainWindowType::StartGame; + QueueResetFocus(); } - if (MenuButton(FSUI_ICONSTR(ICON_FA_FOLDER_OPEN, "Start File"), - FSUI_CSTR("Launch a game by selecting a file/disc image."))) + if (HorizontalMenuItem(GetCachedTexture("fullscreenui/applications-system.png"), FSUI_CSTR("Settings"), + FSUI_CSTR("Changes settings for the application."))) + { + SwitchToSettings(); + } + + if (HorizontalMenuItem(GetCachedTexture("fullscreenui/exit.png"), FSUI_CSTR("Exit"), + FSUI_CSTR("Return to desktop mode, or exit the application.")) || + (!AreAnyDialogsOpen() && WantsToCloseMenu())) + { + s_current_main_window = MainWindowType::Exit; + QueueResetFocus(); + } + } + EndHorizontalMenu(); + + ImGui::PopStyleColor(); + + if (!AreAnyDialogsOpen()) + { + if (ImGui::IsKeyPressed(ImGuiKey_GamepadStart, false) || ImGui::IsKeyPressed(ImGuiKey_F1, false)) + OpenAboutWindow(); + else if (ImGui::IsKeyPressed(ImGuiKey_NavGamepadMenu, false) || ImGui::IsKeyPressed(ImGuiKey_F3, false)) + DoResume(); + else if (ImGui::IsKeyPressed(ImGuiKey_NavGamepadInput, false) || ImGui::IsKeyPressed(ImGuiKey_F11, false)) + DoToggleFullscreen(); + } + + if (IsGamepadInputSource()) + { + SetFullscreenFooterText(std::array{std::make_pair(ICON_PF_BURGER_MENU, FSUI_VSTR("About")), + std::make_pair(ICON_PF_BUTTON_Y, FSUI_VSTR("Resume Last Session")), + std::make_pair(ICON_PF_BUTTON_X, FSUI_VSTR("Toggle Fullscreen")), + std::make_pair(ICON_PF_XBOX_DPAD_LEFT_RIGHT, FSUI_VSTR("Navigate")), + std::make_pair(ICON_PF_BUTTON_A, FSUI_VSTR("Select")), + std::make_pair(ICON_PF_BUTTON_B, FSUI_VSTR("Exit"))}); + } + else + { + SetFullscreenFooterText(std::array{ + std::make_pair(ICON_PF_F1, FSUI_VSTR("About")), std::make_pair(ICON_PF_F3, FSUI_VSTR("Resume Last Session")), + std::make_pair(ICON_PF_F11, FSUI_VSTR("Toggle Fullscreen")), + std::make_pair(ICON_PF_ARROW_LEFT ICON_PF_ARROW_RIGHT, FSUI_VSTR("Navigate")), + std::make_pair(ICON_PF_ENTER, FSUI_VSTR("Select")), std::make_pair(ICON_PF_ESC, FSUI_VSTR("Exit"))}); + } +} + +void FullscreenUI::DrawStartGameWindow() +{ + ImVec2 menu_pos, menu_size; + DrawLandingTemplate(&menu_pos, &menu_size); + + ImGui::PushStyleColor(ImGuiCol_Text, UIBackgroundTextColor); + + if (BeginHorizontalMenu("start_game_window", menu_pos, menu_size, 4)) + { + ResetFocusHere(); + + if (HorizontalMenuItem(GetCachedTexture("fullscreenui/start-file.png"), FSUI_CSTR("Start File"), + FSUI_CSTR("Launch a game by selecting a file/disc image."))) { DoStartFile(); } - if (MenuButton(FSUI_ICONSTR(ICON_FA_MICROCHIP, "Start BIOS"), - FSUI_CSTR("Start the console without any disc inserted."))) + if (HorizontalMenuItem(GetCachedTexture("fullscreenui/drive-cdrom.png"), FSUI_CSTR("Start Disc"), + FSUI_CSTR("Start a game from a disc in your PC's DVD drive."))) + { + DoStartDisc(); + } + + if (HorizontalMenuItem(GetCachedTexture("fullscreenui/start-bios.png"), FSUI_CSTR("Start BIOS"), + FSUI_CSTR("Start the console without any disc inserted."))) { DoStartBIOS(); } - if (MenuButton(FSUI_ICONSTR(ICON_FA_UNDO, "Load State"), FSUI_CSTR("Loads a global save state."))) + // https://www.iconpacks.net/free-icon/arrow-back-3783.html + if (HorizontalMenuItem(GetCachedTexture("fullscreenui/back-icon.png"), FSUI_CSTR("Back"), + FSUI_CSTR("Return to the previous menu.")) || + (!AreAnyDialogsOpen() && WantsToCloseMenu())) { + s_current_main_window = MainWindowType::Landing; + QueueResetFocus(); + } + } + EndHorizontalMenu(); + + ImGui::PopStyleColor(); + + if (!AreAnyDialogsOpen()) + { + if (ImGui::IsKeyPressed(ImGuiKey_NavGamepadMenu, false) || ImGui::IsKeyPressed(ImGuiKey_F1, false)) OpenSaveStateSelector(true); + } + + if (IsGamepadInputSource()) + { + SetFullscreenFooterText(std::array{std::make_pair(ICON_PF_XBOX_DPAD_LEFT_RIGHT, FSUI_VSTR("Navigate")), + std::make_pair(ICON_PF_BUTTON_Y, FSUI_VSTR("Load Global State")), + std::make_pair(ICON_PF_BUTTON_A, FSUI_VSTR("Select")), + std::make_pair(ICON_PF_BUTTON_B, FSUI_VSTR("Back"))}); + } + else + { + SetFullscreenFooterText(std::array{std::make_pair(ICON_PF_ARROW_LEFT ICON_PF_ARROW_RIGHT, FSUI_VSTR("Navigate")), + std::make_pair(ICON_PF_F1, FSUI_VSTR("Load Global State")), + std::make_pair(ICON_PF_ENTER, FSUI_VSTR("Select")), + std::make_pair(ICON_PF_ESC, FSUI_VSTR("Back"))}); + } +} + +void FullscreenUI::DrawExitWindow() +{ + ImVec2 menu_pos, menu_size; + DrawLandingTemplate(&menu_pos, &menu_size); + + ImGui::PushStyleColor(ImGuiCol_Text, UIBackgroundTextColor); + + if (BeginHorizontalMenu("exit_window", menu_pos, menu_size, 3)) + { + ResetFocusHere(); + + // https://www.iconpacks.net/free-icon/arrow-back-3783.html + if (HorizontalMenuItem(GetCachedTexture("fullscreenui/back-icon.png"), FSUI_CSTR("Back"), + FSUI_CSTR("Return to the previous menu.")) || + WantsToCloseMenu()) + { + s_current_main_window = MainWindowType::Landing; + QueueResetFocus(); } - if (MenuButton(FSUI_ICONSTR(ICON_FA_TOOLBOX, "Settings"), FSUI_CSTR("Change settings for the emulator."))) - SwitchToSettings(); - - if (MenuButton(FSUI_ICONSTR(ICON_FA_SIGN_OUT_ALT, "Exit"), FSUI_CSTR("Exits the program."))) + if (HorizontalMenuItem(GetCachedTexture("fullscreenui/exit.png"), FSUI_CSTR("Exit DuckStation"), + FSUI_CSTR("Completely exits the application, returning you to your desktop."))) { DoRequestExit(); } + if (HorizontalMenuItem(GetCachedTexture("fullscreenui/desktop-mode.png"), FSUI_CSTR("Desktop Mode"), + FSUI_CSTR("Exits Big Picture mode, returning to the desktop interface."))) { - ImVec2 fullscreen_pos; - if (FloatingButton(ICON_FA_WINDOW_CLOSE, 0.0f, 0.0f, -1.0f, -1.0f, 1.0f, 0.0f, true, g_large_font, - &fullscreen_pos)) - { - DoRequestExit(); - } - - if (FloatingButton(ICON_FA_EXPAND, fullscreen_pos.x, 0.0f, -1.0f, -1.0f, -1.0f, 0.0f, true, g_large_font, - &fullscreen_pos)) - { - DoToggleFullscreen(); - } - - if (FloatingButton(ICON_FA_QUESTION_CIRCLE, fullscreen_pos.x, 0.0f, -1.0f, -1.0f, -1.0f, 0.0f, true, g_large_font, - &fullscreen_pos)) - { - OpenAboutWindow(); - } - - if (FloatingButton(ICON_FA_LIGHTBULB, fullscreen_pos.x, 0.0f, -1.0f, -1.0f, -1.0f, 0.0f, true, g_large_font, - &fullscreen_pos)) - { - ToggleTheme(); - } + DoDesktopMode(); } - - EndMenuButtons(); - - const ImVec2 rev_size(g_medium_font->CalcTextSizeA(g_medium_font->FontSize, FLT_MAX, 0.0f, g_scm_tag_str)); - ImGui::SetCursorPos(ImVec2(ImGui::GetWindowWidth() - rev_size.x - LayoutScale(20.0f), - ImGui::GetWindowHeight() - rev_size.y - LayoutScale(20.0f))); - ImGui::PushFont(g_medium_font); - ImGui::TextUnformatted(g_scm_tag_str); - ImGui::PopFont(); } + EndHorizontalMenu(); - EndFullscreenColumnWindow(); + ImGui::PopStyleColor(); - EndFullscreenColumns(); + SetStandardSelectionFooterText(true); } bool FullscreenUI::IsEditingGameSettings(SettingsInterface* bsi) @@ -2508,15 +2814,20 @@ void FullscreenUI::DrawSettingsWindow() if (!ImGui::IsPopupOpen(0u, ImGuiPopupFlags_AnyPopup)) { - if (ImGui::IsKeyPressed(ImGuiKey_NavGamepadTweakSlow, false)) + if (ImGui::IsKeyPressed(ImGuiKey_GamepadDpadLeft, true) || + ImGui::IsKeyPressed(ImGuiKey_NavGamepadTweakSlow, true) || ImGui::IsKeyPressed(ImGuiKey_LeftArrow, true)) { index = (index == 0) ? (count - 1) : (index - 1); s_settings_page = pages[index]; + QueueResetFocus(); } - else if (ImGui::IsKeyPressed(ImGuiKey_NavGamepadTweakFast, false)) + else if (ImGui::IsKeyPressed(ImGuiKey_GamepadDpadRight, true) || + ImGui::IsKeyPressed(ImGuiKey_NavGamepadTweakFast, true) || + ImGui::IsKeyPressed(ImGuiKey_RightArrow, true)) { index = (index + 1) % count; s_settings_page = pages[index]; + QueueResetFocus(); } } @@ -2543,17 +2854,20 @@ void FullscreenUI::DrawSettingsWindow() EndFullscreenWindow(); - if (BeginFullscreenWindow(ImVec2(0.0f, heading_size.y), ImVec2(io.DisplaySize.x, io.DisplaySize.y - heading_size.y), - "settings_parent", - ImVec4(UIBackgroundColor.x, UIBackgroundColor.y, UIBackgroundColor.z, bg_alpha))) + // we have to do this here, because otherwise it uses target, and jumps a frame later. + if (IsFocusResetQueued()) + ImGui::SetNextWindowScroll(ImVec2(0.0f, 0.0f)); + + if (BeginFullscreenWindow( + ImVec2(0.0f, heading_size.y), + ImVec2(io.DisplaySize.x, io.DisplaySize.y - heading_size.y - LayoutScale(LAYOUT_FOOTER_HEIGHT)), + TinyString::from_format("settings_page_{}", static_cast(s_settings_page)).c_str(), + ImVec4(UIBackgroundColor.x, UIBackgroundColor.y, UIBackgroundColor.z, bg_alpha))) { ResetFocusHere(); - if (WantsToCloseMenu()) - { - if (ImGui::IsWindowFocused()) - ReturnToPreviousWindow(); - } + if (ImGui::IsWindowFocused() && WantsToCloseMenu()) + ReturnToPreviousWindow(); auto lock = Host::GetSettingsLock(); @@ -2617,6 +2931,21 @@ void FullscreenUI::DrawSettingsWindow() } EndFullscreenWindow(); + + if (IsGamepadInputSource()) + { + SetFullscreenFooterText(std::array{std::make_pair(ICON_PF_XBOX_DPAD_LEFT_RIGHT, FSUI_VSTR("Change Page")), + std::make_pair(ICON_PF_XBOX_DPAD_UP_DOWN, FSUI_VSTR("Navigate")), + std::make_pair(ICON_PF_BUTTON_A, FSUI_VSTR("Select")), + std::make_pair(ICON_PF_BUTTON_B, FSUI_VSTR("Back"))}); + } + else + { + SetFullscreenFooterText(std::array{std::make_pair(ICON_PF_ARROW_LEFT ICON_PF_ARROW_RIGHT, FSUI_VSTR("Change Page")), + std::make_pair(ICON_PF_ARROW_UP ICON_PF_ARROW_DOWN, FSUI_VSTR("Navigate")), + std::make_pair(ICON_PF_ENTER, FSUI_VSTR("Select")), + std::make_pair(ICON_PF_ESC, FSUI_VSTR("Back"))}); + } } void FullscreenUI::DrawSummarySettingsPage() @@ -3155,11 +3484,11 @@ void FullscreenUI::DoSaveInputProfile(const std::string& name) void FullscreenUI::DoSaveNewInputProfile() { OpenInputStringDialog(FSUI_ICONSTR(ICON_FA_SAVE, "Save Profile"), - FSUI_STR("Enter the name of the input profile you wish to create."), std::string(), - FSUI_ICONSTR(ICON_FA_FOLDER_PLUS, "Create"), [](std::string title) { - if (!title.empty()) - DoSaveInputProfile(title); - }); + FSUI_STR("Enter the name of the input profile you wish to create."), std::string(), + FSUI_ICONSTR(ICON_FA_FOLDER_PLUS, "Create"), [](std::string title) { + if (!title.empty()) + DoSaveInputProfile(title); + }); } void FullscreenUI::DoSaveInputProfile() @@ -4555,7 +4884,7 @@ void FullscreenUI::DrawAchievementsSettingsPage() TinyString ts_string; ts_string.format( - "{:%Y-%m-%d %H:%M:%S}", + FSUI_FSTR("{:%Y-%m-%d %H:%M:%S}"), fmt::localtime( StringUtil::FromChars(bsi->GetTinyStringValue("Cheevos", "LoginTimestamp", "0")).value_or(0))); ActiveButton( @@ -4754,7 +5083,7 @@ void FullscreenUI::DrawPauseMenu() g_medium_font->CalcTextSizeA(g_medium_font->FontSize, std::numeric_limits::max(), -1.0f, buffer.c_str())); ImVec2 title_pos(display_size.x - LayoutScale(10.0f + image_width + 20.0f) - title_size.x, - display_size.y - LayoutScale(10.0f + image_height)); + display_size.y - LayoutScale(LAYOUT_FOOTER_HEIGHT) - LayoutScale(10.0f + image_height)); ImVec2 subtitle_pos(display_size.x - LayoutScale(10.0f + image_width + 20.0f) - subtitle_size.x, title_pos.y + g_large_font->FontSize + LayoutScale(4.0f)); @@ -4787,7 +5116,8 @@ void FullscreenUI::DrawPauseMenu() GPUTexture* const cover = GetCoverForCurrentGame(); const ImVec2 image_min(display_size.x - LayoutScale(10.0f + image_width), - display_size.y - LayoutScale(10.0f + image_height) - rp_height); + display_size.y - LayoutScale(LAYOUT_FOOTER_HEIGHT) - LayoutScale(10.0f + image_height) - + rp_height); const ImVec2 image_max(image_min.x + LayoutScale(image_width), image_min.y + LayoutScale(image_height) + rp_height); const ImRect image_rect(CenterImage(ImRect(image_min, image_max), ImVec2(static_cast(cover->GetWidth()), static_cast(cover->GetHeight())))); @@ -4826,7 +5156,7 @@ void FullscreenUI::DrawPauseMenu() } const ImVec2 window_size(LayoutScale(500.0f, LAYOUT_SCREEN_HEIGHT)); - const ImVec2 window_pos(0.0f, display_size.y - window_size.y); + const ImVec2 window_pos(0.0f, display_size.y - LayoutScale(LAYOUT_FOOTER_HEIGHT) - window_size.y); if (BeginFullscreenWindow(window_pos, window_size, "pause_menu", ImVec4(0.0f, 0.0f, 0.0f, 0.0f), 0.0f, 10.0f, ImGuiWindowFlags_NoBackground)) @@ -4978,10 +5308,23 @@ void FullscreenUI::DrawPauseMenu() } Achievements::DrawPauseMenuOverlays(); + + if (IsGamepadInputSource()) + { + SetFullscreenFooterText(std::array{std::make_pair(ICON_PF_XBOX_DPAD_UP_DOWN, FSUI_VSTR("Change Selection")), + std::make_pair(ICON_PF_BUTTON_A, FSUI_VSTR("Select")), + std::make_pair(ICON_PF_BUTTON_B, FSUI_VSTR("Return To Game"))}); + } + else + { + SetFullscreenFooterText(std::array{ + std::make_pair(ICON_PF_ARROW_UP ICON_PF_ARROW_DOWN, FSUI_VSTR("Change Selection")), + std::make_pair(ICON_PF_ENTER, FSUI_VSTR("Select")), std::make_pair(ICON_PF_ESC, FSUI_VSTR("Return To Game"))}); + } } -void FullscreenUI::InitializePlaceholderSaveStateListEntry(SaveStateListEntry* li, const std::string& title, - const std::string& serial, s32 slot, bool global) +void FullscreenUI::InitializePlaceholderSaveStateListEntry(SaveStateListEntry* li, const std::string& serial, s32 slot, + bool global) { li->title = (global || slot > 0) ? fmt::format(global ? FSUI_FSTR("Global Slot {0}##global_slot_{0}") : FSUI_FSTR("Game Slot {0}##game_slot_{0}"), @@ -4995,18 +5338,26 @@ void FullscreenUI::InitializePlaceholderSaveStateListEntry(SaveStateListEntry* l li->global = global; } -bool FullscreenUI::InitializeSaveStateListEntry(SaveStateListEntry* li, const std::string& title, - const std::string& serial, s32 slot, bool global) +bool FullscreenUI::InitializeSaveStateListEntryFromSerial(SaveStateListEntry* li, const std::string& serial, s32 slot, + bool global) { - std::string filename(global ? System::GetGlobalSaveStateFileName(slot) : - System::GetGameSaveStateFileName(serial, slot)); - std::optional ssi(System::GetExtendedSaveStateInfo(filename.c_str())); - if (!ssi.has_value()) + const std::string path = + (global ? System::GetGlobalSaveStateFileName(slot) : System::GetGameSaveStateFileName(serial, slot)); + if (!InitializeSaveStateListEntryFromPath(li, path.c_str(), slot, global)) { - InitializePlaceholderSaveStateListEntry(li, title, serial, slot, global); + InitializePlaceholderSaveStateListEntry(li, serial, slot, global); return false; } + return true; +} + +bool FullscreenUI::InitializeSaveStateListEntryFromPath(SaveStateListEntry* li, std::string path, s32 slot, bool global) +{ + std::optional ssi(System::GetExtendedSaveStateInfo(path.c_str())); + if (!ssi.has_value()) + return false; + if (global) { li->title = fmt::format(FSUI_FSTR("Global Slot {0} - {1}##global_slot_{0}"), slot, ssi->serial); @@ -5019,7 +5370,7 @@ bool FullscreenUI::InitializeSaveStateListEntry(SaveStateListEntry* li, const st li->summary = fmt::format(FSUI_FSTR("Saved {:%c}"), fmt::localtime(ssi->timestamp)); li->timestamp = ssi->timestamp; li->slot = slot; - li->path = std::move(filename); + li->path = std::move(path); li->global = global; PopulateSaveStateScreenshot(li, &ssi.value()); @@ -5078,7 +5429,7 @@ u32 FullscreenUI::PopulateSaveStateListEntries(const std::string& title, const s for (s32 i = 1; i <= System::PER_GAME_SAVE_STATE_SLOTS; i++) { SaveStateListEntry li; - if (InitializeSaveStateListEntry(&li, title, serial, i, false) || !s_save_state_selector_loading) + if (InitializeSaveStateListEntryFromSerial(&li, serial, i, false) || !s_save_state_selector_loading) s_save_state_selector_slots.push_back(std::move(li)); } } @@ -5086,7 +5437,7 @@ u32 FullscreenUI::PopulateSaveStateListEntries(const std::string& title, const s for (s32 i = 1; i <= System::GLOBAL_SAVE_STATE_SLOTS; i++) { SaveStateListEntry li; - if (InitializeSaveStateListEntry(&li, title, serial, i, true) || !s_save_state_selector_loading) + if (InitializeSaveStateListEntryFromSerial(&li, serial, i, true) || !s_save_state_selector_loading) s_save_state_selector_slots.push_back(std::move(li)); } @@ -5142,7 +5493,7 @@ void FullscreenUI::DrawSaveStateSelector(bool is_loading) ImGuiIO& io = ImGui::GetIO(); ImGui::SetNextWindowPos(ImVec2(0.0f, 0.0f)); - ImGui::SetNextWindowSize(io.DisplaySize); + ImGui::SetNextWindowSize(io.DisplaySize - LayoutScale(0.0f, LAYOUT_FOOTER_HEIGHT)); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); @@ -5400,9 +5751,9 @@ void FullscreenUI::DrawSaveStateSelector(bool is_loading) closed = true; } - - if (hovered && - (ImGui::IsItemClicked(ImGuiMouseButton_Right) || ImGui::IsKeyPressed(ImGuiKey_NavGamepadMenu, false))) + else if (hovered && + (ImGui::IsItemClicked(ImGuiMouseButton_Right) || ImGui::IsKeyPressed(ImGuiKey_NavGamepadMenu, false) || + ImGui::IsKeyPressed(ImGuiKey_F1, false))) { s_save_state_selector_submenu_index = static_cast(i); } @@ -5442,12 +5793,30 @@ void FullscreenUI::DrawSaveStateSelector(bool is_loading) CloseSaveStateSelector(); ReturnToPreviousWindow(); } + else + { + if (IsGamepadInputSource()) + { + SetFullscreenFooterText(std::array{std::make_pair(ICON_PF_XBOX_DPAD, FSUI_VSTR("Select State")), + std::make_pair(ICON_PF_BUTTON_Y, FSUI_VSTR("Delete State")), + std::make_pair(ICON_PF_BUTTON_A, FSUI_VSTR("Load State")), + std::make_pair(ICON_PF_BUTTON_B, FSUI_VSTR("Cancel"))}); + } + else + { + SetFullscreenFooterText(std::array{ + std::make_pair(ICON_PF_ARROW_UP ICON_PF_ARROW_DOWN ICON_PF_ARROW_LEFT ICON_PF_ARROW_RIGHT, + FSUI_VSTR("Select State")), + std::make_pair(ICON_PF_F1, FSUI_VSTR("Delete State")), std::make_pair(ICON_PF_ENTER, FSUI_VSTR("Load State")), + std::make_pair(ICON_PF_ESC, FSUI_VSTR("Cancel"))}); + } + } } bool FullscreenUI::OpenLoadStateSelectorForGameResume(const GameList::Entry* entry) { SaveStateListEntry slentry; - if (!InitializeSaveStateListEntry(&slentry, entry->title, entry->serial, -1, false)) + if (!InitializeSaveStateListEntryFromSerial(&slentry, entry->serial, -1, false)) return false; CloseSaveStateSelector(); @@ -5541,6 +5910,10 @@ void FullscreenUI::DrawResumeStateSelector() s_save_state_selector_resuming = false; s_save_state_selector_game_path = {}; } + else + { + SetStandardSelectionFooterText(false); + } } void FullscreenUI::DoLoadState(std::string path) @@ -5677,38 +6050,24 @@ void FullscreenUI::DrawGameListWindow() if (BeginFullscreenWindow(ImVec2(0.0f, 0.0f), heading_size, "gamelist_view", MulAlpha(UIPrimaryColor, bg_alpha))) { static constexpr float ITEM_WIDTH = 25.0f; - static constexpr const char* icons[] = {ICON_FA_BORDER_ALL, ICON_FA_LIST, ICON_FA_COG}; - static constexpr const char* titles[] = {FSUI_NSTR("Game Grid"), FSUI_NSTR("Game List"), - FSUI_NSTR("Game List Settings")}; + static constexpr const char* icons[] = {ICON_FA_BORDER_ALL, ICON_FA_LIST}; + static constexpr const char* titles[] = {FSUI_NSTR("Game Grid"), FSUI_NSTR("Game List")}; static constexpr u32 count = static_cast(std::size(titles)); BeginNavBar(); - if (!ImGui::IsPopupOpen(0u, ImGuiPopupFlags_AnyPopup)) - { - if (ImGui::IsKeyPressed(ImGuiKey_NavGamepadTweakSlow, false)) - { - s_game_list_page = static_cast( - (s_game_list_page == static_cast(0)) ? (count - 1) : (static_cast(s_game_list_page) - 1)); - } - else if (ImGui::IsKeyPressed(ImGuiKey_NavGamepadTweakFast, false)) - { - s_game_list_page = static_cast((static_cast(s_game_list_page) + 1) % count); - } - } - if (NavButton(ICON_FA_BACKWARD, true, true)) ReturnToPreviousWindow(); - NavTitle(Host::TranslateToCString(TR_CONTEXT, titles[static_cast(s_game_list_page)])); + NavTitle(Host::TranslateToCString(TR_CONTEXT, titles[static_cast(s_game_list_view)])); RightAlignNavButtons(count, ITEM_WIDTH, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); for (u32 i = 0; i < count; i++) { - if (NavButton(icons[i], static_cast(i) == s_game_list_page, true, ITEM_WIDTH, + if (NavButton(icons[i], static_cast(i) == s_game_list_view, true, ITEM_WIDTH, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY)) { - s_game_list_page = static_cast(i); + s_game_list_view = static_cast(i); } } @@ -5717,25 +6076,51 @@ void FullscreenUI::DrawGameListWindow() EndFullscreenWindow(); - switch (s_game_list_page) + if (ImGui::IsKeyPressed(ImGuiKey_NavGamepadInput, false) || ImGui::IsKeyPressed(ImGuiKey_F1, false)) { - case GameListPage::Grid: + s_game_list_view = (s_game_list_view == GameListView::Grid) ? GameListView::List : GameListView::Grid; + } + else if (ImGui::IsKeyPressed(ImGuiKey_GamepadStart, false) || ImGui::IsKeyPressed(ImGuiKey_F2)) + { + s_current_main_window = MainWindowType::GameListSettings; + QueueResetFocus(); + } + + switch (s_game_list_view) + { + case GameListView::Grid: DrawGameGrid(heading_size); break; - case GameListPage::List: + case GameListView::List: DrawGameList(heading_size); break; - case GameListPage::Settings: - DrawGameListSettingsPage(heading_size); - break; default: break; } + + if (IsGamepadInputSource()) + { + SetFullscreenFooterText(std::array{std::make_pair(ICON_PF_XBOX_DPAD, FSUI_VSTR("Select Game")), + std::make_pair(ICON_PF_BUTTON_X, FSUI_VSTR("Change View")), + std::make_pair(ICON_PF_BURGER_MENU, FSUI_VSTR("Settings")), + std::make_pair(ICON_PF_BUTTON_Y, FSUI_VSTR("Launch Options")), + std::make_pair(ICON_PF_BUTTON_A, FSUI_VSTR("Start Game")), + std::make_pair(ICON_PF_BUTTON_B, FSUI_VSTR("Back"))}); + } + else + { + SetFullscreenFooterText(std::array{ + std::make_pair(ICON_PF_ARROW_UP ICON_PF_ARROW_DOWN ICON_PF_ARROW_LEFT ICON_PF_ARROW_RIGHT, + FSUI_VSTR("Select Game")), + std::make_pair(ICON_PF_F1, FSUI_VSTR("Change View")), std::make_pair(ICON_PF_F2, FSUI_VSTR("Settings")), + std::make_pair(ICON_PF_F3, FSUI_VSTR("Launch Options")), std::make_pair(ICON_PF_ENTER, FSUI_VSTR("Start Game")), + std::make_pair(ICON_PF_ESC, FSUI_VSTR("Back"))}); + } } void FullscreenUI::DrawGameList(const ImVec2& heading_size) { - if (!BeginFullscreenColumns(nullptr, heading_size.y, true)) + if (!BeginFullscreenColumns(nullptr, heading_size.y, true, true)) { EndFullscreenColumns(); return; @@ -5808,7 +6193,8 @@ void FullscreenUI::DrawGameList(const ImVec2& heading_size) selected_entry = entry; if (selected_entry && - (ImGui::IsItemClicked(ImGuiMouseButton_Right) || ImGui::IsKeyPressed(ImGuiKey_NavGamepadMenu, false))) + (ImGui::IsItemClicked(ImGuiMouseButton_Right) || ImGui::IsKeyPressed(ImGuiKey_NavGamepadMenu, false) || + ImGui::IsKeyPressed(ImGuiKey_F3, false))) { HandleGameListOptions(selected_entry); } @@ -5829,7 +6215,7 @@ void FullscreenUI::DrawGameList(const ImVec2& heading_size) CenterImage(LayoutScale(ImVec2(350.0f, 350.0f)), ImVec2(static_cast(cover_texture->GetWidth()), static_cast(cover_texture->GetHeight())))); - ImGui::SetCursorPos(LayoutScale(ImVec2(90.0f, 50.0f)) + image_rect.Min); + ImGui::SetCursorPos(LayoutScale(ImVec2(90.0f, 0.0f)) + image_rect.Min); ImGui::Image(selected_entry ? GetGameListCover(selected_entry) : GetTextureForGameListEntryType(GameList::EntryType::Count), image_rect.GetSize()); @@ -5838,7 +6224,7 @@ void FullscreenUI::DrawGameList(const ImVec2& heading_size) const float work_width = ImGui::GetCurrentWindow()->WorkRect.GetWidth(); constexpr float field_margin_y = 10.0f; constexpr float start_x = 50.0f; - float text_y = 425.0f; + float text_y = 400.0f; float text_width; PushPrimaryColor(); @@ -5941,8 +6327,10 @@ void FullscreenUI::DrawGameList(const ImVec2& heading_size) void FullscreenUI::DrawGameGrid(const ImVec2& heading_size) { ImGuiIO& io = ImGui::GetIO(); - if (!BeginFullscreenWindow(ImVec2(0.0f, heading_size.y), ImVec2(io.DisplaySize.x, io.DisplaySize.y - heading_size.y), - "game_grid", UIBackgroundColor)) + if (!BeginFullscreenWindow( + ImVec2(0.0f, heading_size.y), + ImVec2(io.DisplaySize.x, io.DisplaySize.y - heading_size.y - LayoutScale(LAYOUT_FOOTER_HEIGHT)), "game_grid", + UIBackgroundColor)) { EndFullscreenWindow(); return; @@ -6028,7 +6416,8 @@ void FullscreenUI::DrawGameGrid(const ImVec2& heading_size) HandleGameListActivate(entry); } else if (hovered && - (ImGui::IsItemClicked(ImGuiMouseButton_Right) || ImGui::IsKeyPressed(ImGuiKey_NavGamepadMenu, false))) + (ImGui::IsItemClicked(ImGuiMouseButton_Right) || ImGui::IsKeyPressed(ImGuiKey_NavGamepadMenu, false) || + ImGui::IsKeyPressed(ImGuiKey_F3, false))) { HandleGameListOptions(entry); } @@ -6105,11 +6494,34 @@ void FullscreenUI::HandleGameListOptions(const GameList::Entry* entry) }); } -void FullscreenUI::DrawGameListSettingsPage(const ImVec2& heading_size) +void FullscreenUI::DrawGameListSettingsWindow() { - const ImGuiIO& io = ImGui::GetIO(); - if (!BeginFullscreenWindow(ImVec2(0.0f, heading_size.y), ImVec2(io.DisplaySize.x, io.DisplaySize.y - heading_size.y), - "settings_parent", UIBackgroundColor)) + ImGuiIO& io = ImGui::GetIO(); + ImVec2 heading_size = ImVec2( + io.DisplaySize.x, LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY + LAYOUT_MENU_BUTTON_Y_PADDING * 2.0f + 2.0f)); + + const float bg_alpha = System::IsValid() ? 0.90f : 1.0f; + + if (BeginFullscreenWindow(ImVec2(0.0f, 0.0f), heading_size, "gamelist_view", MulAlpha(UIPrimaryColor, bg_alpha))) + { + BeginNavBar(); + + if (NavButton(ICON_FA_BACKWARD, true, true)) + { + s_current_main_window = MainWindowType::GameList; + QueueResetFocus(); + } + + NavTitle(FSUI_CSTR("Game List Settings")); + EndNavBar(); + } + + EndFullscreenWindow(); + + if (!BeginFullscreenWindow( + ImVec2(0.0f, heading_size.y), + ImVec2(io.DisplaySize.x, io.DisplaySize.y - heading_size.y - LayoutScale(LAYOUT_FOOTER_HEIGHT)), + "settings_parent", UIBackgroundColor)) { EndFullscreenWindow(); return; @@ -6118,7 +6530,10 @@ void FullscreenUI::DrawGameListSettingsPage(const ImVec2& heading_size) if (WantsToCloseMenu()) { if (ImGui::IsWindowFocused()) - ReturnToPreviousWindow(); + { + s_current_main_window = MainWindowType::GameList; + QueueResetFocus(); + } } auto lock = Host::GetSettingsLock(); @@ -6257,12 +6672,14 @@ void FullscreenUI::DrawGameListSettingsPage(const ImVec2& heading_size) EndMenuButtons(); EndFullscreenWindow(); + + SetStandardSelectionFooterText(true); } void FullscreenUI::SwitchToGameList() { s_current_main_window = MainWindowType::GameList; - s_game_list_page = static_cast(Host::GetBaseIntSettingValue("Main", "DefaultFullscreenUIGameView", 0)); + s_game_list_view = static_cast(Host::GetBaseIntSettingValue("Main", "DefaultFullscreenUIGameView", 0)); { auto lock = Host::GetSettingsLock(); PopulateGameListDirectoryCache(Host::Internal::GetBaseSettingsLayer()); @@ -6343,13 +6760,13 @@ void FullscreenUI::CopyTextToClipboard(std::string title, const std::string_view void FullscreenUI::DrawAboutWindow() { - ImGui::SetNextWindowSize(LayoutScale(1000.0f, 510.0f)); + ImGui::SetNextWindowSize(LayoutScale(1000.0f, 540.0f)); ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f)); ImGui::OpenPopup(FSUI_CSTR("About DuckStation")); ImGui::PushFont(g_large_font); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f)); - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(10.0f, 10.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(30.0f, 30.0f)); if (ImGui::BeginPopupModal(FSUI_CSTR("About DuckStation"), &s_about_window_open, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize)) @@ -6358,8 +6775,7 @@ void FullscreenUI::DrawAboutWindow() FSUI_CSTR("DuckStation is a free and open-source simulator/emulator of the Sony PlayStation(TM) " "console, focusing on playability, speed, and long-term maintainability.")); ImGui::NewLine(); - ImGui::TextWrapped( - "%s", FSUI_CSTR("Contributor List: https://github.com/stenzek/duckstation/blob/master/CONTRIBUTORS.md")); + ImGui::TextWrapped("Version: %s", g_scm_tag_str); ImGui::NewLine(); ImGui::TextWrapped( "%s", FSUI_CSTR("Duck icon by icons8 (https://icons8.com/icon/74847/platforms.undefined.short-title)")); @@ -6373,16 +6789,21 @@ void FullscreenUI::DrawAboutWindow() BeginMenuButtons(); if (ActiveButton(FSUI_ICONSTR(ICON_FA_GLOBE, "GitHub Repository"), false)) ExitFullscreenAndOpenURL("https://github.com/stenzek/duckstation/"); - if (ActiveButton(FSUI_ICONSTR(ICON_FA_BUG, "Issue Tracker"), false)) - ExitFullscreenAndOpenURL("https://github.com/stenzek/duckstation/issues"); if (ActiveButton(FSUI_ICONSTR(ICON_FA_COMMENT, "Discord Server"), false)) - ExitFullscreenAndOpenURL("https://discord.gg/Buktv3t"); + ExitFullscreenAndOpenURL("https://www.duckstation.org/discord.html"); + if (ActiveButton(FSUI_ICONSTR(ICON_FA_PEOPLE_CARRY, "Contributor List"), false)) + ExitFullscreenAndOpenURL("https://github.com/stenzek/duckstation/blob/master/CONTRIBUTORS.md"); - if (ActiveButton(FSUI_ICONSTR(ICON_FA_WINDOW_CLOSE, "Close"), false)) + if (ActiveButton(FSUI_ICONSTR(ICON_FA_WINDOW_CLOSE, "Close"), false) || WantsToCloseMenu()) { ImGui::CloseCurrentPopup(); s_about_window_open = false; } + else + { + SetStandardSelectionFooterText(true); + } + EndMenuButtons(); ImGui::EndPopup(); @@ -6411,7 +6832,10 @@ void FullscreenUI::OpenAchievementsWindow() } if (s_current_main_window != MainWindowType::PauseMenu) + { PauseForMenuOpen(false); + ForceKeyNavEnabled(); + } s_current_main_window = MainWindowType::Achievements; QueueResetFocus(); @@ -6442,7 +6866,10 @@ void FullscreenUI::OpenLeaderboardsWindow() } if (s_current_main_window != MainWindowType::PauseMenu) + { PauseForMenuOpen(false); + ForceKeyNavEnabled(); + } s_current_main_window = MainWindowType::Leaderboards; QueueResetFocus(); @@ -6533,6 +6960,7 @@ TRANSLATE_NOOP("FullscreenUI", "9x"); TRANSLATE_NOOP("FullscreenUI", "9x (18x Speed)"); TRANSLATE_NOOP("FullscreenUI", "9x (for 4K)"); TRANSLATE_NOOP("FullscreenUI", "A resume save state created at %s was found.\n\nDo you want to load this save and continue?"); +TRANSLATE_NOOP("FullscreenUI", "About"); TRANSLATE_NOOP("FullscreenUI", "About DuckStation"); TRANSLATE_NOOP("FullscreenUI", "Account"); TRANSLATE_NOOP("FullscreenUI", "Achievement Notifications"); @@ -6575,6 +7003,7 @@ TRANSLATE_NOOP("FullscreenUI", "BIOS Selection"); TRANSLATE_NOOP("FullscreenUI", "BIOS Settings"); TRANSLATE_NOOP("FullscreenUI", "BIOS for {}"); TRANSLATE_NOOP("FullscreenUI", "BIOS to use when emulating {} consoles."); +TRANSLATE_NOOP("FullscreenUI", "Back"); TRANSLATE_NOOP("FullscreenUI", "Back To Pause Menu"); TRANSLATE_NOOP("FullscreenUI", "Backend Settings"); TRANSLATE_NOOP("FullscreenUI", "Behavior"); @@ -6585,7 +7014,10 @@ TRANSLATE_NOOP("FullscreenUI", "CPU Emulation"); TRANSLATE_NOOP("FullscreenUI", "CPU Mode"); TRANSLATE_NOOP("FullscreenUI", "Cancel"); TRANSLATE_NOOP("FullscreenUI", "Change Disc"); -TRANSLATE_NOOP("FullscreenUI", "Change settings for the emulator."); +TRANSLATE_NOOP("FullscreenUI", "Change Page"); +TRANSLATE_NOOP("FullscreenUI", "Change Selection"); +TRANSLATE_NOOP("FullscreenUI", "Change View"); +TRANSLATE_NOOP("FullscreenUI", "Changes settings for the application."); TRANSLATE_NOOP("FullscreenUI", "Changes the aspect ratio used to display the console's output to the screen."); TRANSLATE_NOOP("FullscreenUI", "Cheat List"); TRANSLATE_NOOP("FullscreenUI", "Chooses the backend to use for rendering the console/game visuals."); @@ -6602,6 +7034,7 @@ TRANSLATE_NOOP("FullscreenUI", "Close Game"); TRANSLATE_NOOP("FullscreenUI", "Close Menu"); TRANSLATE_NOOP("FullscreenUI", "Compatibility Rating"); TRANSLATE_NOOP("FullscreenUI", "Compatibility: "); +TRANSLATE_NOOP("FullscreenUI", "Completely exits the application, returning you to your desktop."); TRANSLATE_NOOP("FullscreenUI", "Configuration"); TRANSLATE_NOOP("FullscreenUI", "Confirm Power Off"); TRANSLATE_NOOP("FullscreenUI", "Console Settings"); @@ -6622,6 +7055,7 @@ TRANSLATE_NOOP("FullscreenUI", "Copies the current global settings to this game. TRANSLATE_NOOP("FullscreenUI", "Copies the global controller configuration to this game."); TRANSLATE_NOOP("FullscreenUI", "Copy Global Settings"); TRANSLATE_NOOP("FullscreenUI", "Copy Settings"); +TRANSLATE_NOOP("FullscreenUI", "Could not find any CD/DVD-ROM devices. Please ensure you have a drive connected and sufficient permissions to access it."); TRANSLATE_NOOP("FullscreenUI", "Cover Settings"); TRANSLATE_NOOP("FullscreenUI", "Covers Directory"); TRANSLATE_NOOP("FullscreenUI", "Create"); @@ -6640,6 +7074,7 @@ TRANSLATE_NOOP("FullscreenUI", "Deinterlacing Mode"); TRANSLATE_NOOP("FullscreenUI", "Delete Save"); TRANSLATE_NOOP("FullscreenUI", "Delete State"); TRANSLATE_NOOP("FullscreenUI", "Depth Buffer"); +TRANSLATE_NOOP("FullscreenUI", "Desktop Mode"); TRANSLATE_NOOP("FullscreenUI", "Details"); TRANSLATE_NOOP("FullscreenUI", "Details unavailable for game not scanned in game list."); TRANSLATE_NOOP("FullscreenUI", "Determines how large the on-screen messages and monitor are."); @@ -6707,12 +7142,14 @@ TRANSLATE_NOOP("FullscreenUI", "Enables the replacement of background textures i TRANSLATE_NOOP("FullscreenUI", "Encore Mode"); TRANSLATE_NOOP("FullscreenUI", "Enhancements"); TRANSLATE_NOOP("FullscreenUI", "Ensures every frame generated is displayed for optimal pacing. Disable if you are having speed or sound issues."); +TRANSLATE_NOOP("FullscreenUI", "Enter Value"); TRANSLATE_NOOP("FullscreenUI", "Enter the name of the input profile you wish to create."); TRANSLATE_NOOP("FullscreenUI", "Execution Mode"); TRANSLATE_NOOP("FullscreenUI", "Exit"); TRANSLATE_NOOP("FullscreenUI", "Exit And Save State"); +TRANSLATE_NOOP("FullscreenUI", "Exit DuckStation"); TRANSLATE_NOOP("FullscreenUI", "Exit Without Saving"); -TRANSLATE_NOOP("FullscreenUI", "Exits the program."); +TRANSLATE_NOOP("FullscreenUI", "Exits Big Picture mode, returning to the desktop interface."); TRANSLATE_NOOP("FullscreenUI", "Failed to copy text to clipboard."); TRANSLATE_NOOP("FullscreenUI", "Failed to delete save state."); TRANSLATE_NOOP("FullscreenUI", "Failed to delete {}."); @@ -6779,7 +7216,9 @@ TRANSLATE_NOOP("FullscreenUI", "Internal Resolution"); TRANSLATE_NOOP("FullscreenUI", "Issue Tracker"); TRANSLATE_NOOP("FullscreenUI", "Last Played"); TRANSLATE_NOOP("FullscreenUI", "Last Played: %s"); +TRANSLATE_NOOP("FullscreenUI", "Launch Options"); TRANSLATE_NOOP("FullscreenUI", "Launch a game by selecting a file/disc image."); +TRANSLATE_NOOP("FullscreenUI", "Launch a game from a file, disc, or starts the console without any disc inserted."); TRANSLATE_NOOP("FullscreenUI", "Launch a game from images scanned from your game directories."); TRANSLATE_NOOP("FullscreenUI", "Leaderboard Notifications"); TRANSLATE_NOOP("FullscreenUI", "Leaderboards"); @@ -6788,10 +7227,10 @@ TRANSLATE_NOOP("FullscreenUI", "Limits how many frames are displayed to the scre TRANSLATE_NOOP("FullscreenUI", "Line Detection"); TRANSLATE_NOOP("FullscreenUI", "List Settings"); TRANSLATE_NOOP("FullscreenUI", "Load Devices From Save States"); +TRANSLATE_NOOP("FullscreenUI", "Load Global State"); TRANSLATE_NOOP("FullscreenUI", "Load Profile"); TRANSLATE_NOOP("FullscreenUI", "Load Resume State"); TRANSLATE_NOOP("FullscreenUI", "Load State"); -TRANSLATE_NOOP("FullscreenUI", "Loads a global save state."); TRANSLATE_NOOP("FullscreenUI", "Loads all replacement texture to RAM, reducing stuttering at runtime."); TRANSLATE_NOOP("FullscreenUI", "Loads the game image into RAM. Useful for network paths that may become unreliable during gameplay."); TRANSLATE_NOOP("FullscreenUI", "Log Level"); @@ -6826,6 +7265,7 @@ TRANSLATE_NOOP("FullscreenUI", "Multitap"); TRANSLATE_NOOP("FullscreenUI", "Multitap Mode"); TRANSLATE_NOOP("FullscreenUI", "Mute All Sound"); TRANSLATE_NOOP("FullscreenUI", "Mute CD Audio"); +TRANSLATE_NOOP("FullscreenUI", "Navigate"); TRANSLATE_NOOP("FullscreenUI", "No Binding"); TRANSLATE_NOOP("FullscreenUI", "No Game Selected"); TRANSLATE_NOOP("FullscreenUI", "No cheats found for {}."); @@ -6908,8 +7348,11 @@ TRANSLATE_NOOP("FullscreenUI", "Resets all configuration to defaults (including TRANSLATE_NOOP("FullscreenUI", "Resets memory card directory to default (user directory)."); TRANSLATE_NOOP("FullscreenUI", "Resolution change will be applied after restarting."); TRANSLATE_NOOP("FullscreenUI", "Restores the state of the system prior to the last state loaded."); -TRANSLATE_NOOP("FullscreenUI", "Resume"); TRANSLATE_NOOP("FullscreenUI", "Resume Game"); +TRANSLATE_NOOP("FullscreenUI", "Resume Last Session"); +TRANSLATE_NOOP("FullscreenUI", "Return To Game"); +TRANSLATE_NOOP("FullscreenUI", "Return to desktop mode, or exit the application."); +TRANSLATE_NOOP("FullscreenUI", "Return to the previous menu."); TRANSLATE_NOOP("FullscreenUI", "Reverses the game list sort order from the default (usually ascending to descending)."); TRANSLATE_NOOP("FullscreenUI", "Rewind Save Frequency"); TRANSLATE_NOOP("FullscreenUI", "Rewind Save Slots"); @@ -6939,9 +7382,13 @@ TRANSLATE_NOOP("FullscreenUI", "Screenshot Quality"); TRANSLATE_NOOP("FullscreenUI", "Screenshot Size"); TRANSLATE_NOOP("FullscreenUI", "Search Directories"); TRANSLATE_NOOP("FullscreenUI", "Seek Speedup"); +TRANSLATE_NOOP("FullscreenUI", "Select"); TRANSLATE_NOOP("FullscreenUI", "Select Device"); +TRANSLATE_NOOP("FullscreenUI", "Select Disc Drive"); TRANSLATE_NOOP("FullscreenUI", "Select Disc Image"); +TRANSLATE_NOOP("FullscreenUI", "Select Game"); TRANSLATE_NOOP("FullscreenUI", "Select Macro {} Binds"); +TRANSLATE_NOOP("FullscreenUI", "Select State"); TRANSLATE_NOOP("FullscreenUI", "Selects the GPU to use for rendering."); TRANSLATE_NOOP("FullscreenUI", "Selects the percentage of the normal clock speed the emulated hardware will run at."); TRANSLATE_NOOP("FullscreenUI", "Selects the quality at which screenshots will be compressed."); @@ -7003,10 +7450,12 @@ TRANSLATE_NOOP("FullscreenUI", "Speeds up CD-ROM reads by the specified factor. TRANSLATE_NOOP("FullscreenUI", "Speeds up CD-ROM seeks by the specified factor. May improve loading speeds in some games, and break others."); TRANSLATE_NOOP("FullscreenUI", "Stage {}: {}"); TRANSLATE_NOOP("FullscreenUI", "Start BIOS"); +TRANSLATE_NOOP("FullscreenUI", "Start Disc"); TRANSLATE_NOOP("FullscreenUI", "Start File"); TRANSLATE_NOOP("FullscreenUI", "Start Fullscreen"); +TRANSLATE_NOOP("FullscreenUI", "Start Game"); +TRANSLATE_NOOP("FullscreenUI", "Start a game from a disc in your PC's DVD drive."); TRANSLATE_NOOP("FullscreenUI", "Start the console without any disc inserted."); -TRANSLATE_NOOP("FullscreenUI", "Starts the console from where it was before it was last closed."); TRANSLATE_NOOP("FullscreenUI", "Stores the current settings to an input profile."); TRANSLATE_NOOP("FullscreenUI", "Stretch Display Vertically"); TRANSLATE_NOOP("FullscreenUI", "Stretch Mode"); @@ -7035,6 +7484,7 @@ TRANSLATE_NOOP("FullscreenUI", "Timing out in {:.0f} seconds..."); TRANSLATE_NOOP("FullscreenUI", "Title"); TRANSLATE_NOOP("FullscreenUI", "Toggle Analog"); TRANSLATE_NOOP("FullscreenUI", "Toggle Fast Forward"); +TRANSLATE_NOOP("FullscreenUI", "Toggle Fullscreen"); TRANSLATE_NOOP("FullscreenUI", "Toggle every %d frames"); TRANSLATE_NOOP("FullscreenUI", "True Color Debanding"); TRANSLATE_NOOP("FullscreenUI", "True Color Rendering"); @@ -7078,6 +7528,8 @@ TRANSLATE_NOOP("FullscreenUI", "Wireframe Rendering"); TRANSLATE_NOOP("FullscreenUI", "Writes textures which can be replaced to the dump directory."); TRANSLATE_NOOP("FullscreenUI", "\"Challenge\" mode for achievements, including leaderboard tracking. Disables save state, cheats, and slowdown functions."); TRANSLATE_NOOP("FullscreenUI", "\"PlayStation\" and \"PSX\" are registered trademarks of Sony Interactive Entertainment Europe Limited. This software is not affiliated in any way with Sony Interactive Entertainment."); +TRANSLATE_NOOP("FullscreenUI", "{:%H:%M}"); +TRANSLATE_NOOP("FullscreenUI", "{:%Y-%m-%d %H:%M:%S}"); TRANSLATE_NOOP("FullscreenUI", "{} Frames"); TRANSLATE_NOOP("FullscreenUI", "{} deleted."); TRANSLATE_NOOP("FullscreenUI", "{} does not exist."); diff --git a/src/core/fullscreen_ui.h b/src/core/fullscreen_ui.h index 99e54894c..5942239f3 100644 --- a/src/core/fullscreen_ui.h +++ b/src/core/fullscreen_ui.h @@ -34,6 +34,7 @@ void OpenLeaderboardsWindow(); bool IsLeaderboardsWindowOpen(); void ReturnToMainWindow(); void ReturnToPreviousWindow(); +void SetStandardSelectionFooterText(bool back_instead_of_cancel); #endif void Shutdown(); @@ -45,5 +46,13 @@ void TimeToPrintableString(SmallStringBase* str, time_t t); // Host UI triggers from Big Picture mode. namespace Host { +/// Requests shut down and exit of the hosting application. This may not actually exit, +/// if the user cancels the shutdown confirmation. +void RequestExitApplication(bool allow_confirm); + +/// Requests Big Picture mode to be shut down, returning to the desktop interface. +void RequestExitBigPicture(); + +/// Requests the cover downloader be opened. void OnCoverDownloaderOpenRequested(); } // namespace Host diff --git a/src/core/host.h b/src/core/host.h index 9cfe47e4c..7b9917112 100644 --- a/src/core/host.h +++ b/src/core/host.h @@ -94,10 +94,6 @@ void DisplayLoadingScreen(const char* message, int progress_min = -1, int progre /// Safely executes a function on the VM thread. void RunOnCPUThread(std::function function, bool block = false); -/// Requests shut down and exit of the hosting application. This may not actually exit, -/// if the user cancels the shutdown confirmation. -void RequestExit(bool allow_confirm); - /// Attempts to create the rendering device backend. bool CreateGPUDevice(RenderAPI api); diff --git a/src/duckstation-qt/qthost.cpp b/src/duckstation-qt/qthost.cpp index c5c9829bc..ef2e346a6 100644 --- a/src/duckstation-qt/qthost.cpp +++ b/src/duckstation-qt/qthost.cpp @@ -696,8 +696,7 @@ void EmuThread::stopFullscreenUI() return; } - if (System::IsValid()) - shutdownSystem(); + setFullscreen(false, true); if (m_run_fullscreen_ui) { @@ -1864,11 +1863,16 @@ void Host::RequestSystemShutdown(bool allow_confirm, bool save_state) Q_ARG(bool, true), Q_ARG(bool, save_state)); } -void Host::RequestExit(bool allow_confirm) +void Host::RequestExitApplication(bool allow_confirm) { QMetaObject::invokeMethod(g_main_window, "requestExit", Qt::QueuedConnection, Q_ARG(bool, allow_confirm)); } +void Host::RequestExitBigPicture() +{ + g_emu_thread->stopFullscreenUI(); +} + std::optional Host::GetTopLevelWindowInfo() { std::optional ret; diff --git a/src/duckstation-regtest/regtest_host.cpp b/src/duckstation-regtest/regtest_host.cpp index 2406a977d..daa290bef 100644 --- a/src/duckstation-regtest/regtest_host.cpp +++ b/src/duckstation-regtest/regtest_host.cpp @@ -299,7 +299,12 @@ void Host::RequestResizeHostDisplay(s32 width, s32 height) // } -void Host::RequestExit(bool save_state_if_running) +void Host::RequestExitApplication(bool save_state_if_running) +{ + // +} + +void Host::RequestExitBigPicture() { // } diff --git a/src/util/image.cpp b/src/util/image.cpp index 8cc05cf1c..dc5b884f1 100644 --- a/src/util/image.cpp +++ b/src/util/image.cpp @@ -292,7 +292,12 @@ bool PNGFileLoader(RGBA8Image* image, const char* filename, std::FILE* fp) if (setjmp(png_jmpbuf(png_ptr))) return false; - png_init_io(png_ptr, fp); + png_set_read_fn(png_ptr, fp, [](png_structp png_ptr, png_bytep data_ptr, png_size_t size) { + std::FILE* fp = static_cast(png_get_io_ptr(png_ptr)); + if (std::fread(data_ptr, size, 1, fp) != 1) + png_error(png_ptr, "Read error"); + }); + return PNGCommonLoader(image, png_ptr, info_ptr, new_data, row_pointers); } diff --git a/src/util/imgui_fullscreen.cpp b/src/util/imgui_fullscreen.cpp index ebe9a4fbb..da12dce3d 100644 --- a/src/util/imgui_fullscreen.cpp +++ b/src/util/imgui_fullscreen.cpp @@ -100,6 +100,10 @@ static std::deque s_texture_load_queue; static std::deque> s_texture_upload_queue; static std::thread s_texture_load_thread; +static SmallString s_fullscreen_footer_text; +static SmallString s_last_fullscreen_footer_text; +static float s_fullscreen_text_change_time; + static bool s_choice_dialog_open = false; static bool s_choice_dialog_checkable = false; static std::string s_choice_dialog_title; @@ -245,6 +249,9 @@ void ImGuiFullscreen::Shutdown() s_notifications.clear(); s_background_progress_dialogs.clear(); + s_fullscreen_footer_text.clear(); + s_last_fullscreen_footer_text.clear(); + s_fullscreen_text_change_time = 0.0f; CloseInputDialog(); CloseMessageDialog(); s_choice_dialog_open = false; @@ -262,6 +269,11 @@ void ImGuiFullscreen::Shutdown() s_file_selector_current_directory = {}; s_file_selector_filters.clear(); s_file_selector_items.clear(); + s_message_dialog_open = false; + s_message_dialog_title = {}; + s_message_dialog_message = {}; + s_message_dialog_buttons = {}; + s_message_dialog_callback = {}; } const std::shared_ptr& ImGuiFullscreen::GetPlaceholderTexture() @@ -499,6 +511,8 @@ void ImGuiFullscreen::EndLayout() DrawInputDialog(); DrawMessageDialog(); + DrawFullscreenFooter(); + const float notification_margin = LayoutScale(10.0f); const float spacing = LayoutScale(10.0f); const float notification_vertical_pos = GetNotificationVerticalPosition(); @@ -511,6 +525,8 @@ void ImGuiFullscreen::EndLayout() PopResetLayout(); + s_fullscreen_footer_text.clear(); + s_rendered_menu_item_border = false; s_had_hovered_menu_item = std::exchange(s_has_hovered_menu_item, false); } @@ -558,6 +574,10 @@ bool ImGuiFullscreen::ResetFocusHere() if (!s_focus_reset_queued) return false; + // don't take focus from dialogs + if (ImGui::FindBlockingModal(ImGui::GetCurrentWindow())) + return false; + s_focus_reset_queued = false; ImGui::SetWindowFocus(); @@ -565,6 +585,20 @@ bool ImGuiFullscreen::ResetFocusHere() return (GImGui->NavInputSource == ImGuiInputSource_Keyboard || GImGui->NavInputSource == ImGuiInputSource_Gamepad); } +bool ImGuiFullscreen::IsFocusResetQueued() +{ + return s_focus_reset_queued; +} + +void ImGuiFullscreen::ForceKeyNavEnabled() +{ + ImGuiContext& g = *ImGui::GetCurrentContext(); + g.ActiveIdSource = (g.ActiveIdSource == ImGuiInputSource_Mouse) ? ImGuiInputSource_Keyboard : g.ActiveIdSource; + g.NavInputSource = (g.NavInputSource == ImGuiInputSource_Mouse) ? ImGuiInputSource_Keyboard : g.ActiveIdSource; + g.NavDisableHighlight = false; + g.NavDisableMouseHover = true; +} + bool ImGuiFullscreen::WantsToCloseMenu() { // Wait for the Close button to be released, THEN pressed @@ -606,12 +640,12 @@ void ImGuiFullscreen::PopPrimaryColor() ImGui::PopStyleColor(5); } -bool ImGuiFullscreen::BeginFullscreenColumns(const char* title, float pos_y, bool expand_to_screen_width) +bool ImGuiFullscreen::BeginFullscreenColumns(const char* title, float pos_y, bool expand_to_screen_width, bool footer) { ImGui::SetNextWindowPos(ImVec2(expand_to_screen_width ? 0.0f : g_layout_padding_left, pos_y)); ImGui::SetNextWindowSize( ImVec2(expand_to_screen_width ? ImGui::GetIO().DisplaySize.x : LayoutScale(LAYOUT_SCREEN_WIDTH), - ImGui::GetIO().DisplaySize.y - pos_y)); + ImGui::GetIO().DisplaySize.y - pos_y - (footer ? LayoutScale(LAYOUT_FOOTER_HEIGHT) : 0.0f))); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); @@ -705,14 +739,103 @@ void ImGuiFullscreen::EndFullscreenWindow() ImGui::PopStyleColor(); } +bool ImGuiFullscreen::IsGamepadInputSource() +{ + return (ImGui::GetCurrentContext()->NavInputSource == ImGuiInputSource_Gamepad); +} + +void ImGuiFullscreen::CreateFooterTextString(SmallStringBase& dest, + std::span> items) +{ + dest.clear(); + for (const auto& [icon, text] : items) + { + if (!dest.empty()) + dest.append(" "); + + dest.append(icon); + dest.append(' '); + dest.append(text); + } +} + +void ImGuiFullscreen::SetFullscreenFooterText(std::string_view text) +{ + s_fullscreen_footer_text.assign(text); +} + +void ImGuiFullscreen::SetFullscreenFooterText(std::span> items) +{ + CreateFooterTextString(s_fullscreen_footer_text, items); +} + +void ImGuiFullscreen::DrawFullscreenFooter() +{ + const ImGuiIO& io = ImGui::GetIO(); + if (s_fullscreen_footer_text.empty()) + { + s_last_fullscreen_footer_text.clear(); + return; + } + + const float padding = LayoutScale(LAYOUT_FOOTER_PADDING); + const float height = LayoutScale(LAYOUT_FOOTER_HEIGHT); + + ImDrawList* dl = ImGui::GetForegroundDrawList(); + dl->AddRectFilled(ImVec2(0.0f, io.DisplaySize.y - height), io.DisplaySize, ImGui::GetColorU32(UIPrimaryColor), 0.0f); + + ImFont* const font = g_medium_font; + const float max_width = io.DisplaySize.x - padding * 2.0f; + + float prev_opacity = 0.0f; + if (!s_last_fullscreen_footer_text.empty() && s_fullscreen_footer_text != s_last_fullscreen_footer_text) + { + if (s_fullscreen_text_change_time == 0.0f) + s_fullscreen_text_change_time = 0.15f; + else + s_fullscreen_text_change_time = std::max(s_fullscreen_text_change_time - io.DeltaTime, 0.0f); + + if (s_fullscreen_text_change_time == 0.0f) + s_last_fullscreen_footer_text = s_fullscreen_footer_text; + + prev_opacity = s_fullscreen_text_change_time * (1.0f / 0.15f); + if (prev_opacity > 0.0f) + { + const ImVec2 text_size = + font->CalcTextSizeA(font->FontSize, max_width, 0.0f, s_last_fullscreen_footer_text.c_str(), + s_last_fullscreen_footer_text.end_ptr()); + dl->AddText( + font, font->FontSize, + ImVec2(io.DisplaySize.x - padding * 2.0f - text_size.x, io.DisplaySize.y - font->FontSize - padding), + ImGui::GetColorU32(ImVec4(UIPrimaryTextColor.x, UIPrimaryTextColor.y, UIPrimaryTextColor.z, prev_opacity)), + s_last_fullscreen_footer_text.c_str(), s_last_fullscreen_footer_text.end_ptr()); + } + } + else if (s_last_fullscreen_footer_text.empty()) + { + s_last_fullscreen_footer_text = s_fullscreen_footer_text; + } + + if (prev_opacity < 1.0f) + { + const ImVec2 text_size = font->CalcTextSizeA(font->FontSize, max_width, 0.0f, s_fullscreen_footer_text.c_str(), + s_fullscreen_footer_text.end_ptr()); + dl->AddText( + font, font->FontSize, + ImVec2(io.DisplaySize.x - padding * 2.0f - text_size.x, io.DisplaySize.y - font->FontSize - padding), + ImGui::GetColorU32(ImVec4(UIPrimaryTextColor.x, UIPrimaryTextColor.y, UIPrimaryTextColor.z, 1.0f - prev_opacity)), + s_fullscreen_footer_text.c_str(), s_fullscreen_footer_text.end_ptr()); + } +} + void ImGuiFullscreen::PrerenderMenuButtonBorder() { if (!s_had_hovered_menu_item) return; // updating might finish the animation - const ImVec2 min = s_menu_button_frame_min_animated.UpdateAndGetValue(); - const ImVec2 max = s_menu_button_frame_max_animated.UpdateAndGetValue(); + const ImVec2& min = s_menu_button_frame_min_animated.UpdateAndGetValue(); + const ImVec2& max = s_menu_button_frame_max_animated.UpdateAndGetValue(); const ImU32 col = ImGui::GetColorU32(ImGuiCol_ButtonHovered); const float t = static_cast(std::min(std::abs(std::sin(ImGui::GetTime() * 0.75) * 1.1), 1.0)); @@ -874,7 +997,7 @@ void ImGuiFullscreen::DrawMenuButtonFrame(const ImVec2& p_min, const ImVec2& p_m MENU_BACKGROUND_ANIMATION_TIME); } if (frame_max.x != s_menu_button_frame_max_animated.GetEndValue().x || - frame_max.y != s_menu_button_frame_max_animated.GetEndValue().x) + frame_max.y != s_menu_button_frame_max_animated.GetEndValue().y) { s_menu_button_frame_max_animated.Start(s_menu_button_frame_max_animated.GetCurrentValue(), frame_max, MENU_BACKGROUND_ANIMATION_TIME); @@ -1794,6 +1917,100 @@ bool ImGuiFullscreen::NavTab(const char* title, bool is_active, bool enabled /* return pressed; } +bool ImGuiFullscreen::BeginHorizontalMenu(const char* name, const ImVec2& position, const ImVec2& size, u32 num_items) +{ + s_menu_button_index = 0; + + const float item_padding = LayoutScale(LAYOUT_HORIZONTAL_MENU_PADDING); + const float item_width = LayoutScale(LAYOUT_HORIZONTAL_MENU_ITEM_WIDTH); + const float item_spacing = LayoutScale(30.0f); + const float menu_width = static_cast(num_items) * (item_width + item_spacing) - item_spacing; + const float menu_height = LayoutScale(LAYOUT_HORIZONTAL_MENU_HEIGHT); + + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(item_padding, item_padding)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, LayoutScale(1.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(item_spacing, 0.0f)); + + if (!BeginFullscreenWindow(position, size, name, UIBackgroundColor, 0.0f, 0.0f)) + return false; + + ImGui::SetCursorPos(ImVec2((size.x - menu_width) * 0.5f, (size.y - menu_height) * 0.5f)); + + PrerenderMenuButtonBorder(); + return true; +} + +void ImGuiFullscreen::EndHorizontalMenu() +{ + ImGui::PopStyleVar(4); + EndFullscreenWindow(); +} + +bool ImGuiFullscreen::HorizontalMenuItem(GPUTexture* icon, const char* title, const char* description) +{ + ImGuiWindow* window = ImGui::GetCurrentWindow(); + if (window->SkipItems) + return false; + + const ImVec2 pos = window->DC.CursorPos; + const ImVec2 size = LayoutScale(LAYOUT_HORIZONTAL_MENU_ITEM_WIDTH, LAYOUT_HORIZONTAL_MENU_HEIGHT); + ImRect bb = ImRect(pos, pos + size); + + const ImGuiID id = window->GetID(title); + ImGui::ItemSize(size); + if (!ImGui::ItemAdd(bb, id)) + return false; + + bool held; + bool hovered; + const bool pressed = ImGui::ButtonBehavior(bb, id, &hovered, &held, 0); + if (hovered) + { + const ImU32 col = ImGui::GetColorU32(held ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered, 1.0f); + + const float t = static_cast(std::min(std::abs(std::sin(ImGui::GetTime() * 0.75) * 1.1), 1.0)); + ImGui::PushStyleColor(ImGuiCol_Border, ImGui::GetColorU32(ImGuiCol_Border, t)); + + DrawMenuButtonFrame(bb.Min, bb.Max, col, true, 0.0f); + + ImGui::PopStyleColor(); + } + + const ImGuiStyle& style = ImGui::GetStyle(); + bb.Min += style.FramePadding; + bb.Max -= style.FramePadding; + + const float avail_width = bb.Max.x - bb.Min.x; + const float icon_size = LayoutScale(150.0f); + const ImVec2 icon_pos = bb.Min + ImVec2((avail_width - icon_size) * 0.5f, 0.0f); + + ImDrawList* dl = ImGui::GetWindowDrawList(); + dl->AddImage(reinterpret_cast(icon), icon_pos, icon_pos + ImVec2(icon_size, icon_size)); + + ImFont* title_font = g_large_font; + const ImVec2 title_size = title_font->CalcTextSizeA(title_font->FontSize, avail_width, 0.0f, title); + const ImVec2 title_pos = + ImVec2(bb.Min.x + (avail_width - title_size.x) * 0.5f, icon_pos.y + icon_size + LayoutScale(10.0f)); + const ImVec4 title_bb = ImVec4(title_pos.x, title_pos.y, title_pos.x + title_size.x, title_pos.y + title_size.y); + + dl->AddText(title_font, title_font->FontSize, title_pos, ImGui::GetColorU32(ImGuiCol_Text), title, nullptr, 0.0f, + &title_bb); + + ImFont* desc_font = g_medium_font; + const ImVec2 desc_size = desc_font->CalcTextSizeA(desc_font->FontSize, avail_width, avail_width, description); + const ImVec2 desc_pos = ImVec2(bb.Min.x + (avail_width - desc_size.x) * 0.5f, title_bb.w + LayoutScale(10.0f)); + const ImVec4 desc_bb = ImVec4(desc_pos.x, desc_pos.y, desc_pos.x + desc_size.x, desc_pos.y + desc_size.y); + + dl->AddText(desc_font, desc_font->FontSize, desc_pos, ImGui::GetColorU32(ImGuiCol_Text), description, nullptr, + avail_width, &desc_bb); + + ImGui::SameLine(); + + s_menu_button_index++; + return pressed; +} + void ImGuiFullscreen::PopulateFileSelectorItems() { s_file_selector_items.clear(); @@ -1869,7 +2086,7 @@ bool ImGuiFullscreen::IsFileSelectorOpen() return s_file_selector_open; } -void ImGuiFullscreen::OpenFileSelector(const char* title, bool select_directory, FileSelectorCallback callback, +void ImGuiFullscreen::OpenFileSelector(std::string_view title, bool select_directory, FileSelectorCallback callback, FileSelectorFilters filters, std::string initial_directory) { if (s_file_selector_open) @@ -1884,6 +2101,7 @@ void ImGuiFullscreen::OpenFileSelector(const char* title, bool select_directory, if (initial_directory.empty() || !FileSystem::DirectoryExists(initial_directory.c_str())) initial_directory = FileSystem::GetWorkingDirectory(); SetFileSelectorDirectory(std::move(initial_directory)); + QueueResetFocus(); } void ImGuiFullscreen::CloseFileSelector() @@ -1931,6 +2149,7 @@ void ImGuiFullscreen::DrawFileSelector() ImGui::PushStyleColor(ImGuiCol_Text, UIBackgroundTextColor); BeginMenuButtons(); + ResetFocusHere(); if (!s_file_selector_current_directory.empty()) { @@ -1965,6 +2184,9 @@ void ImGuiFullscreen::DrawFileSelector() ImGui::PopStyleVar(3); ImGui::PopFont(); + if (is_open) + GetFileSelectorHelpText(s_fullscreen_footer_text); + if (selected) { if (selected->is_file) @@ -1974,6 +2196,7 @@ void ImGuiFullscreen::DrawFileSelector() else { SetFileSelectorDirectory(std::move(selected->full_path)); + QueueResetFocus(); } } else if (directory_selected) @@ -1986,6 +2209,18 @@ void ImGuiFullscreen::DrawFileSelector() s_file_selector_callback(no_path); CloseFileSelector(); } + else + { + if (ImGui::IsKeyPressed(ImGuiKey_Backspace, false) || ImGui::IsKeyPressed(ImGuiKey_NavGamepadMenu, false)) + { + if (!s_file_selector_items.empty() && s_file_selector_items.front().display_name == ICON_FA_FOLDER_OPEN + " ") + { + SetFileSelectorDirectory(std::move(s_file_selector_items.front().full_path)); + QueueResetFocus(); + } + } + } } bool ImGuiFullscreen::IsChoiceDialogOpen() @@ -1993,7 +2228,7 @@ bool ImGuiFullscreen::IsChoiceDialogOpen() return s_choice_dialog_open; } -void ImGuiFullscreen::OpenChoiceDialog(const char* title, bool checkable, ChoiceDialogOptions options, +void ImGuiFullscreen::OpenChoiceDialog(std::string_view title, bool checkable, ChoiceDialogOptions options, ChoiceDialogCallback callback) { if (s_choice_dialog_open) @@ -2004,6 +2239,7 @@ void ImGuiFullscreen::OpenChoiceDialog(const char* title, bool checkable, Choice s_choice_dialog_title = fmt::format("{}##choice_dialog", title); s_choice_dialog_options = std::move(options); s_choice_dialog_callback = std::move(callback); + QueueResetFocus(); } void ImGuiFullscreen::CloseChoiceDialog() @@ -2054,6 +2290,7 @@ void ImGuiFullscreen::DrawChoiceDialog() ImGui::PushStyleColor(ImGuiCol_Text, UIBackgroundTextColor); BeginMenuButtons(); + ResetFocusHere(); if (s_choice_dialog_checkable) { @@ -2115,6 +2352,10 @@ void ImGuiFullscreen::DrawChoiceDialog() s_choice_dialog_callback(-1, no_string, false); CloseChoiceDialog(); } + else + { + GetChoiceDialogHelpText(s_fullscreen_footer_text); + } } bool ImGuiFullscreen::IsInputDialogOpen() @@ -2131,6 +2372,7 @@ void ImGuiFullscreen::OpenInputStringDialog(std::string title, std::string messa s_input_dialog_caption = std::move(caption); s_input_dialog_ok_text = std::move(ok_button_text); s_input_dialog_callback = std::move(callback); + QueueResetFocus(); } void ImGuiFullscreen::DrawInputDialog() @@ -2157,6 +2399,7 @@ void ImGuiFullscreen::DrawInputDialog() ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove)) { + ResetFocusHere(); ImGui::TextWrapped("%s", s_input_dialog_message.c_str()); BeginMenuButtons(); @@ -2202,6 +2445,8 @@ void ImGuiFullscreen::DrawInputDialog() } if (!is_open) CloseInputDialog(); + else + GetInputDialogHelpText(s_fullscreen_footer_text); ImGui::PopStyleColor(4); ImGui::PopStyleVar(3); @@ -2239,6 +2484,7 @@ void ImGuiFullscreen::OpenConfirmMessageDialog(std::string title, std::string me s_message_dialog_callback = std::move(callback); s_message_dialog_buttons[0] = std::move(yes_button_text); s_message_dialog_buttons[1] = std::move(no_button_text); + QueueResetFocus(); } void ImGuiFullscreen::OpenInfoMessageDialog(std::string title, std::string message, InfoMessageDialogCallback callback, @@ -2251,6 +2497,7 @@ void ImGuiFullscreen::OpenInfoMessageDialog(std::string title, std::string messa s_message_dialog_message = std::move(message); s_message_dialog_callback = std::move(callback); s_message_dialog_buttons[0] = std::move(button_text); + QueueResetFocus(); } void ImGuiFullscreen::OpenMessageDialog(std::string title, std::string message, MessageDialogCallback callback, @@ -2266,6 +2513,7 @@ void ImGuiFullscreen::OpenMessageDialog(std::string title, std::string message, s_message_dialog_buttons[0] = std::move(first_button_text); s_message_dialog_buttons[1] = std::move(second_button_text); s_message_dialog_buttons[2] = std::move(third_button_text); + QueueResetFocus(); } void ImGuiFullscreen::CloseMessageDialog() @@ -2278,6 +2526,7 @@ void ImGuiFullscreen::CloseMessageDialog() s_message_dialog_message = {}; s_message_dialog_buttons = {}; s_message_dialog_callback = {}; + QueueResetFocus(); } void ImGuiFullscreen::DrawMessageDialog() @@ -2310,6 +2559,7 @@ void ImGuiFullscreen::DrawMessageDialog() if (ImGui::BeginPopupModal(win_id, &is_open, flags)) { BeginMenuButtons(); + QueueResetFocus(); ImGui::TextWrapped("%s", s_message_dialog_message.c_str()); ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(20.0f)); @@ -2352,6 +2602,10 @@ void ImGuiFullscreen::DrawMessageDialog() func(result.value_or(1) == 0); } } + else + { + GetChoiceDialogHelpText(s_fullscreen_footer_text); + } } static float s_notification_vertical_position = 0.15f; @@ -2719,7 +2973,7 @@ void ImGuiFullscreen::DrawToast() ImFont* message_font = g_medium_font; const float padding = LayoutScale(20.0f); const float total_padding = padding * 2.0f; - const float margin = LayoutScale(20.0f); + const float margin = LayoutScale(20.0f + (s_fullscreen_footer_text.empty() ? 0.0f : LAYOUT_FOOTER_HEIGHT)); const float spacing = s_toast_title.empty() ? 0.0f : LayoutScale(10.0f); const ImVec2 display_size(ImGui::GetIO().DisplaySize); const ImVec2 title_size(s_toast_title.empty() ? @@ -2743,7 +2997,7 @@ void ImGuiFullscreen::DrawToast() const float offset = (comb_size.x - title_size.x) * 0.5f; dl->AddText(title_font, title_font->FontSize, box_pos + ImVec2(offset + padding, padding), ImGui::GetColorU32(ModAlpha(UIPrimaryTextColor, alpha)), s_toast_title.c_str(), - s_toast_title.c_str() + s_toast_title.length()); + s_toast_title.c_str() + s_toast_title.length(), max_width); } if (!s_toast_message.empty()) { @@ -2751,7 +3005,7 @@ void ImGuiFullscreen::DrawToast() dl->AddText(message_font, message_font->FontSize, box_pos + ImVec2(offset + padding, padding + spacing + title_size.y), ImGui::GetColorU32(ModAlpha(UIPrimaryTextColor, alpha)), s_toast_message.c_str(), - s_toast_message.c_str() + s_toast_message.length()); + s_toast_message.c_str() + s_toast_message.length(), max_width); } } diff --git a/src/util/imgui_fullscreen.h b/src/util/imgui_fullscreen.h index 490ba0828..b5ee5c3d2 100644 --- a/src/util/imgui_fullscreen.h +++ b/src/util/imgui_fullscreen.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin +// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) #pragma once @@ -12,10 +12,14 @@ #include #include #include +#include #include +#include +#include #include class GPUTexture; +class SmallStringBase; namespace ImGuiFullscreen { #define HEX_TO_IMVEC4(hex, alpha) \ @@ -31,6 +35,11 @@ static constexpr float LAYOUT_MENU_BUTTON_HEIGHT = 50.0f; static constexpr float LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY = 26.0f; static constexpr float LAYOUT_MENU_BUTTON_X_PADDING = 15.0f; static constexpr float LAYOUT_MENU_BUTTON_Y_PADDING = 10.0f; +static constexpr float LAYOUT_FOOTER_PADDING = 10.0f; +static constexpr float LAYOUT_FOOTER_HEIGHT = LAYOUT_MEDIUM_FONT_SIZE + LAYOUT_FOOTER_PADDING * 2.0f; +static constexpr float LAYOUT_HORIZONTAL_MENU_HEIGHT = 320.0f; +static constexpr float LAYOUT_HORIZONTAL_MENU_PADDING = 30.0f; +static constexpr float LAYOUT_HORIZONTAL_MENU_ITEM_WIDTH = 250.0f; extern ImFont* g_standard_font; extern ImFont* g_medium_font; @@ -162,6 +171,9 @@ void PopResetLayout(); void QueueResetFocus(); bool ResetFocusHere(); +bool IsFocusResetQueued(); +void ForceKeyNavEnabled(); + bool WantsToCloseMenu(); void ResetCloseMenuIfNeeded(); @@ -170,7 +182,8 @@ void PopPrimaryColor(); void DrawWindowTitle(const char* title); -bool BeginFullscreenColumns(const char* title = nullptr, float pos_y = 0.0f, bool expand_to_screen_width = false); +bool BeginFullscreenColumns(const char* title = nullptr, float pos_y = 0.0f, bool expand_to_screen_width = false, + bool footer = false); void EndFullscreenColumns(); bool BeginFullscreenColumnWindow(float start, float end, const char* name, @@ -185,6 +198,12 @@ bool BeginFullscreenWindow(const ImVec2& position, const ImVec2& size, const cha float padding = 0.0f, ImGuiWindowFlags flags = 0); void EndFullscreenWindow(); +bool IsGamepadInputSource(); +void CreateFooterTextString(SmallStringBase& dest, std::span> items); +void SetFullscreenFooterText(std::string_view text); +void SetFullscreenFooterText(std::span> items); +void DrawFullscreenFooter(); + void PrerenderMenuButtonBorder(); void BeginMenuButtons(u32 num_items = 0, float y_align = 0.0f, float x_padding = LAYOUT_MENU_BUTTON_X_PADDING, float y_padding = LAYOUT_MENU_BUTTON_Y_PADDING, float item_height = LAYOUT_MENU_BUTTON_HEIGHT); @@ -267,10 +286,14 @@ bool NavButton(const char* title, bool is_active, bool enabled = true, float wid bool NavTab(const char* title, bool is_active, bool enabled, float width, float height, const ImVec4& background, ImFont* font = g_large_font); +bool BeginHorizontalMenu(const char* name, const ImVec2& position, const ImVec2& size, u32 num_items); +void EndHorizontalMenu(); +bool HorizontalMenuItem(GPUTexture* icon, const char* title, const char* description); + using FileSelectorCallback = std::function; using FileSelectorFilters = std::vector; bool IsFileSelectorOpen(); -void OpenFileSelector(const char* title, bool select_directory, FileSelectorCallback callback, +void OpenFileSelector(std::string_view title, bool select_directory, FileSelectorCallback callback, FileSelectorFilters filters = FileSelectorFilters(), std::string initial_directory = std::string()); void CloseFileSelector(); @@ -278,7 +301,8 @@ void CloseFileSelector(); using ChoiceDialogCallback = std::function; using ChoiceDialogOptions = std::vector>; bool IsChoiceDialogOpen(); -void OpenChoiceDialog(const char* title, bool checkable, ChoiceDialogOptions options, ChoiceDialogCallback callback); +void OpenChoiceDialog(std::string_view title, bool checkable, ChoiceDialogOptions options, + ChoiceDialogCallback callback); void CloseChoiceDialog(); using InputStringDialogCallback = std::function; @@ -313,4 +337,9 @@ void ClearNotifications(); void ShowToast(std::string title, std::string message, float duration = 10.0f); void ClearToast(); + +// Message callbacks. +void GetChoiceDialogHelpText(SmallStringBase& dest); +void GetFileSelectorHelpText(SmallStringBase& dest); +void GetInputDialogHelpText(SmallStringBase& dest); } // namespace ImGuiFullscreen diff --git a/src/util/imgui_manager.cpp b/src/util/imgui_manager.cpp index c9006b796..e5c756375 100644 --- a/src/util/imgui_manager.cpp +++ b/src/util/imgui_manager.cpp @@ -176,6 +176,7 @@ bool ImGuiManager::Initialize(float global_scale, bool show_osd_messages, Error* io.BackendFlags |= ImGuiBackendFlags_HasGamepad | ImGuiBackendFlags_RendererHasVtxOffset; io.BackendUsingLegacyKeyArrays = 0; io.BackendUsingLegacyNavInputArray = 0; + io.KeyRepeatDelay = 0.5f; #ifndef __ANDROID__ // Android has no keyboard, nor are we using ImGui for any actual user-interactable windows. io.ConfigFlags |= @@ -562,22 +563,21 @@ bool ImGuiManager::AddIconFonts(float size) static constexpr ImWchar range_fa[] = { 0xe086, 0xe086, 0xf002, 0xf002, 0xf005, 0xf005, 0xf007, 0xf007, 0xf00c, 0xf00e, 0xf011, 0xf011, 0xf013, 0xf013, 0xf017, 0xf017, 0xf019, 0xf019, 0xf01c, 0xf01c, 0xf021, 0xf021, 0xf023, 0xf023, 0xf025, 0xf025, 0xf027, 0xf028, - 0xf02e, 0xf02e, 0xf030, 0xf030, 0xf03a, 0xf03a, 0xf03d, 0xf03d, 0xf049, 0xf04c, 0xf050, 0xf050, 0xf059, 0xf059, - 0xf05e, 0xf05e, 0xf062, 0xf063, 0xf065, 0xf065, 0xf067, 0xf067, 0xf071, 0xf071, 0xf075, 0xf075, 0xf077, 0xf078, - 0xf07b, 0xf07c, 0xf084, 0xf085, 0xf091, 0xf091, 0xf0a0, 0xf0a0, 0xf0ac, 0xf0ad, 0xf0c5, 0xf0c5, 0xf0c7, 0xf0c9, - 0xf0cb, 0xf0cb, 0xf0d0, 0xf0d0, 0xf0dc, 0xf0dc, 0xf0e2, 0xf0e2, 0xf0e7, 0xf0e7, 0xf0eb, 0xf0eb, 0xf0f1, 0xf0f1, - 0xf0f3, 0xf0f3, 0xf0fe, 0xf0fe, 0xf110, 0xf110, 0xf119, 0xf119, 0xf11b, 0xf11c, 0xf140, 0xf140, 0xf144, 0xf144, - 0xf14a, 0xf14a, 0xf15b, 0xf15b, 0xf15d, 0xf15d, 0xf188, 0xf188, 0xf191, 0xf192, 0xf1ab, 0xf1ab, 0xf1dd, 0xf1de, - 0xf1e6, 0xf1e6, 0xf1eb, 0xf1eb, 0xf1f8, 0xf1f8, 0xf1fc, 0xf1fc, 0xf242, 0xf242, 0xf245, 0xf245, 0xf26c, 0xf26c, - 0xf279, 0xf279, 0xf2d0, 0xf2d0, 0xf2db, 0xf2db, 0xf2f2, 0xf2f2, 0xf2f5, 0xf2f5, 0xf3c1, 0xf3c1, 0xf3fd, 0xf3fd, - 0xf410, 0xf410, 0xf466, 0xf466, 0xf500, 0xf500, 0xf51f, 0xf51f, 0xf538, 0xf538, 0xf545, 0xf545, 0xf547, 0xf548, - 0xf552, 0xf552, 0xf57a, 0xf57a, 0xf5a2, 0xf5a2, 0xf5aa, 0xf5aa, 0xf5e7, 0xf5e7, 0xf65d, 0xf65e, 0xf6a9, 0xf6a9, - 0xf6cf, 0xf6cf, 0xf794, 0xf794, 0xf7c2, 0xf7c2, 0xf807, 0xf807, 0xf815, 0xf815, 0xf818, 0xf818, 0xf84c, 0xf84c, - 0xf8cc, 0xf8cc, 0x0, 0x0}; + 0xf02e, 0xf02e, 0xf030, 0xf030, 0xf03a, 0xf03a, 0xf03d, 0xf03d, 0xf049, 0xf04c, 0xf050, 0xf050, 0xf05e, 0xf05e, + 0xf062, 0xf063, 0xf067, 0xf067, 0xf071, 0xf071, 0xf075, 0xf075, 0xf077, 0xf078, 0xf07b, 0xf07c, 0xf084, 0xf085, + 0xf091, 0xf091, 0xf0a0, 0xf0a0, 0xf0ac, 0xf0ad, 0xf0c5, 0xf0c5, 0xf0c7, 0xf0c9, 0xf0cb, 0xf0cb, 0xf0d0, 0xf0d0, + 0xf0dc, 0xf0dc, 0xf0e2, 0xf0e2, 0xf0e7, 0xf0e7, 0xf0eb, 0xf0eb, 0xf0f1, 0xf0f1, 0xf0f3, 0xf0f3, 0xf0fe, 0xf0fe, + 0xf110, 0xf110, 0xf119, 0xf119, 0xf11b, 0xf11c, 0xf140, 0xf140, 0xf14a, 0xf14a, 0xf15b, 0xf15b, 0xf15d, 0xf15d, + 0xf191, 0xf192, 0xf1ab, 0xf1ab, 0xf1dd, 0xf1de, 0xf1e6, 0xf1e6, 0xf1eb, 0xf1eb, 0xf1f8, 0xf1f8, 0xf1fc, 0xf1fc, + 0xf242, 0xf242, 0xf245, 0xf245, 0xf26c, 0xf26c, 0xf279, 0xf279, 0xf2d0, 0xf2d0, 0xf2db, 0xf2db, 0xf2f2, 0xf2f2, + 0xf3c1, 0xf3c1, 0xf3fd, 0xf3fd, 0xf410, 0xf410, 0xf466, 0xf466, 0xf4ce, 0xf4ce, 0xf500, 0xf500, 0xf51f, 0xf51f, + 0xf538, 0xf538, 0xf545, 0xf545, 0xf547, 0xf548, 0xf57a, 0xf57a, 0xf5a2, 0xf5a2, 0xf5aa, 0xf5aa, 0xf5e7, 0xf5e7, + 0xf65d, 0xf65e, 0xf6a9, 0xf6a9, 0xf6cf, 0xf6cf, 0xf794, 0xf794, 0xf7c2, 0xf7c2, 0xf807, 0xf807, 0xf815, 0xf815, + 0xf818, 0xf818, 0xf84c, 0xf84c, 0xf8cc, 0xf8cc, 0x0, 0x0}; static constexpr ImWchar range_pf[] = { 0x2196, 0x2199, 0x219e, 0x21a1, 0x21b0, 0x21b3, 0x21ba, 0x21c3, 0x21c7, 0x21ca, 0x21d0, 0x21d4, 0x21dc, 0x21dd, - 0x21e0, 0x21e3, 0x21ed, 0x21ee, 0x21f7, 0x21f8, 0x21fa, 0x21fb, 0x227a, 0x227d, 0x235e, 0x235e, 0x2360, 0x2361, - 0x2364, 0x2366, 0x23b2, 0x23b4, 0x23f4, 0x23f7, 0x2427, 0x243a, 0x243c, 0x243c, 0x243e, 0x243e, 0x2460, 0x246b, + 0x21e0, 0x21e3, 0x21ed, 0x21ee, 0x21f7, 0x21f8, 0x21fa, 0x21fb, 0x227a, 0x227f, 0x2284, 0x2284, 0x235e, 0x235e, + 0x2360, 0x2361, 0x2364, 0x2366, 0x23b2, 0x23b4, 0x23f4, 0x23f7, 0x2427, 0x243a, 0x243c, 0x243e, 0x2460, 0x246b, 0x24f5, 0x24fd, 0x24ff, 0x24ff, 0x278a, 0x278e, 0x27fc, 0x27fc, 0xe001, 0xe001, 0xff21, 0xff3a, 0x0, 0x0}; { @@ -1006,14 +1006,14 @@ bool ImGuiManager::ProcessGenericInputEvent(GenericInputBinding key, float value ImGuiKey_GamepadL2, // R2 }; - if (!ImGui::GetCurrentContext() || !s_imgui_wants_keyboard.load(std::memory_order_acquire)) + if (!ImGui::GetCurrentContext()) return false; if (static_cast(key) >= std::size(key_map) || key_map[static_cast(key)] == ImGuiKey_None) return false; ImGui::GetIO().AddKeyAnalogEvent(key_map[static_cast(key)], (value > 0.0f), value); - return true; + return s_imgui_wants_keyboard.load(std::memory_order_acquire); } void ImGuiManager::CreateSoftwareCursorTextures()