diff --git a/src/core/fullscreen_ui.cpp b/src/core/fullscreen_ui.cpp index a5b091990..39a5934eb 100644 --- a/src/core/fullscreen_ui.cpp +++ b/src/core/fullscreen_ui.cpp @@ -82,6 +82,8 @@ using ImGuiFullscreen::FocusResetType; using ImGuiFullscreen::LAYOUT_FOOTER_HEIGHT; using ImGuiFullscreen::LAYOUT_HORIZONTAL_MENU_ITEM_IMAGE_SIZE; using ImGuiFullscreen::LAYOUT_LARGE_FONT_SIZE; +using ImGuiFullscreen::LAYOUT_LARGE_POPUP_PADDING; +using ImGuiFullscreen::LAYOUT_LARGE_POPUP_ROUNDING; using ImGuiFullscreen::LAYOUT_MEDIUM_FONT_SIZE; using ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT; using ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY; @@ -89,10 +91,11 @@ using ImGuiFullscreen::LAYOUT_MENU_BUTTON_X_PADDING; using ImGuiFullscreen::LAYOUT_MENU_BUTTON_Y_PADDING; using ImGuiFullscreen::LAYOUT_SCREEN_HEIGHT; using ImGuiFullscreen::LAYOUT_SCREEN_WIDTH; +using ImGuiFullscreen::LAYOUT_SMALL_POPUP_PADDING; using ImGuiFullscreen::UIStyle; using ImGuiFullscreen::AddNotification; -using ImGuiFullscreen::BeginFixedPopupModal; +using ImGuiFullscreen::BeginFixedPopupDialog; using ImGuiFullscreen::BeginFullscreenColumns; using ImGuiFullscreen::BeginFullscreenColumnWindow; using ImGuiFullscreen::BeginFullscreenWindow; @@ -101,8 +104,10 @@ using ImGuiFullscreen::BeginMenuButtons; using ImGuiFullscreen::BeginNavBar; using ImGuiFullscreen::CancelPendingMenuClose; using ImGuiFullscreen::CenterImage; +using ImGuiFullscreen::CloseFixedPopupDialog; +using ImGuiFullscreen::CloseFixedPopupDialogImmediately; using ImGuiFullscreen::DarkerColor; -using ImGuiFullscreen::EndFixedPopupModal; +using ImGuiFullscreen::EndFixedPopupDialog; using ImGuiFullscreen::EndFullscreenColumns; using ImGuiFullscreen::EndFullscreenColumnWindow; using ImGuiFullscreen::EndFullscreenWindow; @@ -116,6 +121,8 @@ using ImGuiFullscreen::GetCachedTexture; using ImGuiFullscreen::GetCachedTextureAsync; using ImGuiFullscreen::GetPlaceholderTexture; using ImGuiFullscreen::HorizontalMenuItem; +using ImGuiFullscreen::IsAnyFixedPopupDialogOpen; +using ImGuiFullscreen::IsFixedPopupDialogOpen; using ImGuiFullscreen::IsFocusResetFromWindowChange; using ImGuiFullscreen::IsFocusResetQueued; using ImGuiFullscreen::IsGamepadInputSource; @@ -137,6 +144,7 @@ using ImGuiFullscreen::NavTitle; using ImGuiFullscreen::OpenChoiceDialog; using ImGuiFullscreen::OpenConfirmMessageDialog; using ImGuiFullscreen::OpenFileSelector; +using ImGuiFullscreen::OpenFixedPopupDialog; using ImGuiFullscreen::OpenInfoMessageDialog; using ImGuiFullscreen::OpenInputStringDialog; using ImGuiFullscreen::PopPrimaryColor; @@ -226,7 +234,6 @@ static void DrawPauseMenu(); static void ExitFullscreenAndOpenURL(std::string_view url); static void CopyTextToClipboard(std::string title, std::string_view text); static void DrawAboutWindow(); -static void OpenAboutWindow(); static void FixStateIfPaused(); static void GetStandardSelectionFooterText(SmallStringBase& dest, bool back_instead_of_cancel); @@ -430,7 +437,8 @@ struct SaveStateListEntry static void InitializePlaceholderSaveStateListEntry(SaveStateListEntry* li, 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 bool InitializeSaveStateListEntryFromPath(SaveStateListEntry* li, std::string path, s32 slot, bool global, + std::string* media_path); static void ClearSaveStateEntryList(); static u32 PopulateSaveStateListEntries(const std::string& serial, std::optional undo_save_state); @@ -492,6 +500,9 @@ static constexpr std::array s_theme_names = { static constexpr std::array s_theme_values = {"", "Dark", "Light", "AMOLED", "CobaltSky", "GreyMatter", "GreenGiant", "PinkyPals", "DarkRuby", "PurpleRain"}; +static constexpr std::string_view RESUME_STATE_SELECTOR_DIALOG_NAME = "##resume_state_selector"; +static constexpr std::string_view ABOUT_DIALOG_NAME = "##about_duckstation"; + ////////////////////////////////////////////////////////////////////////// // State ////////////////////////////////////////////////////////////////////////// @@ -507,8 +518,6 @@ struct ALIGN_TO_CACHE_LINE UIState bool tried_to_initialize = false; bool pause_menu_was_open = false; bool was_paused_on_quick_menu_open = false; - bool about_window_open = false; - bool achievements_login_window_open = false; std::string current_game_title; std::string current_game_serial; std::string current_game_path; @@ -562,7 +571,6 @@ struct ALIGN_TO_CACHE_LINE UIState s32 save_state_selector_submenu_index = -1; bool save_state_selector_open = false; bool save_state_selector_loading = true; - bool save_state_selector_resuming = false; // Lazily populated cover images. std::unordered_map cover_image_map; @@ -755,8 +763,8 @@ bool FullscreenUI::HasActiveWindow() bool FullscreenUI::AreAnyDialogsOpen() { - return (s_state.save_state_selector_open || s_state.about_window_open || - s_state.input_binding_type != InputBindingInfo::Type::Unknown || ImGuiFullscreen::IsChoiceDialogOpen() || + return (s_state.save_state_selector_open || s_state.input_binding_type != InputBindingInfo::Type::Unknown || + ImGuiFullscreen::IsAnyFixedPopupDialogOpen() || ImGuiFullscreen::IsChoiceDialogOpen() || ImGuiFullscreen::IsInputDialogOpen() || ImGuiFullscreen::IsFileSelectorOpen() || ImGuiFullscreen::IsMessageBoxDialogOpen()); } @@ -938,7 +946,6 @@ void FullscreenUI::Shutdown(bool clear_state) s_state.current_pause_submenu = PauseSubMenu::None; s_state.pause_menu_was_open = false; s_state.was_paused_on_quick_menu_open = false; - s_state.about_window_open = false; Achievements::ClearUIState(); ClearInputBindingVariables(); @@ -1027,18 +1034,13 @@ void FullscreenUI::Render() break; } - if (s_state.save_state_selector_open) - { - if (s_state.save_state_selector_resuming) - DrawResumeStateSelector(); - else - DrawSaveStateSelector(s_state.save_state_selector_loading); - } - - if (s_state.about_window_open) + if (IsFixedPopupDialogOpen(ABOUT_DIALOG_NAME)) DrawAboutWindow(); - - if (s_state.input_binding_type != InputBindingInfo::Type::Unknown) + else if (IsFixedPopupDialogOpen(RESUME_STATE_SELECTOR_DIALOG_NAME)) + DrawResumeStateSelector(); + else if (s_state.save_state_selector_open) + DrawSaveStateSelector(s_state.save_state_selector_loading); + else if (s_state.input_binding_type != InputBindingInfo::Type::Unknown) DrawInputBindingWindow(); ImGuiFullscreen::EndLayout(); @@ -1253,17 +1255,17 @@ void FullscreenUI::DoResume() return; } - SaveStateListEntry slentry; - if (!InitializeSaveStateListEntryFromPath(&slentry, std::move(path), -1, false)) - return; - CloseSaveStateSelector(); + + SaveStateListEntry slentry; + if (!InitializeSaveStateListEntryFromPath(&slentry, std::move(path), -1, false, + &s_state.save_state_selector_game_path)) + { + return; + } + s_state.save_state_selector_slots.push_back(std::move(slentry)); - s_state.save_state_selector_game_path = {}; - s_state.save_state_selector_loading = true; - s_state.save_state_selector_open = true; - s_state.save_state_selector_resuming = true; - QueueResetFocus(FocusResetType::PopupOpened); + OpenFixedPopupDialog(RESUME_STATE_SELECTOR_DIALOG_NAME); } void FullscreenUI::DoStartFile() @@ -1969,7 +1971,7 @@ void FullscreenUI::DrawLandingWindow() if (!AreAnyDialogsOpen()) { if (ImGui::IsKeyPressed(ImGuiKey_GamepadBack, false) || ImGui::IsKeyPressed(ImGuiKey_F1, false)) - OpenAboutWindow(); + OpenFixedPopupDialog(ABOUT_DIALOG_NAME); else if (ImGui::IsKeyPressed(ImGuiKey_GamepadStart, false) || ImGui::IsKeyPressed(ImGuiKey_F3, false)) DoResume(); else if (ImGui::IsKeyPressed(ImGuiKey_NavGamepadMenu, false) || ImGui::IsKeyPressed(ImGuiKey_F11, false)) @@ -2323,6 +2325,9 @@ void FullscreenUI::ClearInputBindingVariables() s_state.input_binding_display_name = {}; s_state.input_binding_new_bindings = {}; s_state.input_binding_value_ranges = {}; + + if (IsFixedPopupDialogOpen(FSUI_ICONSTR(ICON_FA_GAMEPAD, "Set Input Binding"))) + CloseFixedPopupDialogImmediately(); } void FullscreenUI::BeginInputBinding(SettingsInterface* bsi, InputBindingInfo::Type type, std::string_view section, @@ -2341,6 +2346,7 @@ void FullscreenUI::BeginInputBinding(SettingsInterface* bsi, InputBindingInfo::T s_state.input_binding_new_bindings = {}; s_state.input_binding_value_ranges = {}; s_state.input_binding_start_time = Timer::GetCurrentValue(); + OpenFixedPopupDialog(FSUI_ICONSTR(ICON_FA_GAMEPAD, "Set Input Binding")); const bool game_settings = IsEditingGameSettings(bsi); @@ -2396,8 +2402,9 @@ void FullscreenUI::BeginInputBinding(SettingsInterface* bsi, InputBindingInfo::T bsi->SetStringValue(s_state.input_binding_section.c_str(), s_state.input_binding_key.c_str(), new_binding.c_str()); SetSettingsChanged(bsi); - ClearInputBindingVariables(); - QueueResetFocus(FocusResetType::PopupClosed); + + GPUThread::RunOnThread(&CloseFixedPopupDialog); + return InputInterceptHook::CallbackResult::RemoveHookAndStopProcessingEvent; } @@ -2474,27 +2481,27 @@ void FullscreenUI::DrawInputBindingWindow() INPUT_BINDING_TIMEOUT_SECONDS - Timer::ConvertValueToSeconds(Timer::GetCurrentValue() - s_state.input_binding_start_time); if (time_remaining <= 0.0) + { + InputManager::RemoveHook(); + CloseFixedPopupDialog(); + } + + if (!BeginFixedPopupDialog(LayoutScale(LAYOUT_SMALL_POPUP_PADDING), LayoutScale(LAYOUT_SMALL_POPUP_PADDING), + LayoutScale(500.0f, 0.0f))) { InputManager::RemoveHook(); ClearInputBindingVariables(); - QueueResetFocus(FocusResetType::PopupClosed); return; } - const char* title = FSUI_ICONSTR(ICON_FA_GAMEPAD, "Set Input Binding"); - ImGui::SetNextWindowSize(LayoutScale(500.0f, 0.0f)); - ImGui::OpenPopup(title); + ImGui::TextWrapped("%s", SmallString::from_format(FSUI_FSTR("Setting {} binding {}."), s_state.input_binding_section, + s_state.input_binding_display_name) + .c_str()); + ImGui::TextUnformatted(FSUI_CSTR("Push a controller button or axis now.")); + ImGui::NewLine(); + ImGui::TextUnformatted(SmallString::from_format(FSUI_FSTR("Timing out in {:.0f} seconds..."), time_remaining)); - if (BeginFixedPopupModal(title)) - { - ImGui::TextWrapped("%s", SmallString::from_format(FSUI_FSTR("Setting {} binding {}."), - s_state.input_binding_section, s_state.input_binding_display_name) - .c_str()); - ImGui::TextUnformatted(FSUI_CSTR("Push a controller button or axis now.")); - ImGui::NewLine(); - ImGui::TextUnformatted(SmallString::from_format(FSUI_FSTR("Timing out in {:.0f} seconds..."), time_remaining)); - EndFixedPopupModal(); - } + EndFixedPopupDialog(); } bool FullscreenUI::DrawToggleSetting(SettingsInterface* bsi, const char* title, const char* summary, @@ -2661,39 +2668,38 @@ void FullscreenUI::DrawIntRangeSetting(SettingsInterface* bsi, const char* title value.has_value() ? SmallString::from_sprintf(format, value.value()) : SmallString(FSUI_VSTR("Use Global Setting")); if (MenuButtonWithValue(title, summary, value_text.c_str(), enabled, height, font, summary_font)) - ImGui::OpenPopup(title); + OpenFixedPopupDialog(title); - ImGui::SetNextWindowSize(LayoutScale(500.0f, 194.0f)); - ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f)); - - bool is_open = true; - if (BeginFixedPopupModal(title, &is_open)) + if (!IsFixedPopupDialogOpen(title) || + !BeginFixedPopupDialog(LayoutScale(LAYOUT_SMALL_POPUP_PADDING), LayoutScale(LAYOUT_SMALL_POPUP_PADDING), + LayoutScale(500.0f, 194.0f))) { - BeginMenuButtons(); - - const float end = ImGui::GetCurrentWindow()->WorkRect.GetWidth(); - ImGui::SetNextItemWidth(end); - s32 dlg_value = static_cast(value.value_or(default_value)); - if (ImGui::SliderInt("##value", &dlg_value, min_value, max_value, format, ImGuiSliderFlags_NoInput)) - { - if (IsEditingGameSettings(bsi) && dlg_value == default_value) - bsi->DeleteValue(section, key); - else - bsi->SetIntValue(section, key, dlg_value); - - SetSettingsChanged(bsi); - } - - ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f)); - if (MenuButtonWithoutSummary(FSUI_CSTR("OK"), true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, UIStyle.LargeFont, - ImVec2(0.5f, 0.0f))) - { - ImGui::CloseCurrentPopup(); - } - EndMenuButtons(); - - EndFixedPopupModal(); + return; } + + BeginMenuButtons(); + + ImGui::SetNextItemWidth(ImGui::GetCurrentWindow()->WorkRect.GetWidth()); + s32 dlg_value = static_cast(value.value_or(default_value)); + if (ImGui::SliderInt("##value", &dlg_value, min_value, max_value, format, ImGuiSliderFlags_NoInput)) + { + if (IsEditingGameSettings(bsi) && dlg_value == default_value) + bsi->DeleteValue(section, key); + else + bsi->SetIntValue(section, key, dlg_value); + + SetSettingsChanged(bsi); + } + + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f)); + if (MenuButtonWithoutSummary(FSUI_CSTR("OK"), true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, UIStyle.LargeFont, + ImVec2(0.5f, 0.0f))) + { + CloseFixedPopupDialog(); + } + EndMenuButtons(); + + EndFixedPopupDialog(); } void FullscreenUI::DrawFloatRangeSetting(SettingsInterface* bsi, const char* title, const char* summary, @@ -2708,41 +2714,42 @@ void FullscreenUI::DrawFloatRangeSetting(SettingsInterface* bsi, const char* tit SmallString(FSUI_VSTR("Use Global Setting")); if (MenuButtonWithValue(title, summary, value_text.c_str(), enabled, height, font, summary_font)) - ImGui::OpenPopup(title); + OpenFixedPopupDialog(title); - ImGui::SetNextWindowSize(LayoutScale(500.0f, 194.0f)); - - bool is_open = true; - if (BeginFixedPopupModal(title, &is_open)) + if (!IsFixedPopupDialogOpen(title) || + !BeginFixedPopupDialog(LayoutScale(LAYOUT_SMALL_POPUP_PADDING), LayoutScale(LAYOUT_SMALL_POPUP_PADDING), + LayoutScale(500.0f, 194.0f))) { - BeginMenuButtons(); - - const float end = ImGui::GetCurrentWindow()->WorkRect.GetWidth(); - ImGui::SetNextItemWidth(end); - float dlg_value = value.value_or(default_value) * multiplier; - if (ImGui::SliderFloat("##value", &dlg_value, min_value * multiplier, max_value * multiplier, format, - ImGuiSliderFlags_NoInput)) - { - dlg_value /= multiplier; - - if (IsEditingGameSettings(bsi) && dlg_value == default_value) - bsi->DeleteValue(section, key); - else - bsi->SetFloatValue(section, key, dlg_value); - - SetSettingsChanged(bsi); - } - - ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f)); - if (MenuButtonWithoutSummary(FSUI_CSTR("OK"), true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, UIStyle.LargeFont, - ImVec2(0.5f, 0.0f))) - { - ImGui::CloseCurrentPopup(); - } - EndMenuButtons(); - - EndFixedPopupModal(); + return; } + + BeginMenuButtons(); + + const float end = ImGui::GetCurrentWindow()->WorkRect.GetWidth(); + ImGui::SetNextItemWidth(end); + float dlg_value = value.value_or(default_value) * multiplier; + if (ImGui::SliderFloat("##value", &dlg_value, min_value * multiplier, max_value * multiplier, format, + ImGuiSliderFlags_NoInput)) + { + dlg_value /= multiplier; + + if (IsEditingGameSettings(bsi) && dlg_value == default_value) + bsi->DeleteValue(section, key); + else + bsi->SetFloatValue(section, key, dlg_value); + + SetSettingsChanged(bsi); + } + + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f)); + if (MenuButtonWithoutSummary(FSUI_CSTR("OK"), true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, UIStyle.LargeFont, + ImVec2(0.5f, 0.0f))) + { + CloseFixedPopupDialog(); + } + EndMenuButtons(); + + EndFixedPopupDialog(); } void FullscreenUI::DrawFloatSpinBoxSetting(SettingsInterface* bsi, const char* title, const char* summary, @@ -2760,107 +2767,107 @@ void FullscreenUI::DrawFloatSpinBoxSetting(SettingsInterface* bsi, const char* t if (MenuButtonWithValue(title, summary, value_text.c_str(), enabled, height, font, summary_font)) { - ImGui::OpenPopup(title); + OpenFixedPopupDialog(title); manual_input = false; } - ImGui::SetNextWindowSize(LayoutScale(500.0f, 194.0f)); - - bool is_open = true; - if (BeginFixedPopupModal(title, &is_open)) + if (!IsFixedPopupDialogOpen(title) || + !BeginFixedPopupDialog(LayoutScale(LAYOUT_SMALL_POPUP_PADDING), LayoutScale(LAYOUT_SMALL_POPUP_PADDING), + LayoutScale(500.0f, 194.0f))) { - BeginMenuButtons(); - - float dlg_value = value.value_or(default_value) * multiplier; - bool dlg_value_changed = false; - - char str_value[32]; - std::snprintf(str_value, std::size(str_value), format, dlg_value); - - if (manual_input) - { - const float end = ImGui::GetCurrentWindow()->WorkRect.GetWidth(); - ImGui::SetNextItemWidth(end); - - // round trip to drop any suffixes (e.g. percent) - if (auto tmp_value = StringUtil::FromChars(str_value); tmp_value.has_value()) - { - std::snprintf(str_value, std::size(str_value), - ((tmp_value.value() - std::floor(tmp_value.value())) < 0.01f) ? "%.0f" : "%f", tmp_value.value()); - } - - if (ImGui::InputText("##value", str_value, std::size(str_value), ImGuiInputTextFlags_CharsDecimal)) - { - dlg_value = StringUtil::FromChars(str_value).value_or(dlg_value); - dlg_value_changed = true; - } - - ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f)); - } - else - { - const ImVec2& padding(ImGui::GetStyle().FramePadding); - ImVec2 button_pos(ImGui::GetCursorPos()); - - // Align value text in middle. - ImGui::SetCursorPosY( - button_pos.y + - ((LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY) + padding.y * 2.0f) - UIStyle.LargeFont->FontSize) * 0.5f); - ImGui::TextUnformatted(str_value); - - float step = 0; - if (FloatingButton(ICON_FA_CHEVRON_UP, padding.x, button_pos.y, -1.0f, -1.0f, 1.0f, 0.0f, true, UIStyle.LargeFont, - &button_pos, true)) - { - step = step_value; - } - if (FloatingButton(ICON_FA_CHEVRON_DOWN, button_pos.x - padding.x, button_pos.y, -1.0f, -1.0f, -1.0f, 0.0f, true, - UIStyle.LargeFont, &button_pos, true)) - { - step = -step_value; - } - if (FloatingButton(ICON_FA_KEYBOARD, button_pos.x - padding.x, button_pos.y, -1.0f, -1.0f, -1.0f, 0.0f, true, - UIStyle.LargeFont, &button_pos)) - { - manual_input = true; - } - if (FloatingButton(ICON_FA_TRASH, button_pos.x - padding.x, button_pos.y, -1.0f, -1.0f, -1.0f, 0.0f, true, - UIStyle.LargeFont, &button_pos)) - { - dlg_value = default_value * multiplier; - dlg_value_changed = true; - } - - if (step != 0) - { - dlg_value += step * multiplier; - dlg_value_changed = true; - } - - ImGui::SetCursorPosY(button_pos.y + (padding.y * 2.0f) + - LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY + 10.0f)); - } - - if (dlg_value_changed) - { - dlg_value = std::clamp(dlg_value / multiplier, min_value, max_value); - if (IsEditingGameSettings(bsi) && dlg_value == default_value) - bsi->DeleteValue(section, key); - else - bsi->SetFloatValue(section, key, dlg_value); - - SetSettingsChanged(bsi); - } - - if (MenuButtonWithoutSummary(FSUI_CSTR("OK"), true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, UIStyle.LargeFont, - ImVec2(0.5f, 0.0f))) - { - ImGui::CloseCurrentPopup(); - } - EndMenuButtons(); - - EndFixedPopupModal(); + return; } + + BeginMenuButtons(); + + float dlg_value = value.value_or(default_value) * multiplier; + bool dlg_value_changed = false; + + char str_value[32]; + std::snprintf(str_value, std::size(str_value), format, dlg_value); + + if (manual_input) + { + const float end = ImGui::GetCurrentWindow()->WorkRect.GetWidth(); + ImGui::SetNextItemWidth(end); + + // round trip to drop any suffixes (e.g. percent) + if (auto tmp_value = StringUtil::FromChars(str_value); tmp_value.has_value()) + { + std::snprintf(str_value, std::size(str_value), + ((tmp_value.value() - std::floor(tmp_value.value())) < 0.01f) ? "%.0f" : "%f", tmp_value.value()); + } + + if (ImGui::InputText("##value", str_value, std::size(str_value), ImGuiInputTextFlags_CharsDecimal)) + { + dlg_value = StringUtil::FromChars(str_value).value_or(dlg_value); + dlg_value_changed = true; + } + + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f)); + } + else + { + const ImVec2& padding(ImGui::GetStyle().FramePadding); + ImVec2 button_pos(ImGui::GetCursorPos()); + + // Align value text in middle. + ImGui::SetCursorPosY( + button_pos.y + + ((LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY) + padding.y * 2.0f) - UIStyle.LargeFont->FontSize) * 0.5f); + ImGui::TextUnformatted(str_value); + + float step = 0; + if (FloatingButton(ICON_FA_CHEVRON_UP, padding.x, button_pos.y, -1.0f, -1.0f, 1.0f, 0.0f, true, UIStyle.LargeFont, + &button_pos, true)) + { + step = step_value; + } + if (FloatingButton(ICON_FA_CHEVRON_DOWN, button_pos.x - padding.x, button_pos.y, -1.0f, -1.0f, -1.0f, 0.0f, true, + UIStyle.LargeFont, &button_pos, true)) + { + step = -step_value; + } + if (FloatingButton(ICON_FA_KEYBOARD, button_pos.x - padding.x, button_pos.y, -1.0f, -1.0f, -1.0f, 0.0f, true, + UIStyle.LargeFont, &button_pos)) + { + manual_input = true; + } + if (FloatingButton(ICON_FA_TRASH, button_pos.x - padding.x, button_pos.y, -1.0f, -1.0f, -1.0f, 0.0f, true, + UIStyle.LargeFont, &button_pos)) + { + dlg_value = default_value * multiplier; + dlg_value_changed = true; + } + + if (step != 0) + { + dlg_value += step * multiplier; + dlg_value_changed = true; + } + + ImGui::SetCursorPosY(button_pos.y + (padding.y * 2.0f) + LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY + 10.0f)); + } + + if (dlg_value_changed) + { + dlg_value = std::clamp(dlg_value / multiplier, min_value, max_value); + if (IsEditingGameSettings(bsi) && dlg_value == default_value) + bsi->DeleteValue(section, key); + else + bsi->SetFloatValue(section, key, dlg_value); + + SetSettingsChanged(bsi); + } + + if (MenuButtonWithoutSummary(FSUI_CSTR("OK"), true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, UIStyle.LargeFont, + ImVec2(0.5f, 0.0f))) + { + CloseFixedPopupDialog(); + } + EndMenuButtons(); + + EndFixedPopupDialog(); } bool FullscreenUI::DrawIntRectSetting(SettingsInterface* bsi, const char* title, const char* summary, @@ -2887,90 +2894,91 @@ bool FullscreenUI::DrawIntRectSetting(SettingsInterface* bsi, const char* title, TinyString(FSUI_VSTR("Default"))); if (MenuButtonWithValue(title, summary, value_text.c_str(), enabled, height, font, summary_font)) - ImGui::OpenPopup(title); + OpenFixedPopupDialog(title); - ImGui::SetNextWindowSize(LayoutScale(500.0f, 370.0f)); - - bool is_open = true; - bool changed = false; - if (BeginFixedPopupModal(title, &is_open)) + if (!IsFixedPopupDialogOpen(title) || + !BeginFixedPopupDialog(LayoutScale(LAYOUT_SMALL_POPUP_PADDING), LayoutScale(LAYOUT_SMALL_POPUP_PADDING), + LayoutScale(500.0f, 370.0f))) { - s32 dlg_left_value = static_cast(left_value.value_or(default_left)); - s32 dlg_top_value = static_cast(top_value.value_or(default_top)); - s32 dlg_right_value = static_cast(right_value.value_or(default_right)); - s32 dlg_bottom_value = static_cast(bottom_value.value_or(default_bottom)); - - BeginMenuButtons(); - - const float midpoint = LayoutScale(150.0f); - const float end = (ImGui::GetCurrentWindow()->WorkRect.GetWidth() - midpoint) + ImGui::GetStyle().WindowPadding.x; - ImGui::TextUnformatted("Left: "); - ImGui::SameLine(midpoint); - ImGui::SetNextItemWidth(end); - const bool left_modified = - ImGui::SliderInt("##left", &dlg_left_value, min_value, max_value, format, ImGuiSliderFlags_NoInput); - ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f)); - ImGui::TextUnformatted("Top: "); - ImGui::SameLine(midpoint); - ImGui::SetNextItemWidth(end); - const bool top_modified = - ImGui::SliderInt("##top", &dlg_top_value, min_value, max_value, format, ImGuiSliderFlags_NoInput); - ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f)); - ImGui::TextUnformatted("Right: "); - ImGui::SameLine(midpoint); - ImGui::SetNextItemWidth(end); - const bool right_modified = - ImGui::SliderInt("##right", &dlg_right_value, min_value, max_value, format, ImGuiSliderFlags_NoInput); - ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f)); - ImGui::TextUnformatted("Bottom: "); - ImGui::SameLine(midpoint); - ImGui::SetNextItemWidth(end); - const bool bottom_modified = - ImGui::SliderInt("##bottom", &dlg_bottom_value, min_value, max_value, format, ImGuiSliderFlags_NoInput); - ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f)); - if (left_modified) - { - if (IsEditingGameSettings(bsi) && dlg_left_value == default_left) - bsi->DeleteValue(section, left_key); - else - bsi->SetIntValue(section, left_key, dlg_left_value); - } - if (top_modified) - { - if (IsEditingGameSettings(bsi) && dlg_top_value == default_top) - bsi->DeleteValue(section, top_key); - else - bsi->SetIntValue(section, top_key, dlg_top_value); - } - if (right_modified) - { - if (IsEditingGameSettings(bsi) && dlg_right_value == default_right) - bsi->DeleteValue(section, right_key); - else - bsi->SetIntValue(section, right_key, dlg_right_value); - } - if (bottom_modified) - { - if (IsEditingGameSettings(bsi) && dlg_bottom_value == default_bottom) - bsi->DeleteValue(section, bottom_key); - else - bsi->SetIntValue(section, bottom_key, dlg_bottom_value); - } - - changed = (left_modified || top_modified || right_modified || bottom_modified); - if (changed) - SetSettingsChanged(bsi); - - if (MenuButtonWithoutSummary(FSUI_CSTR("OK"), true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, UIStyle.LargeFont, - ImVec2(0.5f, 0.0f))) - { - ImGui::CloseCurrentPopup(); - } - EndMenuButtons(); - - EndFixedPopupModal(); + return false; } + s32 dlg_left_value = static_cast(left_value.value_or(default_left)); + s32 dlg_top_value = static_cast(top_value.value_or(default_top)); + s32 dlg_right_value = static_cast(right_value.value_or(default_right)); + s32 dlg_bottom_value = static_cast(bottom_value.value_or(default_bottom)); + bool changed = false; + + BeginMenuButtons(); + + const float midpoint = LayoutScale(150.0f); + const float end = (ImGui::GetCurrentWindow()->WorkRect.GetWidth() - midpoint) + ImGui::GetStyle().WindowPadding.x; + ImGui::TextUnformatted("Left: "); + ImGui::SameLine(midpoint); + ImGui::SetNextItemWidth(end); + const bool left_modified = + ImGui::SliderInt("##left", &dlg_left_value, min_value, max_value, format, ImGuiSliderFlags_NoInput); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f)); + ImGui::TextUnformatted("Top: "); + ImGui::SameLine(midpoint); + ImGui::SetNextItemWidth(end); + const bool top_modified = + ImGui::SliderInt("##top", &dlg_top_value, min_value, max_value, format, ImGuiSliderFlags_NoInput); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f)); + ImGui::TextUnformatted("Right: "); + ImGui::SameLine(midpoint); + ImGui::SetNextItemWidth(end); + const bool right_modified = + ImGui::SliderInt("##right", &dlg_right_value, min_value, max_value, format, ImGuiSliderFlags_NoInput); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f)); + ImGui::TextUnformatted("Bottom: "); + ImGui::SameLine(midpoint); + ImGui::SetNextItemWidth(end); + const bool bottom_modified = + ImGui::SliderInt("##bottom", &dlg_bottom_value, min_value, max_value, format, ImGuiSliderFlags_NoInput); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f)); + if (left_modified) + { + if (IsEditingGameSettings(bsi) && dlg_left_value == default_left) + bsi->DeleteValue(section, left_key); + else + bsi->SetIntValue(section, left_key, dlg_left_value); + } + if (top_modified) + { + if (IsEditingGameSettings(bsi) && dlg_top_value == default_top) + bsi->DeleteValue(section, top_key); + else + bsi->SetIntValue(section, top_key, dlg_top_value); + } + if (right_modified) + { + if (IsEditingGameSettings(bsi) && dlg_right_value == default_right) + bsi->DeleteValue(section, right_key); + else + bsi->SetIntValue(section, right_key, dlg_right_value); + } + if (bottom_modified) + { + if (IsEditingGameSettings(bsi) && dlg_bottom_value == default_bottom) + bsi->DeleteValue(section, bottom_key); + else + bsi->SetIntValue(section, bottom_key, dlg_bottom_value); + } + + changed = (left_modified || top_modified || right_modified || bottom_modified); + if (changed) + SetSettingsChanged(bsi); + + if (MenuButtonWithoutSummary(FSUI_CSTR("OK"), true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, UIStyle.LargeFont, + ImVec2(0.5f, 0.0f))) + { + CloseFixedPopupDialog(); + } + EndMenuButtons(); + + EndFixedPopupDialog(); + return changed; } @@ -2992,101 +3000,101 @@ void FullscreenUI::DrawIntSpinBoxSetting(SettingsInterface* bsi, const char* tit if (MenuButtonWithValue(title, summary, value_text, enabled, height, font, summary_font)) { - ImGui::OpenPopup(title); + OpenFixedPopupDialog(title); manual_input = false; } - ImGui::SetNextWindowSize(LayoutScale(500.0f, 194.0f)); - - bool is_open = true; - if (BeginFixedPopupModal(title, &is_open)) + if (!IsFixedPopupDialogOpen(title) || + !BeginFixedPopupDialog(LayoutScale(LAYOUT_SMALL_POPUP_PADDING), LayoutScale(LAYOUT_SMALL_POPUP_PADDING), + LayoutScale(500.0f, 194.0f))) { - BeginMenuButtons(); - - s32 dlg_value = static_cast(value.value_or(default_value)); - bool dlg_value_changed = false; - - char str_value[32]; - std::snprintf(str_value, std::size(str_value), format, dlg_value); - - if (manual_input) - { - const float end = ImGui::GetCurrentWindow()->WorkRect.GetWidth(); - ImGui::SetNextItemWidth(end); - - std::snprintf(str_value, std::size(str_value), "%d", dlg_value); - if (ImGui::InputText("##value", str_value, std::size(str_value), ImGuiInputTextFlags_CharsDecimal)) - { - dlg_value = StringUtil::FromChars(str_value).value_or(dlg_value); - dlg_value_changed = true; - } - - ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f)); - } - else - { - const ImVec2& padding(ImGui::GetStyle().FramePadding); - ImVec2 button_pos(ImGui::GetCursorPos()); - - // Align value text in middle. - ImGui::SetCursorPosY( - button_pos.y + - ((LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY) + padding.y * 2.0f) - UIStyle.LargeFont->FontSize) * 0.5f); - ImGui::TextUnformatted(str_value); - - s32 step = 0; - if (FloatingButton(ICON_FA_CHEVRON_UP, padding.x, button_pos.y, -1.0f, -1.0f, 1.0f, 0.0f, true, UIStyle.LargeFont, - &button_pos, true)) - { - step = step_value; - } - if (FloatingButton(ICON_FA_CHEVRON_DOWN, button_pos.x - padding.x, button_pos.y, -1.0f, -1.0f, -1.0f, 0.0f, true, - UIStyle.LargeFont, &button_pos, true)) - { - step = -step_value; - } - if (FloatingButton(ICON_FA_KEYBOARD, button_pos.x - padding.x, button_pos.y, -1.0f, -1.0f, -1.0f, 0.0f, true, - UIStyle.LargeFont, &button_pos)) - { - manual_input = true; - } - if (FloatingButton(ICON_FA_TRASH, button_pos.x - padding.x, button_pos.y, -1.0f, -1.0f, -1.0f, 0.0f, true, - UIStyle.LargeFont, &button_pos)) - { - dlg_value = default_value; - dlg_value_changed = true; - } - - if (step != 0) - { - dlg_value += step; - dlg_value_changed = true; - } - - ImGui::SetCursorPosY(button_pos.y + (padding.y * 2.0f) + - LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY + 10.0f)); - } - - if (dlg_value_changed) - { - dlg_value = std::clamp(dlg_value, min_value, max_value); - if (IsEditingGameSettings(bsi) && dlg_value == default_value) - bsi->DeleteValue(section, key); - else - bsi->SetIntValue(section, key, dlg_value); - - SetSettingsChanged(bsi); - } - - if (MenuButtonWithoutSummary(FSUI_CSTR("OK"), true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, UIStyle.LargeFont, - ImVec2(0.5f, 0.0f))) - { - ImGui::CloseCurrentPopup(); - } - EndMenuButtons(); - - EndFixedPopupModal(); + return; } + + BeginMenuButtons(); + + s32 dlg_value = static_cast(value.value_or(default_value)); + bool dlg_value_changed = false; + + char str_value[32]; + std::snprintf(str_value, std::size(str_value), format, dlg_value); + + if (manual_input) + { + const float end = ImGui::GetCurrentWindow()->WorkRect.GetWidth(); + ImGui::SetNextItemWidth(end); + + std::snprintf(str_value, std::size(str_value), "%d", dlg_value); + if (ImGui::InputText("##value", str_value, std::size(str_value), ImGuiInputTextFlags_CharsDecimal)) + { + dlg_value = StringUtil::FromChars(str_value).value_or(dlg_value); + dlg_value_changed = true; + } + + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f)); + } + else + { + const ImVec2& padding(ImGui::GetStyle().FramePadding); + ImVec2 button_pos(ImGui::GetCursorPos()); + + // Align value text in middle. + ImGui::SetCursorPosY( + button_pos.y + + ((LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY) + padding.y * 2.0f) - UIStyle.LargeFont->FontSize) * 0.5f); + ImGui::TextUnformatted(str_value); + + s32 step = 0; + if (FloatingButton(ICON_FA_CHEVRON_UP, padding.x, button_pos.y, -1.0f, -1.0f, 1.0f, 0.0f, true, UIStyle.LargeFont, + &button_pos, true)) + { + step = step_value; + } + if (FloatingButton(ICON_FA_CHEVRON_DOWN, button_pos.x - padding.x, button_pos.y, -1.0f, -1.0f, -1.0f, 0.0f, true, + UIStyle.LargeFont, &button_pos, true)) + { + step = -step_value; + } + if (FloatingButton(ICON_FA_KEYBOARD, button_pos.x - padding.x, button_pos.y, -1.0f, -1.0f, -1.0f, 0.0f, true, + UIStyle.LargeFont, &button_pos)) + { + manual_input = true; + } + if (FloatingButton(ICON_FA_TRASH, button_pos.x - padding.x, button_pos.y, -1.0f, -1.0f, -1.0f, 0.0f, true, + UIStyle.LargeFont, &button_pos)) + { + dlg_value = default_value; + dlg_value_changed = true; + } + + if (step != 0) + { + dlg_value += step; + dlg_value_changed = true; + } + + ImGui::SetCursorPosY(button_pos.y + (padding.y * 2.0f) + LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY + 10.0f)); + } + + if (dlg_value_changed) + { + dlg_value = std::clamp(dlg_value, min_value, max_value); + if (IsEditingGameSettings(bsi) && dlg_value == default_value) + bsi->DeleteValue(section, key); + else + bsi->SetIntValue(section, key, dlg_value); + + SetSettingsChanged(bsi); + } + + if (MenuButtonWithoutSummary(FSUI_CSTR("OK"), true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, UIStyle.LargeFont, + ImVec2(0.5f, 0.0f))) + { + CloseFixedPopupDialog(); + } + EndMenuButtons(); + + EndFixedPopupDialog(); } [[maybe_unused]] void @@ -4737,7 +4745,7 @@ void FullscreenUI::DrawControllerSettingsPage() "Determines the frequency at which the macro will toggle the buttons on and off (aka auto fire)."), freq_summary, true)) { - ImGui::OpenPopup(freq_label.c_str()); + OpenFixedPopupDialog(freq_label); } DrawFloatSpinBoxSetting(bsi, FSUI_ICONSTR(ICON_FA_ARROW_DOWN, "Pressure"), @@ -4750,13 +4758,15 @@ void FullscreenUI::DrawControllerSettingsPage() section, TinyString::from_format("Macro{}Deadzone", macro_index + 1).c_str(), 0.0f, 0.00f, 1.0f, 0.01f, 100.0f, "%.0f%%"); - ImGui::SetNextWindowSize(LayoutScale(500.0f, 180.0f)); - - bool is_open = true; - if (BeginFixedPopupModal(freq_label, &is_open)) + if (IsFixedPopupDialogOpen(freq_label) && + BeginFixedPopupDialog(LayoutScale(LAYOUT_SMALL_POPUP_PADDING), LayoutScale(LAYOUT_SMALL_POPUP_PADDING), + LayoutScale(500.0f, 194.0f))) { - ImGui::SetNextItemWidth(LayoutScale(450.0f)); - if (ImGui::SliderInt("##value", &frequency, 0, 60, FSUI_CSTR("Toggle every %d frames"), + BeginMenuButtons(); + + ImGui::SetNextItemWidth(ImGui::GetCurrentWindow()->WorkRect.GetWidth()); + if (ImGui::SliderInt("##value", &frequency, 0, 60, + (frequency == 0) ? FSUI_CSTR("Disabled") : FSUI_CSTR("Toggle every %d frames"), ImGuiSliderFlags_NoInput)) { if (frequency == 0) @@ -4765,12 +4775,16 @@ void FullscreenUI::DrawControllerSettingsPage() bsi->SetIntValue(section.c_str(), freq_key.c_str(), frequency); } - BeginMenuButtons(); - if (MenuButton(FSUI_CSTR("OK"), nullptr, true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY)) - ImGui::CloseCurrentPopup(); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f)); + if (MenuButtonWithoutSummary(FSUI_CSTR("OK"), true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, UIStyle.LargeFont, + ImVec2(0.5f, 0.0f))) + { + CloseFixedPopupDialog(); + } + EndMenuButtons(); - EndFixedPopupModal(); + EndFixedPopupDialog(); } } @@ -5553,7 +5567,7 @@ void FullscreenUI::DrawPostProcessingSettingsPage() case PostProcessing::ShaderOption::Type::Bool: { bool value = (opt.value[0].int_value != 0); - tstr.format(ICON_FA_COGS "{}", opt.ui_name); + tstr.format(ICON_FA_COGS " {}", opt.ui_name); if (ToggleButton(tstr, (opt.default_value[0].int_value != 0) ? FSUI_CSTR("Default: Enabled") : FSUI_CSTR("Default: Disabled"), @@ -5569,38 +5583,20 @@ void FullscreenUI::DrawPostProcessingSettingsPage() case PostProcessing::ShaderOption::Type::Float: { - tstr.format(ICON_FA_RULER_VERTICAL "{}##{}", opt.ui_name, opt.name); + tstr.format(ICON_FA_RULER_VERTICAL " {}##{}", opt.ui_name, opt.name); str.format(FSUI_FSTR("Value: {} | Default: {} | Minimum: {} | Maximum: {}"), opt.value[0].float_value, opt.default_value[0].float_value, opt.min_value[0].float_value, opt.max_value[0].float_value); if (MenuButton(tstr, str)) - ImGui::OpenPopup(tstr); + OpenFixedPopupDialog(tstr); - ImGui::SetNextWindowSize(LayoutScale(500.0f, 194.0f)); - - bool is_open = true; - if (BeginFixedPopupModal(tstr, &is_open)) + if (IsFixedPopupDialogOpen(tstr) && + BeginFixedPopupDialog(LayoutScale(LAYOUT_SMALL_POPUP_PADDING), LayoutScale(LAYOUT_SMALL_POPUP_PADDING), + LayoutScale(500.0f, 194.0f))) { BeginMenuButtons(); const float end = ImGui::GetCurrentWindow()->WorkRect.GetWidth(); -#if 0 - for (u32 i = 0; i < opt.vector_size; i++) - { - static constexpr const char* components[] = { "X", "Y", "Z", "W" }; - if (opt.vector_size == 1) - tstr.Assign("##value"); - else - tstr.Fmt("{}##value{}", components[i], i); - - ImGui::SetNextItemWidth(end); - if (ImGui::SliderFloat(tstr, &opt.value[i].float_value, opt.min_value[i].float_value, - opt.max_value[i].float_value, "%f", ImGuiSliderFlags_NoInput)) - { - SavePostProcessingChain(); - } - } -#else ImGui::SetNextItemWidth(end); bool changed = false; @@ -5641,55 +5637,36 @@ void FullscreenUI::DrawPostProcessingSettingsPage() SetSettingsChanged(bsi); reload_pending = true; } -#endif ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f)); if (MenuButtonWithoutSummary(FSUI_CSTR("OK"), true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, UIStyle.LargeFont, ImVec2(0.5f, 0.0f))) { - ImGui::CloseCurrentPopup(); + CloseFixedPopupDialog(); } + EndMenuButtons(); - EndFixedPopupModal(); + EndFixedPopupDialog(); } } break; case PostProcessing::ShaderOption::Type::Int: { - tstr.format(ICON_FA_RULER_VERTICAL "{}##{}", opt.ui_name, opt.name); + tstr.format(ICON_FA_RULER_VERTICAL " {}##{}", opt.ui_name, opt.name); str.format(FSUI_FSTR("Value: {} | Default: {} | Minimum: {} | Maximum: {}"), opt.value[0].int_value, opt.default_value[0].int_value, opt.min_value[0].int_value, opt.max_value[0].int_value); if (MenuButton(tstr, str)) - ImGui::OpenPopup(tstr); + OpenFixedPopupDialog(tstr); - ImGui::SetNextWindowSize(LayoutScale(500.0f, 194.0f)); - - bool is_open = true; - if (BeginFixedPopupModal(tstr, &is_open)) + if (IsFixedPopupDialogOpen(tstr) && + BeginFixedPopupDialog(LayoutScale(LAYOUT_SMALL_POPUP_PADDING), LayoutScale(LAYOUT_SMALL_POPUP_PADDING), + LayoutScale(500.0f, 194.0f))) { BeginMenuButtons(); const float end = ImGui::GetCurrentWindow()->WorkRect.GetWidth(); - -#if 0 - for (u32 i = 0; i < opt.vector_size; i++) - { - static constexpr const char* components[] = { "X", "Y", "Z", "W" }; - if (opt.vector_size == 1) - tstr.Assign("##value"); - else - tstr.Fmt("{}##value{}", components[i], i); - - ImGui::SetNextItemWidth(end); - if (ImGui::SliderInt(tstr, &opt.value[i].int_value, opt.min_value[i].int_value, - opt.max_value[i].int_value, "%d", ImGuiSliderFlags_NoInput)) - { - SavePostProcessingChain(); - } - } -#else bool changed = false; ImGui::SetNextItemWidth(end); switch (opt.vector_size) @@ -5729,17 +5706,16 @@ void FullscreenUI::DrawPostProcessingSettingsPage() SetSettingsChanged(bsi); reload_pending = true; } -#endif ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f)); if (MenuButtonWithoutSummary(FSUI_CSTR("OK"), true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, UIStyle.LargeFont, ImVec2(0.5f, 0.0f))) { - ImGui::CloseCurrentPopup(); + CloseFixedPopupDialog(); } EndMenuButtons(); - EndFixedPopupModal(); + EndFixedPopupDialog(); } } break; @@ -6022,13 +5998,12 @@ void FullscreenUI::DrawAchievementsSettingsPage() { MenuButtonWithoutSummary(FSUI_ICONSTR(ICON_FA_USER, "Not Logged In"), false); - if (MenuButton(FSUI_ICONSTR(ICON_FA_KEY, "Login"), FSUI_CSTR("Logs in to RetroAchievements."))) - { - s_state.achievements_login_window_open = true; - QueueResetFocus(FocusResetType::PopupOpened); - } + static constexpr std::string_view popup_title = "##achievements_login"; - if (s_state.achievements_login_window_open) + if (MenuButton(FSUI_ICONSTR(ICON_FA_KEY, "Login"), FSUI_CSTR("Logs in to RetroAchievements."))) + OpenFixedPopupDialog(popup_title); + + if (IsFixedPopupDialogOpen(popup_title)) DrawAchievementsLoginWindow(); } @@ -6065,99 +6040,85 @@ void FullscreenUI::DrawAchievementsLoginWindow() static char username[256] = {}; static char password[256] = {}; + static std::unique_ptr login_error; - static constexpr auto actually_close_popup = []() { + if (!BeginFixedPopupDialog(LayoutScale(LAYOUT_LARGE_POPUP_PADDING), LayoutScale(LAYOUT_LARGE_POPUP_ROUNDING), + LayoutScale(600.0f, 0.0f))) + { std::memset(username, 0, sizeof(username)); std::memset(password, 0, sizeof(password)); - s_state.achievements_login_window_open = false; - QueueResetFocus(FocusResetType::PopupClosed); - }; + login_error.reset(); + return; + } - ImGui::SetNextWindowSize(LayoutScale(600.0f, 0.0f)); + BeginMenuButtons(); - const char* popup_title = FSUI_ICONSTR(ICON_FA_KEY, "RetroAchievements Login"); - bool popup_closed = false; - ImGui::OpenPopup(popup_title); - if (BeginFixedPopupModal(popup_title, nullptr)) + if (!login_error) { - BeginMenuButtons(); - ImGui::TextWrapped( FSUI_CSTR("Please enter your user name and password for retroachievements.org below. Your password will " "not be saved in DuckStation, an access token will be generated and used instead.")); - - ImGui::NewLine(); - - const bool is_logging_in = ImGuiFullscreen::IsBackgroundProgressDialogOpen(LOGIN_PROGRESS_NAME); - ResetFocusHere(); - - const float item_margin = LayoutScale(10.0f); - const float item_width = LayoutScale(550.0f); - - ImGui::SetCursorPosX((ImGui::GetWindowWidth() - item_width) * 0.5f); - ImGui::SetNextItemWidth(item_width); - ImGui::InputTextWithHint("##username", FSUI_CSTR("User Name"), username, sizeof(username), - is_logging_in ? ImGuiInputTextFlags_ReadOnly : 0); - ImGui::NextColumn(); - - ImGui::SetCursorPosX((ImGui::GetWindowWidth() - item_width) * 0.5f); - ImGui::SetCursorPosY(ImGui::GetCursorPosY() + item_margin); - ImGui::SetNextItemWidth(item_width); - ImGui::InputTextWithHint("##password", FSUI_CSTR("Password"), password, sizeof(password), - is_logging_in ? (ImGuiInputTextFlags_ReadOnly | ImGuiInputTextFlags_Password) : - ImGuiInputTextFlags_Password); - - ImGui::SetCursorPosY(ImGui::GetCursorPosY() + item_margin); - - const bool login_enabled = (std::strlen(username) > 0 && std::strlen(password) > 0 && !is_logging_in); - - if (MenuButtonWithoutSummary(FSUI_ICONSTR(ICON_FA_KEY, "Login"), login_enabled)) - { - ImGuiFullscreen::OpenBackgroundProgressDialog(LOGIN_PROGRESS_NAME, FSUI_STR("Logging in to RetroAchievements..."), - 0, 0, 0); - - Host::RunOnCPUThread([username = std::string(username), password = std::string(password)]() { - Error error; - const bool result = Achievements::Login(username.c_str(), password.c_str(), &error); - GPUThread::RunOnThread([result, error = std::move(error)]() { - ImGuiFullscreen::CloseBackgroundProgressDialog(LOGIN_PROGRESS_NAME); - - if (result) - { - actually_close_popup(); - return; - } - - // keep popup open on failure - // because of the whole popup stack thing, we need to hide the dialog while this popup is visible - s_state.achievements_login_window_open = false; - ImGuiFullscreen::OpenInfoMessageDialog( - FSUI_STR("Login Error"), - fmt::format(FSUI_FSTR("Login Failed.\nError: {}\nPlease check your username and password, and try again."), - error.GetDescription()), - []() { - s_state.achievements_login_window_open = true; - QueueResetFocus(FocusResetType::PopupOpened); - }, - FSUI_ICONSTR(ICON_FA_TIMES, "Close")); - }); - }); - } - - if (MenuButtonWithoutSummary(FSUI_ICONSTR(ICON_FA_TIMES, "Cancel"), !is_logging_in)) - popup_closed = true; - - popup_closed = popup_closed || (!is_logging_in && WantsToCloseMenu()); - if (popup_closed) - ImGui::CloseCurrentPopup(); - - EndMenuButtons(); - - EndFixedPopupModal(); + } + else + { + ImGui::TextWrapped("%s", login_error->c_str()); } - if (popup_closed) - actually_close_popup(); + ImGui::NewLine(); + + const bool is_logging_in = ImGuiFullscreen::IsBackgroundProgressDialogOpen(LOGIN_PROGRESS_NAME); + ResetFocusHere(); + + const float item_margin = LayoutScale(10.0f); + const float item_width = LayoutScale(550.0f); + + ImGui::SetCursorPosX((ImGui::GetWindowWidth() - item_width) * 0.5f); + ImGui::SetNextItemWidth(item_width); + ImGui::InputTextWithHint("##username", FSUI_CSTR("User Name"), username, sizeof(username), + is_logging_in ? ImGuiInputTextFlags_ReadOnly : 0); + ImGui::NextColumn(); + + ImGui::SetCursorPosX((ImGui::GetWindowWidth() - item_width) * 0.5f); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + item_margin); + ImGui::SetNextItemWidth(item_width); + ImGui::InputTextWithHint("##password", FSUI_CSTR("Password"), password, sizeof(password), + is_logging_in ? (ImGuiInputTextFlags_ReadOnly | ImGuiInputTextFlags_Password) : + ImGuiInputTextFlags_Password); + + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + item_margin); + + const bool login_enabled = (std::strlen(username) > 0 && std::strlen(password) > 0 && !is_logging_in); + + if (MenuButtonWithoutSummary(FSUI_ICONSTR(ICON_FA_KEY, "Login"), login_enabled)) + { + ImGuiFullscreen::OpenBackgroundProgressDialog(LOGIN_PROGRESS_NAME, FSUI_STR("Logging in to RetroAchievements..."), + 0, 0, 0); + + Host::RunOnCPUThread([username = std::string(username), password = std::string(password)]() { + Error error; + const bool result = Achievements::Login(username.c_str(), password.c_str(), &error); + GPUThread::RunOnThread([result, error = std::move(error)]() { + ImGuiFullscreen::CloseBackgroundProgressDialog(LOGIN_PROGRESS_NAME); + + if (result) + { + CloseFixedPopupDialog(); + return; + } + + login_error = std::make_unique( + fmt::format(FSUI_FSTR("Login Failed.\nError: {}\nPlease check your username and password, and try again."), + error.GetDescription())); + }); + }); + } + + if (MenuButtonWithoutSummary(FSUI_ICONSTR(ICON_FA_TIMES, "Cancel"), !is_logging_in)) + CloseFixedPopupDialog(); + + EndMenuButtons(); + + EndFixedPopupDialog(); } void FullscreenUI::DrawAdvancedSettingsPage() @@ -6324,12 +6285,11 @@ void FullscreenUI::DrawPatchesOrCheatsSettingsPage(bool cheats) constexpr s32 step_value = 1; if (MenuButtonWithValue(title.c_str(), ci.description.c_str(), visible_value.c_str())) - ImGui::OpenPopup(title); + OpenFixedPopupDialog(title); - ImGui::SetNextWindowSize(LayoutScale(500.0f, 194.0f)); - - bool is_open = true; - if (BeginFixedPopupModal(title, &is_open)) + if (IsFixedPopupDialogOpen(title) && + BeginFixedPopupDialog(LayoutScale(LAYOUT_SMALL_POPUP_PADDING), LayoutScale(LAYOUT_SMALL_POPUP_PADDING), + LayoutScale(500.0f, 194.0f))) { BeginMenuButtons(); @@ -6396,11 +6356,11 @@ void FullscreenUI::DrawPatchesOrCheatsSettingsPage(bool cheats) if (MenuButtonWithoutSummary(FSUI_CSTR("OK"), true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, UIStyle.LargeFont, ImVec2(0.5f, 0.0f))) { - ImGui::CloseCurrentPopup(); + CloseFixedPopupDialog(); } EndMenuButtons(); - EndFixedPopupModal(); + EndFixedPopupDialog(); } } else @@ -6776,7 +6736,7 @@ bool FullscreenUI::InitializeSaveStateListEntryFromSerial(SaveStateListEntry* li { const std::string path = (global ? System::GetGlobalSaveStateFileName(slot) : System::GetGameSaveStateFileName(serial, slot)); - if (!InitializeSaveStateListEntryFromPath(li, path.c_str(), slot, global)) + if (!InitializeSaveStateListEntryFromPath(li, path.c_str(), slot, global, nullptr)) { InitializePlaceholderSaveStateListEntry(li, slot, global); return false; @@ -6785,7 +6745,8 @@ bool FullscreenUI::InitializeSaveStateListEntryFromSerial(SaveStateListEntry* li return true; } -bool FullscreenUI::InitializeSaveStateListEntryFromPath(SaveStateListEntry* li, std::string path, s32 slot, bool global) +bool FullscreenUI::InitializeSaveStateListEntryFromPath(SaveStateListEntry* li, std::string path, s32 slot, bool global, + std::string* media_path) { std::optional ssi(System::GetExtendedSaveStateInfo(path.c_str())); if (!ssi.has_value()) @@ -6807,6 +6768,8 @@ bool FullscreenUI::InitializeSaveStateListEntryFromPath(SaveStateListEntry* li, li->global = global; if (ssi->screenshot.IsValid()) li->preview_texture = g_gpu_device->FetchAndUploadTextureImage(ssi->screenshot); + if (media_path) + *media_path = std::move(ssi->media_path); return true; } @@ -6865,7 +6828,6 @@ void FullscreenUI::OpenSaveStateSelector(const std::string& serial, const std::s std::optional undo_state = System::GetUndoSaveStateInfo(); GPUThread::RunOnThread([serial = std::move(serial), undo_state = std::move(undo_state), is_loading]() mutable { s_state.save_state_selector_loading = is_loading; - s_state.save_state_selector_resuming = false; s_state.save_state_selector_game_path = {}; if (PopulateSaveStateListEntries(serial, std::move(undo_state)) > 0) { @@ -6883,7 +6845,6 @@ void FullscreenUI::OpenSaveStateSelector(const std::string& serial, const std::s else { s_state.save_state_selector_loading = is_loading; - s_state.save_state_selector_resuming = false; if (PopulateSaveStateListEntries(serial, std::nullopt) > 0) { s_state.save_state_selector_game_path = path; @@ -6905,8 +6866,10 @@ void FullscreenUI::CloseSaveStateSelector() ClearSaveStateEntryList(); s_state.save_state_selector_open = false; s_state.save_state_selector_loading = false; - s_state.save_state_selector_resuming = false; s_state.save_state_selector_game_path = {}; + + if (IsFixedPopupDialogOpen(RESUME_STATE_SELECTOR_DIALOG_NAME)) + CloseFixedPopupDialogImmediately(); } void FullscreenUI::DrawSaveStateSelector(bool is_loading) @@ -7240,101 +7203,79 @@ bool FullscreenUI::OpenLoadStateSelectorForGameResume(const GameList::Entry* ent CloseSaveStateSelector(); s_state.save_state_selector_slots.push_back(std::move(slentry)); s_state.save_state_selector_game_path = entry->path; - s_state.save_state_selector_loading = true; - s_state.save_state_selector_open = true; - s_state.save_state_selector_resuming = true; - QueueResetFocus(FocusResetType::PopupOpened); + OpenFixedPopupDialog(RESUME_STATE_SELECTOR_DIALOG_NAME); return true; } void FullscreenUI::DrawResumeStateSelector() { - ImGui::SetNextWindowSize(LayoutScale(820.0f, 625.0f)); - ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f)); - ImGui::OpenPopup(FSUI_CSTR("Load Resume State")); - - ImGui::PushFont(UIStyle.LargeFont); - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(40.0f)); - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(30.0f, 30.0f)); - - bool is_open = true; - if (ImGui::BeginPopupModal(FSUI_CSTR("Load Resume State"), &is_open, - ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize)) + if (!BeginFixedPopupDialog(LayoutScale(30.0f), LayoutScale(40.0f), LayoutScale(820.0f, 625.0f))) { - SaveStateListEntry& entry = s_state.save_state_selector_slots.front(); - SmallString time; - TimeToPrintableString(&time, entry.timestamp); - ImGui::TextWrapped( - FSUI_CSTR("A resume save state created at %s was found.\n\nDo you want to load this save and continue?"), - time.c_str()); - - const GPUTexture* image = entry.preview_texture ? entry.preview_texture.get() : GetPlaceholderTexture().get(); - const float image_height = LayoutScale(250.0f); - const float image_width = - image_height * (static_cast(image->GetWidth()) / static_cast(image->GetHeight())); - const ImVec2 pos(ImGui::GetCursorScreenPos() + - ImVec2((ImGui::GetCurrentWindow()->WorkRect.GetWidth() - image_width) * 0.5f, LayoutScale(20.0f))); - const ImRect image_bb(pos, pos + ImVec2(image_width, image_height)); - ImGui::GetWindowDrawList()->AddImage( - static_cast(entry.preview_texture ? entry.preview_texture.get() : GetPlaceholderTexture().get()), - image_bb.Min, image_bb.Max); - - ImGui::SetCursorPosY(ImGui::GetCursorPosY() + image_height + LayoutScale(40.0f)); - - ResetFocusHere(); - BeginMenuButtons(); - - if (MenuButtonWithoutSummary(FSUI_ICONSTR(ICON_FA_PLAY, "Load State"))) - { - DoStartPath(s_state.save_state_selector_game_path, std::move(entry.path)); - is_open = false; - } - - if (MenuButtonWithoutSummary(FSUI_ICONSTR(ICON_FA_LIGHTBULB, "Clean Boot"))) - { - DoStartPath(s_state.save_state_selector_game_path); - is_open = false; - } - - if (MenuButtonWithoutSummary(FSUI_ICONSTR(ICON_FA_FOLDER_MINUS, "Delete State"))) - { - if (FileSystem::DeleteFile(entry.path.c_str())) - { - DoStartPath(s_state.save_state_selector_game_path); - is_open = false; - } - else - { - ShowToast(std::string(), FSUI_STR("Failed to delete save state.")); - } - } - - if (MenuButtonWithoutSummary(FSUI_ICONSTR(ICON_FA_WINDOW_CLOSE, "Cancel")) || WantsToCloseMenu()) - { - ImGui::CloseCurrentPopup(); - is_open = false; - } - EndMenuButtons(); - - ImGui::EndPopup(); + CloseSaveStateSelector(); + return; } - ImGui::PopStyleVar(2); - ImGui::PopFont(); + SaveStateListEntry& entry = s_state.save_state_selector_slots.front(); + SmallString time; + TimeToPrintableString(&time, entry.timestamp); + ImGui::TextWrapped( + FSUI_CSTR("A resume save state created at %s was found.\n\nDo you want to load this save and continue?"), + time.c_str()); - if (!is_open) + const GPUTexture* image = entry.preview_texture ? entry.preview_texture.get() : GetPlaceholderTexture().get(); + const float image_height = LayoutScale(250.0f); + const float image_width = + image_height * (static_cast(image->GetWidth()) / static_cast(image->GetHeight())); + const ImVec2 pos(ImGui::GetCursorScreenPos() + + ImVec2((ImGui::GetCurrentWindow()->WorkRect.GetWidth() - image_width) * 0.5f, LayoutScale(20.0f))); + const ImRect image_bb(pos, pos + ImVec2(image_width, image_height)); + ImGui::GetWindowDrawList()->AddImage( + static_cast(entry.preview_texture ? entry.preview_texture.get() : GetPlaceholderTexture().get()), + image_bb.Min, image_bb.Max, ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f), + ImGui::GetColorU32(IM_COL32(255, 255, 255, 255))); + + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + image_height + LayoutScale(40.0f)); + + ResetFocusHere(); + BeginMenuButtons(); + + if (MenuButtonWithoutSummary(FSUI_ICONSTR(ICON_FA_PLAY, "Load State"))) { - ClearSaveStateEntryList(); - QueueResetFocus(FocusResetType::PopupClosed); - s_state.save_state_selector_open = false; - s_state.save_state_selector_loading = false; - s_state.save_state_selector_resuming = false; - s_state.save_state_selector_game_path = {}; + std::string game_path = std::move(s_state.save_state_selector_game_path); + std::string state_path = std::move(entry.path); + CloseSaveStateSelector(); + DoStartPath(std::move(game_path), std::move(state_path)); } - else + + if (MenuButtonWithoutSummary(FSUI_ICONSTR(ICON_FA_LIGHTBULB, "Clean Boot"))) { - SetStandardSelectionFooterText(false); + std::string game_path = std::move(s_state.save_state_selector_game_path); + CloseSaveStateSelector(); + DoStartPath(std::move(game_path)); } + + if (MenuButtonWithoutSummary(FSUI_ICONSTR(ICON_FA_FOLDER_MINUS, "Delete State"))) + { + if (FileSystem::DeleteFile(entry.path.c_str())) + { + std::string game_path = std::move(s_state.save_state_selector_game_path); + CloseSaveStateSelector(); + DoStartPath(std::move(game_path)); + } + else + { + ShowToast(std::string(), FSUI_STR("Failed to delete save state.")); + } + } + + if (MenuButtonWithoutSummary(FSUI_ICONSTR(ICON_FA_WINDOW_CLOSE, "Cancel"))) + CloseFixedPopupDialog(); + + EndMenuButtons(); + + SetStandardSelectionFooterText(false); + + EndFixedPopupDialog(); } void FullscreenUI::DoLoadState(std::string path) @@ -8409,11 +8350,6 @@ GPUTexture* FullscreenUI::GetCoverForCurrentGame() // Overlays ////////////////////////////////////////////////////////////////////////// -void FullscreenUI::OpenAboutWindow() -{ - s_state.about_window_open = true; -} - void FullscreenUI::ExitFullscreenAndOpenURL(std::string_view url) { Host::RunOnCPUThread([url = std::string(url)]() { @@ -8434,64 +8370,52 @@ void FullscreenUI::CopyTextToClipboard(std::string title, std::string_view text) void FullscreenUI::DrawAboutWindow() { - ImGui::SetNextWindowSize(LayoutScale(1020.0f, 590.0f)); - ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f)); - ImGui::OpenPopup(FSUI_CSTR("About DuckStation")); - - ImGui::PushFont(UIStyle.LargeFont); - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(40.0f)); - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(30.0f, 30.0f)); - - if (ImGui::BeginPopupModal(FSUI_CSTR("About DuckStation"), &s_state.about_window_open, - ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize)) + if (!BeginFixedPopupDialog(LayoutScale(LAYOUT_LARGE_POPUP_PADDING), LayoutScale(LAYOUT_LARGE_POPUP_ROUNDING), + LayoutScale(1020.0f, 590.0f))) { - const ImVec2 image_size = LayoutScale(64.0f, 64.0f); - const float indent = image_size.x + LayoutScale(8.0f); - ImGui::GetWindowDrawList()->AddImage(s_state.app_icon_texture.get(), ImGui::GetCursorScreenPos(), - ImGui::GetCursorScreenPos() + image_size); - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + indent); - ImGui::TextUnformatted("DuckStation"); - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + indent); - ImGui::TextUnformatted(g_scm_tag_str); - - ImGui::NewLine(); - ImGui::TextWrapped("%s", FSUI_CSTR("DuckStation is a free simulator/emulator of the Sony PlayStation(TM) " - "console, focusing on playability, speed, and long-term maintainability.")); - ImGui::NewLine(); - ImGui::TextWrapped( - "%s", FSUI_CSTR("Duck icon by icons8 (https://icons8.com/icon/74847/platforms.undefined.short-title)")); - ImGui::NewLine(); - ImGui::TextWrapped( - "%s", FSUI_CSTR("\"PlayStation\" and \"PSX\" are registered trademarks of Sony Interactive Entertainment Europe " - "Limited. This software is not affiliated in any way with Sony Interactive Entertainment.")); - - ImGui::NewLine(); - - BeginMenuButtons(); - if (MenuButtonWithoutSummary(FSUI_ICONSTR(ICON_FA_GLOBE, "GitHub Repository"))) - ExitFullscreenAndOpenURL("https://github.com/stenzek/duckstation/"); - if (MenuButtonWithoutSummary(FSUI_ICONSTR(ICON_FA_COMMENT, "Discord Server"))) - ExitFullscreenAndOpenURL("https://www.duckstation.org/discord.html"); - if (MenuButtonWithoutSummary(FSUI_ICONSTR(ICON_FA_PEOPLE_CARRY, "Contributor List"))) - ExitFullscreenAndOpenURL("https://github.com/stenzek/duckstation/blob/master/CONTRIBUTORS.md"); - - if (MenuButtonWithoutSummary(FSUI_ICONSTR(ICON_FA_WINDOW_CLOSE, "Close")) || WantsToCloseMenu()) - { - ImGui::CloseCurrentPopup(); - s_state.about_window_open = false; - } - else - { - SetStandardSelectionFooterText(true); - } - - EndMenuButtons(); - - ImGui::EndPopup(); + return; } - ImGui::PopStyleVar(2); - ImGui::PopFont(); + const ImVec2 image_size = LayoutScale(64.0f, 64.0f); + const float indent = image_size.x + LayoutScale(8.0f); + ImGui::GetWindowDrawList()->AddImage(s_state.app_icon_texture.get(), ImGui::GetCursorScreenPos(), + ImGui::GetCursorScreenPos() + image_size); + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + indent); + ImGui::TextUnformatted("DuckStation"); + ImGui::PushStyleColor(ImGuiCol_Text, DarkerColor(UIStyle.BackgroundTextColor)); + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + indent); + ImGui::TextUnformatted(g_scm_tag_str); + ImGui::PopStyleColor(); + + ImGui::NewLine(); + ImGui::TextWrapped("%s", FSUI_CSTR("DuckStation is a free simulator/emulator of the Sony PlayStation(TM) " + "console, focusing on playability, speed, and long-term maintainability.")); + ImGui::NewLine(); + ImGui::TextWrapped("%s", + FSUI_CSTR("Duck icon by icons8 (https://icons8.com/icon/74847/platforms.undefined.short-title)")); + ImGui::NewLine(); + ImGui::TextWrapped( + "%s", FSUI_CSTR("\"PlayStation\" and \"PSX\" are registered trademarks of Sony Interactive Entertainment Europe " + "Limited. This software is not affiliated in any way with Sony Interactive Entertainment.")); + + ImGui::NewLine(); + + BeginMenuButtons(); + if (MenuButtonWithoutSummary(FSUI_ICONSTR(ICON_FA_GLOBE, "GitHub Repository"))) + ExitFullscreenAndOpenURL("https://github.com/stenzek/duckstation/"); + if (MenuButtonWithoutSummary(FSUI_ICONSTR(ICON_FA_COMMENT, "Discord Server"))) + ExitFullscreenAndOpenURL("https://www.duckstation.org/discord.html"); + if (MenuButtonWithoutSummary(FSUI_ICONSTR(ICON_FA_PEOPLE_CARRY, "Contributor List"))) + ExitFullscreenAndOpenURL("https://github.com/stenzek/duckstation/blob/master/CONTRIBUTORS.md"); + + if (MenuButtonWithoutSummary(FSUI_ICONSTR(ICON_FA_WINDOW_CLOSE, "Close")) || WantsToCloseMenu()) + CloseFixedPopupDialog(); + else + SetStandardSelectionFooterText(true); + + EndMenuButtons(); + + EndFixedPopupDialog(); } void FullscreenUI::OpenAchievementsWindow() diff --git a/src/util/imgui_fullscreen.cpp b/src/util/imgui_fullscreen.cpp index cab9de9c0..6be60f1e9 100644 --- a/src/util/imgui_fullscreen.cpp +++ b/src/util/imgui_fullscreen.cpp @@ -38,7 +38,6 @@ LOG_CHANNEL(ImGuiFullscreen); namespace ImGuiFullscreen { -using MessageDialogCallbackVariant = std::variant; static constexpr float MENU_BACKGROUND_ANIMATION_TIME = 0.5f; static constexpr float MENU_ITEM_BORDER_ROUNDING = 10.0f; @@ -47,12 +46,6 @@ static constexpr float SMOOTH_SCROLLING_SPEED = 3.5f; static std::optional LoadTextureImage(std::string_view path, u32 svg_width, u32 svg_height); static std::shared_ptr UploadTexture(std::string_view path, const Image& image); -static void PushPopupStyle(float window_padding = 20.0f); -static void PopPopupStyle(); -static void DrawFileSelector(); -static void DrawChoiceDialog(); -static void DrawInputDialog(); -static void DrawMessageDialog(); static void DrawBackgroundProgressDialogs(ImVec2& position, float spacing); static void DrawLoadingScreen(std::string_view image, std::string_view message, s32 progress_min, s32 progress_max, s32 progress_value, bool is_persistent); @@ -60,8 +53,6 @@ static void DrawNotifications(ImVec2& position, float spacing); static void DrawToast(); static bool MenuButtonFrame(const char* str_id, bool enabled, float height, bool* visible, bool* hovered, ImRect* bb, ImGuiButtonFlags flags = 0, float hover_alpha = 1.0f); -static void PopulateFileSelectorItems(); -static void SetFileSelectorDirectory(std::string dir); static ImGuiID GetBackgroundProgressID(const char* str_id); namespace { @@ -76,25 +67,6 @@ enum class CloseButtonState Cancelled, }; -struct FileSelectorItem -{ - FileSelectorItem() = default; - FileSelectorItem(std::string display_name_, std::string full_path_, bool is_file_) - : display_name(std::move(display_name_)), full_path(std::move(full_path_)), is_file(is_file_) - { - } - FileSelectorItem(const FileSelectorItem&) = default; - FileSelectorItem(FileSelectorItem&&) = default; - ~FileSelectorItem() = default; - - FileSelectorItem& operator=(const FileSelectorItem&) = default; - FileSelectorItem& operator=(FileSelectorItem&&) = default; - - std::string display_name; - std::string full_path; - bool is_file; -}; - struct Notification { std::string key; @@ -117,6 +89,119 @@ struct BackgroundProgressDialogData s32 value; }; +class MessageDialog : public PopupDialog +{ +public: + using CallbackVariant = std::variant; + + MessageDialog(); + ~MessageDialog(); + + void Open(std::string_view title, std::string message, CallbackVariant callback, std::string first_button_text, + std::string second_button_text, std::string third_button_text); + void ClearState(); + + void Draw(); + +private: + static void InvokeCallback(const CallbackVariant& cb, std::optional choice); + + std::string m_message; + std::array m_buttons; + CallbackVariant m_callback; +}; + +class ChoiceDialog : public PopupDialog +{ +public: + ChoiceDialog(); + ~ChoiceDialog(); + + void Open(std::string_view title, ChoiceDialogOptions options, ChoiceDialogCallback callback, bool checkable); + void ClearState(); + + void Draw(); + +private: + ChoiceDialogOptions m_options; + ChoiceDialogCallback m_callback; + bool m_checkable = false; +}; + +class FileSelectorDialog : public PopupDialog +{ +public: + FileSelectorDialog(); + ~FileSelectorDialog(); + + void Open(std::string_view title, FileSelectorCallback callback, FileSelectorFilters filters, + std::string initial_directory, bool select_directory); + void ClearState(); + + void Draw(); + +private: + struct Item + { + Item() = default; + Item(std::string display_name_, std::string full_path_, bool is_file_); + Item(const Item&) = default; + Item(Item&&) = default; + ~Item() = default; + + Item& operator=(const Item&) = default; + Item& operator=(Item&&) = default; + + std::string display_name; + std::string full_path; + bool is_file; + }; + + void PopulateItems(); + void SetDirectory(std::string dir); + + std::string m_current_directory; + std::vector m_items; + std::vector m_filters; + FileSelectorCallback m_callback; + + bool m_is_directory = false; + bool m_directory_changed = false; +}; + +class InputStringDialog : public PopupDialog +{ +public: + InputStringDialog(); + ~InputStringDialog(); + + void Open(std::string_view title, std::string message, std::string caption, std::string ok_button_text, + InputStringDialogCallback callback); + void ClearState(); + + void Draw(); + +private: + std::string m_message; + std::string m_caption; + std::string m_text; + std::string m_ok_text; + InputStringDialogCallback m_callback; +}; + +class FixedPopupDialog : public PopupDialog +{ +public: + FixedPopupDialog(); + ~FixedPopupDialog(); + + void Open(std::string title); + + bool Begin(float scaled_window_padding = LayoutScale(20.0f), float scaled_window_rounding = LayoutScale(20.0f), + const ImVec2& scaled_window_size = ImVec2(0.0f, 0.0f)); + void End(); +}; + struct ALIGN_TO_CACHE_LINE UIState { std::recursive_mutex shared_state_mutex; @@ -140,28 +225,15 @@ struct ALIGN_TO_CACHE_LINE UIState float fullscreen_text_change_time; float fullscreen_text_alpha; - std::string choice_dialog_title; - ChoiceDialogOptions choice_dialog_options; - ChoiceDialogCallback choice_dialog_callback; ImGuiID enum_choice_button_id = 0; s32 enum_choice_button_value = 0; bool enum_choice_button_set = false; - bool choice_dialog_open = false; - bool choice_dialog_checkable = false; - bool input_dialog_open = false; - std::string input_dialog_title; - std::string input_dialog_message; - std::string input_dialog_caption; - std::string input_dialog_text; - std::string input_dialog_ok_text; - InputStringDialogCallback input_dialog_callback; - - bool message_dialog_open = false; - std::string message_dialog_title; - std::string message_dialog_message; - std::array message_dialog_buttons; - MessageDialogCallbackVariant message_dialog_callback; + MessageDialog message_dialog; + ChoiceDialog choice_dialog; + FileSelectorDialog file_selector_dialog; + InputStringDialog input_string_dialog; + FixedPopupDialog fixed_popup_dialog; ImAnimatedVec2 menu_button_frame_min_animated; ImAnimatedVec2 menu_button_frame_max_animated; @@ -169,15 +241,6 @@ struct ALIGN_TO_CACHE_LINE UIState bool has_hovered_menu_item = false; bool rendered_menu_item_border = false; - bool file_selector_open = false; - bool file_selector_directory = false; - bool file_selector_directory_changed = false; - std::string file_selector_title; - ImGuiFullscreen::FileSelectorCallback file_selector_callback; - std::string file_selector_current_directory; - std::vector file_selector_filters; - std::vector file_selector_items; - std::vector notifications; std::string toast_title; @@ -248,28 +311,10 @@ void ImGuiFullscreen::Shutdown(bool clear_state) s_state.left_fullscreen_footer_text.clear(); s_state.last_left_fullscreen_footer_text.clear(); s_state.fullscreen_text_change_time = 0.0f; - CloseInputDialog(); - CloseMessageDialog(); - s_state.choice_dialog_open = false; - s_state.choice_dialog_checkable = false; - s_state.choice_dialog_title = {}; - s_state.choice_dialog_options.clear(); - s_state.choice_dialog_callback = {}; - s_state.enum_choice_button_id = 0; - s_state.enum_choice_button_value = 0; - s_state.enum_choice_button_set = false; - s_state.file_selector_open = false; - s_state.file_selector_directory = false; - s_state.file_selector_title = {}; - s_state.file_selector_callback = {}; - s_state.file_selector_current_directory = {}; - s_state.file_selector_filters.clear(); - s_state.file_selector_items.clear(); - s_state.message_dialog_open = false; - s_state.message_dialog_title = {}; - s_state.message_dialog_message = {}; - s_state.message_dialog_buttons = {}; - s_state.message_dialog_callback = {}; + s_state.input_string_dialog.ClearState(); + s_state.message_dialog.ClearState(); + s_state.choice_dialog.ClearState(); + s_state.file_selector_dialog.ClearState(); } } @@ -642,10 +687,10 @@ void ImGuiFullscreen::BeginLayout() void ImGuiFullscreen::EndLayout() { - DrawFileSelector(); - DrawChoiceDialog(); - DrawInputDialog(); - DrawMessageDialog(); + s_state.message_dialog.Draw(); + s_state.choice_dialog.Draw(); + s_state.file_selector_dialog.Draw(); + s_state.input_string_dialog.Draw(); DrawFullscreenFooter(); @@ -658,59 +703,76 @@ void ImGuiFullscreen::EndLayout() s_state.had_hovered_menu_item = std::exchange(s_state.has_hovered_menu_item, false); } -void ImGuiFullscreen::PushPopupStyle(float window_padding) +ImGuiFullscreen::FixedPopupDialog::FixedPopupDialog() = default; + +ImGuiFullscreen::FixedPopupDialog::~FixedPopupDialog() = default; + +void ImGuiFullscreen::FixedPopupDialog::Open(std::string title) { - ImGui::PushFont(UIStyle.LargeFont); - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(20.0f)); - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(window_padding, window_padding)); - ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarRounding, LayoutScale(10.0f)); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, - LayoutScale(LAYOUT_MENU_BUTTON_X_PADDING, LAYOUT_MENU_BUTTON_Y_PADDING)); - ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); - ImGui::PushStyleColor(ImGuiCol_PopupBg, UIStyle.PopupBackgroundColor); - ImGui::PushStyleColor(ImGuiCol_FrameBg, UIStyle.PopupFrameBackgroundColor); - ImGui::PushStyleColor(ImGuiCol_TitleBg, UIStyle.PrimaryDarkColor); - ImGui::PushStyleColor(ImGuiCol_TitleBgActive, UIStyle.PrimaryColor); - ImGui::PushStyleColor(ImGuiCol_Text, UIStyle.PrimaryTextColor); + SetTitleAndOpen(std::move(title)); + + // fixed dialogs are locked to the parent scope + if (m_state == State::OpeningTrigger) + { + ImGui::OpenPopup(m_title.c_str()); + m_state = State::Opening; + } } -bool ImGuiFullscreen::BeginFixedPopupModal(const char* name, bool* p_open) +bool ImGuiFullscreen::FixedPopupDialog::Begin(float scaled_window_padding /* = LayoutScale(20.0f) */, + float scaled_window_rounding /* = LayoutScale(20.0f) */, + const ImVec2& scaled_window_size /* = ImVec2(0.0f, 0.0f) */) { - PushPopupStyle(); + return BeginRender(scaled_window_padding, scaled_window_rounding, scaled_window_size); +} - ImGui::SetNextWindowPos((ImGui::GetIO().DisplaySize - LayoutScale(0.0f, LAYOUT_FOOTER_HEIGHT)) * 0.5f, - ImGuiCond_Always, ImVec2(0.5f, 0.5f)); +void ImGuiFullscreen::FixedPopupDialog::End() +{ + EndRender(); +} - if (!ImGui::BeginPopupModal(name, p_open, - ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove)) +bool ImGuiFullscreen::IsAnyFixedPopupDialogOpen() +{ + return s_state.fixed_popup_dialog.IsOpen(); +} + +bool ImGuiFullscreen::IsFixedPopupDialogOpen(std::string_view name) +{ + return (s_state.fixed_popup_dialog.GetTitle() == name); +} + +void ImGuiFullscreen::OpenFixedPopupDialog(std::string_view name) +{ + // TODO: suffix? + s_state.fixed_popup_dialog.Open(std::string(name)); +} + +void ImGuiFullscreen::CloseFixedPopupDialog() +{ + s_state.fixed_popup_dialog.StartClose(); +} + +void ImGuiFullscreen::CloseFixedPopupDialogImmediately() +{ + s_state.fixed_popup_dialog.CloseImmediately(); +} + +bool ImGuiFullscreen::BeginFixedPopupDialog(float scaled_window_padding /* = LayoutScale(20.0f) */, + float scaled_window_rounding /* = LayoutScale(20.0f) */, + const ImVec2& scaled_window_size /* = ImVec2(0.0f, 0.0f) */) +{ + if (!s_state.fixed_popup_dialog.Begin(scaled_window_padding, scaled_window_rounding, scaled_window_size)) { - PopPopupStyle(); + s_state.fixed_popup_dialog.ClearState(); return false; } - if (p_open && *p_open && WantsToCloseMenu()) - { - *p_open = false; - ImGui::CloseCurrentPopup(); - } - - // don't draw unreadable text - ImGui::PushStyleColor(ImGuiCol_Text, UIStyle.BackgroundTextColor); return true; } -void ImGuiFullscreen::PopPopupStyle() +void ImGuiFullscreen::EndFixedPopupDialog() { - ImGui::PopStyleColor(5); - ImGui::PopStyleVar(5); - ImGui::PopFont(); -} - -void ImGuiFullscreen::EndFixedPopupModal() -{ - ImGui::PopStyleColor(); - ImGui::EndPopup(); - PopPopupStyle(); + s_state.fixed_popup_dialog.End(); } void ImGuiFullscreen::RenderOverlays() @@ -754,11 +816,12 @@ void ImGuiFullscreen::PushResetLayout() ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabHovered, UIStyle.PrimaryLightColor); ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabActive, UIStyle.PrimaryDarkColor); ImGui::PushStyleColor(ImGuiCol_PopupBg, UIStyle.PopupBackgroundColor); + ImGui::PushStyleColor(ImGuiCol_ModalWindowDimBg, ImVec4(0.0f, 0.0f, 0.0f, 0.5f)); } void ImGuiFullscreen::PopResetLayout() { - ImGui::PopStyleColor(11); + ImGui::PopStyleColor(12); ImGui::PopStyleVar(13); } @@ -811,6 +874,7 @@ bool ImGuiFullscreen::IsFocusResetQueued() bool ImGuiFullscreen::IsFocusResetFromWindowChange() { return (s_state.focus_reset_queued != FocusResetType::None && + s_state.focus_reset_queued != FocusResetType::PopupOpened && s_state.focus_reset_queued != FocusResetType::PopupClosed); } @@ -1852,13 +1916,13 @@ bool ImGuiFullscreen::RangeButton(const char* title, const char* summary, s32* v } if (pressed) - ImGui::OpenPopup(title); + OpenFixedPopupDialog(title); bool changed = false; - ImGui::SetNextWindowSize(LayoutScale(500.0f, 192.0f)); - - if (BeginFixedPopupModal(title, nullptr)) + if (IsFixedPopupDialogOpen(title) && + BeginFixedPopupDialog(LayoutScale(LAYOUT_SMALL_POPUP_PADDING), LayoutScale(LAYOUT_SMALL_POPUP_PADDING), + LayoutScale(500.0f, 194.0f))) { BeginMenuButtons(); @@ -1871,11 +1935,12 @@ bool ImGuiFullscreen::RangeButton(const char* title, const char* summary, s32* v if (MenuButtonWithoutSummary(ok_text, true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, UIStyle.LargeFont, ImVec2(0.5f, 0.0f))) { - ImGui::CloseCurrentPopup(); + CloseFixedPopupDialog(); } + EndMenuButtons(); - EndFixedPopupModal(); + EndFixedPopupDialog(); } return changed; @@ -1913,13 +1978,13 @@ bool ImGuiFullscreen::RangeButton(const char* title, const char* summary, float* } if (pressed) - ImGui::OpenPopup(title); + OpenFixedPopupDialog(title); bool changed = false; - ImGui::SetNextWindowSize(LayoutScale(500.0f, 192.0f)); - - if (BeginFixedPopupModal(title, nullptr)) + if (IsFixedPopupDialogOpen(title) && + BeginFixedPopupDialog(LayoutScale(LAYOUT_SMALL_POPUP_PADDING), LayoutScale(LAYOUT_SMALL_POPUP_PADDING), + LayoutScale(500.0f, 194.0f))) { BeginMenuButtons(); @@ -1930,10 +1995,13 @@ bool ImGuiFullscreen::RangeButton(const char* title, const char* summary, float* if (MenuButtonWithoutSummary(ok_text, true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, UIStyle.LargeFont, ImVec2(0.5f, 0.0f))) - ImGui::CloseCurrentPopup(); + { + CloseFixedPopupDialog(); + } + EndMenuButtons(); - EndFixedPopupModal(); + EndFixedPopupDialog(); } return changed; @@ -2312,75 +2380,188 @@ bool ImGuiFullscreen::HorizontalMenuItem(GPUTexture* icon, const char* title, co return pressed; } -void ImGuiFullscreen::PopulateFileSelectorItems() -{ - s_state.file_selector_items.clear(); +ImGuiFullscreen::PopupDialog::PopupDialog() = default; - if (s_state.file_selector_current_directory.empty()) +ImGuiFullscreen::PopupDialog::~PopupDialog() = default; + +void ImGuiFullscreen::PopupDialog::StartClose() +{ + if (!IsOpen() || m_state == State::Closing || m_state == State::ClosingTrigger) + return; + + m_state = State::Closing; + m_animation_time_remaining = CLOSE_TIME; +} + +void ImGuiFullscreen::PopupDialog::ClearState() +{ + m_state = State::Inactive; + m_title = {}; +} + +void ImGuiFullscreen::PopupDialog::SetTitleAndOpen(std::string title) +{ + DebugAssert(!title.empty()); + m_title = std::move(title); + + if (m_state == State::Inactive) { - for (std::string& root_path : FileSystem::GetRootDirectoryList()) - s_state.file_selector_items.emplace_back(fmt::format(ICON_FA_FOLDER " {}", root_path), std::move(root_path), - false); + // inactive -> active + m_state = State::OpeningTrigger; + m_animation_time_remaining = OPEN_TIME; + QueueResetFocus(FocusResetType::PopupOpened); + } + else if (m_state == State::Closing) + { + // cancel close + m_state = State::Opening; + } + else if (m_state == State::ClosingTrigger) + { + // prevent from closing + m_state = State::Open; + } +} + +void ImGuiFullscreen::PopupDialog::CloseImmediately() +{ + if (!IsOpen()) + return; + + if (!GImGui->BeginPopupStack.empty() && GImGui->BeginPopupStack.front().Window == ImGui::GetCurrentWindowRead()) + { + ImGui::CloseCurrentPopup(); + ClearState(); } else { - FileSystem::FindResultsArray results; - FileSystem::FindFiles(s_state.file_selector_current_directory.c_str(), "*", - FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_FOLDERS | FILESYSTEM_FIND_HIDDEN_FILES | - FILESYSTEM_FIND_RELATIVE_PATHS | FILESYSTEM_FIND_SORT_BY_NAME, - &results); - - std::string parent_path; - std::string::size_type sep_pos = s_state.file_selector_current_directory.rfind(FS_OSPATH_SEPARATOR_CHARACTER); - if (sep_pos != std::string::npos) - parent_path = Path::Canonicalize(s_state.file_selector_current_directory.substr(0, sep_pos)); - - s_state.file_selector_items.emplace_back(ICON_FA_FOLDER_OPEN " ", std::move(parent_path), false); - - for (const FILESYSTEM_FIND_DATA& fd : results) - { - std::string full_path = - fmt::format("{}" FS_OSPATH_SEPARATOR_STR "{}", s_state.file_selector_current_directory, fd.FileName); - - if (fd.Attributes & FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY) - { - std::string title = fmt::format(ICON_FA_FOLDER " {}", fd.FileName); - s_state.file_selector_items.emplace_back(std::move(title), std::move(full_path), false); - } - else - { - if (s_state.file_selector_filters.empty() || - std::none_of(s_state.file_selector_filters.begin(), s_state.file_selector_filters.end(), - [&fd](const std::string& filter) { - return StringUtil::WildcardMatch(fd.FileName.c_str(), filter.c_str(), false); - })) - { - continue; - } - - std::string title = fmt::format(ICON_FA_FILE " {}", fd.FileName); - s_state.file_selector_items.emplace_back(std::move(title), std::move(full_path), true); - } - } + // have to defer it + m_state = State::ClosingTrigger; } } -void ImGuiFullscreen::SetFileSelectorDirectory(std::string dir) +bool ImGuiFullscreen::PopupDialog::BeginRender(float scaled_window_padding /* = LayoutScale(20.0f) */, + float scaled_window_rounding /* = LayoutScale(20.0f) */, + const ImVec2& scaled_window_size /* = ImVec2(0.0f, 0.0f) */) { - while (!dir.empty() && dir.back() == FS_OSPATH_SEPARATOR_CHARACTER) - dir.erase(dir.size() - 1); + DebugAssert(IsOpen()); - s_state.file_selector_current_directory = std::move(dir); - PopulateFileSelectorItems(); + // check for animation completion + ImVec2 pos_offset = ImVec2(0.0f, 0.0f); + float alpha = 1.0f; + if (m_state >= State::OpeningTrigger) + { + m_animation_time_remaining -= ImGui::GetIO().DeltaTime; + if (m_animation_time_remaining <= 0.0f) + { + m_animation_time_remaining = 0.0f; + if (m_state == State::Opening) + { + m_state = State::Open; + } + else + { + m_state = State::ClosingTrigger; + alpha = 0.0f; + } + } + else + { + if (m_state == State::OpeningTrigger) + { + // need to have the openpopup at the correct level + m_state = State::Opening; + ImGui::OpenPopup(m_title.c_str()); + } + + // inhibit menu animation while opening, otherwise it jitters + ResetMenuButtonFrame(); + + if (m_state == State::Opening) + { + const float fract = m_animation_time_remaining / OPEN_TIME; + alpha = 1.0f - fract; + pos_offset.y = LayoutScale(50.0f) * Easing::InExpo(fract); + } + else + { + const float fract = m_animation_time_remaining / CLOSE_TIME; + alpha = fract; + pos_offset.y = LayoutScale(20.0f) * (1.0f - fract); + } + } + } + + ImGui::PushFont(UIStyle.LargeFont); + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, alpha); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, scaled_window_rounding); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(scaled_window_padding, scaled_window_padding)); + ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarRounding, LayoutScale(10.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, + LayoutScale(LAYOUT_MENU_BUTTON_X_PADDING, LAYOUT_MENU_BUTTON_Y_PADDING)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); + ImGui::PushStyleColor(ImGuiCol_PopupBg, ModAlpha(UIStyle.PopupBackgroundColor, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_FrameBg, UIStyle.PopupFrameBackgroundColor); + ImGui::PushStyleColor(ImGuiCol_TitleBg, UIStyle.PrimaryDarkColor); + ImGui::PushStyleColor(ImGuiCol_TitleBgActive, UIStyle.PrimaryColor); + ImGui::PushStyleColor(ImGuiCol_Text, UIStyle.PrimaryTextColor); + + ImGui::SetNextWindowPos((ImGui::GetIO().DisplaySize - LayoutScale(0.0f, LAYOUT_FOOTER_HEIGHT)) * 0.5f + pos_offset, + ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + ImGui::SetNextWindowSize(scaled_window_size); + + // Based on BeginPopupModal(), because we need to control is_open smooth closing. + const bool popup_open = ImGui::IsPopupOpen( + ImGui::GetCurrentWindowRead()->GetID(m_title.c_str(), m_title.c_str() + m_title.length()), ImGuiPopupFlags_None); + const ImGuiWindowFlags window_flags = ImGuiWindowFlags_Popup | ImGuiWindowFlags_Modal | ImGuiWindowFlags_NoCollapse | + ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | + (m_title.starts_with("##") ? ImGuiWindowFlags_NoTitleBar : 0); + bool is_open = true; + if (!ImGui::Begin(m_title.c_str(), &is_open, window_flags)) + is_open = false; + + if (!is_open && m_state != State::ClosingTrigger) + { + StartClose(); + } + else if (m_state == State::ClosingTrigger) + { + if (popup_open) + ImGui::CloseCurrentPopup(); + + ImGui::EndPopup(); + ImGui::PopStyleColor(5); + ImGui::PopStyleVar(6); + ImGui::PopFont(); + QueueResetFocus(FocusResetType::PopupClosed); + return false; + } + + // don't draw unreadable text + ImGui::PopStyleColor(1); + ImGui::PushStyleColor(ImGuiCol_Text, UIStyle.BackgroundTextColor); + + if (WantsToCloseMenu()) + StartClose(); + + return true; } -bool ImGuiFullscreen::IsFileSelectorOpen() +void ImGuiFullscreen::PopupDialog::EndRender() { - return s_state.file_selector_open; + ImGui::EndPopup(); + ImGui::PopStyleColor(5); + ImGui::PopStyleVar(6); + ImGui::PopFont(); } -void ImGuiFullscreen::OpenFileSelector(std::string_view title, bool select_directory, FileSelectorCallback callback, - FileSelectorFilters filters, std::string initial_directory) +ImGuiFullscreen::FileSelectorDialog::FileSelectorDialog() = default; + +ImGuiFullscreen::FileSelectorDialog::~FileSelectorDialog() = default; + +void ImGuiFullscreen::FileSelectorDialog::Open(std::string_view title, FileSelectorCallback callback, + FileSelectorFilters filters, std::string initial_directory, + bool select_directory) { if (initial_directory.empty() || !FileSystem::DirectoryExists(initial_directory.c_str())) initial_directory = FileSystem::GetWorkingDirectory(); @@ -2392,543 +2573,532 @@ void ImGuiFullscreen::OpenFileSelector(std::string_view title, bool select_direc return; } - if (s_state.file_selector_open) - CloseFileSelector(); - - s_state.file_selector_open = true; - s_state.file_selector_directory = select_directory; - s_state.file_selector_directory_changed = true; - s_state.file_selector_title = fmt::format("{}##file_selector", title); - s_state.file_selector_callback = std::move(callback); - s_state.file_selector_filters = std::move(filters); - - SetFileSelectorDirectory(std::move(initial_directory)); + SetTitleAndOpen(fmt::format("{}##file_selector_dialog", title)); + m_callback = std::move(callback); + m_filters = std::move(filters); + m_is_directory = select_directory; + SetDirectory(std::move(initial_directory)); } -void ImGuiFullscreen::CloseFileSelector() +void ImGuiFullscreen::FileSelectorDialog::ClearState() { - if (!s_state.file_selector_open) - return; - - if (ImGui::IsPopupOpen(s_state.file_selector_title.c_str(), 0)) - ImGui::ClosePopupToLevel(GImGui->OpenPopupStack.Size - 1, true); - - s_state.file_selector_open = false; - s_state.file_selector_directory = false; - s_state.file_selector_directory_changed = false; - std::string().swap(s_state.file_selector_title); - FileSelectorCallback().swap(s_state.file_selector_callback); - FileSelectorFilters().swap(s_state.file_selector_filters); - std::string().swap(s_state.file_selector_current_directory); - s_state.file_selector_items.clear(); - ImGui::CloseCurrentPopup(); - QueueResetFocus(FocusResetType::PopupClosed); + PopupDialog::ClearState(); + m_callback = {}; + m_filters = {}; + m_is_directory = false; + m_directory_changed = false; } -void ImGuiFullscreen::DrawFileSelector() +ImGuiFullscreen::FileSelectorDialog::Item::Item(std::string display_name_, std::string full_path_, bool is_file_) + : display_name(std::move(display_name_)), full_path(std::move(full_path_)), is_file(is_file_) { - if (!s_state.file_selector_open) - return; +} - ImGui::SetNextWindowSize(LayoutScale(1000.0f, 650.0f)); - ImGui::OpenPopup(s_state.file_selector_title.c_str()); +void ImGuiFullscreen::FileSelectorDialog::PopulateItems() +{ + m_items.clear(); - FileSelectorItem* selected = nullptr; - - PushPopupStyle(10.0f); - - bool is_open = !WantsToCloseMenu(); - bool directory_selected = false; - if (ImGui::BeginPopupModal(s_state.file_selector_title.c_str(), &is_open, - ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove)) + if (m_current_directory.empty()) { - ImGui::PushStyleColor(ImGuiCol_Text, UIStyle.BackgroundTextColor); - - if (s_state.file_selector_directory_changed) - { - s_state.file_selector_directory_changed = false; - QueueResetFocus(FocusResetType::Other); - } - - ResetFocusHere(); - BeginMenuButtons(); - - if (!s_state.file_selector_current_directory.empty()) - { - MenuButton(SmallString::from_format(ICON_FA_FOLDER_OPEN " {}", s_state.file_selector_current_directory).c_str(), - nullptr, false, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); - } - - if (s_state.file_selector_directory && !s_state.file_selector_current_directory.empty()) - { - if (MenuButton(ICON_FA_FOLDER_PLUS " ", nullptr, true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY)) - directory_selected = true; - } - - for (FileSelectorItem& item : s_state.file_selector_items) - { - if (MenuButton(item.display_name.c_str(), nullptr, true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY)) - selected = &item; - } - - EndMenuButtons(); - - ImGui::PopStyleColor(1); - - ImGui::EndPopup(); + for (std::string& root_path : FileSystem::GetRootDirectoryList()) + m_items.emplace_back(fmt::format(ICON_FA_FOLDER " {}", root_path), std::move(root_path), false); } else { - is_open = false; + FileSystem::FindResultsArray results; + FileSystem::FindFiles(m_current_directory.c_str(), "*", + FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_FOLDERS | FILESYSTEM_FIND_HIDDEN_FILES | + FILESYSTEM_FIND_RELATIVE_PATHS | FILESYSTEM_FIND_SORT_BY_NAME, + &results); + + std::string parent_path; + std::string::size_type sep_pos = m_current_directory.rfind(FS_OSPATH_SEPARATOR_CHARACTER); + if (sep_pos != std::string::npos) + parent_path = Path::Canonicalize(m_current_directory.substr(0, sep_pos)); + + m_items.emplace_back(ICON_FA_FOLDER_OPEN " ", std::move(parent_path), false); + + for (const FILESYSTEM_FIND_DATA& fd : results) + { + std::string full_path = Path::Combine(m_current_directory, fd.FileName); + + if (fd.Attributes & FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY) + { + std::string title = fmt::format(ICON_FA_FOLDER " {}", fd.FileName); + m_items.emplace_back(std::move(title), std::move(full_path), false); + } + else + { + if (m_filters.empty() || std::none_of(m_filters.begin(), m_filters.end(), [&fd](const std::string& filter) { + return StringUtil::WildcardMatch(fd.FileName.c_str(), filter.c_str(), false); + })) + { + continue; + } + + std::string title = fmt::format(ICON_FA_FILE " {}", fd.FileName); + m_items.emplace_back(std::move(title), std::move(full_path), true); + } + } + } +} + +void ImGuiFullscreen::FileSelectorDialog::SetDirectory(std::string dir) +{ + while (!dir.empty() && dir.back() == FS_OSPATH_SEPARATOR_CHARACTER) + dir.pop_back(); + + m_current_directory = std::move(dir); + m_directory_changed = true; + PopulateItems(); +} + +void ImGuiFullscreen::FileSelectorDialog::Draw() +{ + if (!IsOpen()) + return; + + if (!BeginRender(LayoutScale(10.0f), LayoutScale(20.0f), LayoutScale(1000.0f, 650.0f))) + { + const FileSelectorCallback callback = std::move(m_callback); + ClearState(); + if (callback) + callback(std::string()); + return; } - PopPopupStyle(); + if (m_directory_changed) + { + m_directory_changed = false; + ImGui::SetScrollY(0.0f); + QueueResetFocus(FocusResetType::Other); + } - if (is_open) - GetFileSelectorHelpText(s_state.fullscreen_footer_text); + ResetFocusHere(); + BeginMenuButtons(); + + Item* selected = nullptr; + bool directory_selected = false; + + if (!m_current_directory.empty()) + { + MenuButton(SmallString::from_format(ICON_FA_FOLDER_OPEN " {}", m_current_directory).c_str(), nullptr, false, + LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); + } + + if (m_is_directory && !m_current_directory.empty()) + { + if (MenuButton(ICON_FA_FOLDER_PLUS " ", nullptr, true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY)) + directory_selected = true; + } + + for (Item& item : m_items) + { + if (MenuButton(item.display_name.c_str(), nullptr, true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY)) + selected = &item; + } + + EndMenuButtons(); + + GetFileSelectorHelpText(s_state.fullscreen_footer_text); + + EndRender(); if (selected) { if (selected->is_file) { std::string path = std::move(selected->full_path); - const FileSelectorCallback callback = std::move(s_state.file_selector_callback); - CloseFileSelector(); + const FileSelectorCallback callback = std::move(m_callback); + StartClose(); callback(std::move(path)); } else { - SetFileSelectorDirectory(std::move(selected->full_path)); - s_state.file_selector_directory_changed = true; + SetDirectory(std::move(selected->full_path)); } } else if (directory_selected) { - std::string path = std::move(s_state.file_selector_current_directory); - const FileSelectorCallback callback = std::move(s_state.file_selector_callback); - CloseFileSelector(); + std::string path = std::move(m_current_directory); + const FileSelectorCallback callback = std::move(m_callback); + StartClose(); callback(std::move(path)); } - else if (!is_open) - { - const FileSelectorCallback callback = std::move(s_state.file_selector_callback); - CloseFileSelector(); - callback(std::string()); - } else { if (ImGui::IsKeyPressed(ImGuiKey_Backspace, false) || ImGui::IsKeyPressed(ImGuiKey_NavGamepadMenu, false)) { - if (!s_state.file_selector_items.empty() && - s_state.file_selector_items.front().display_name == ICON_FA_FOLDER_OPEN " ") + if (!m_items.empty() && m_items.front().display_name == ICON_FA_FOLDER_OPEN " ") + SetDirectory(std::move(m_items.front().full_path)); + } + } +} + +bool ImGuiFullscreen::IsFileSelectorOpen() +{ + return s_state.file_selector_dialog.IsOpen(); +} + +void ImGuiFullscreen::OpenFileSelector(std::string_view title, bool select_directory, FileSelectorCallback callback, + FileSelectorFilters filters, std::string initial_directory) +{ + s_state.file_selector_dialog.Open(title, std::move(callback), std::move(filters), std::move(initial_directory), + select_directory); +} + +void ImGuiFullscreen::CloseFileSelector() +{ + s_state.file_selector_dialog.StartClose(); +} + +ImGuiFullscreen::ChoiceDialog::ChoiceDialog() = default; + +ImGuiFullscreen::ChoiceDialog::~ChoiceDialog() = default; + +void ImGuiFullscreen::ChoiceDialog::Open(std::string_view title, ChoiceDialogOptions options, + ChoiceDialogCallback callback, bool checkable) +{ + SetTitleAndOpen(fmt::format("{}##choice_dialog", title)); + m_options = std::move(options); + m_callback = std::move(callback); + m_checkable = checkable; +} + +void ImGuiFullscreen::ChoiceDialog::ClearState() +{ + PopupDialog::ClearState(); + m_options = {}; + m_callback = {}; + m_checkable = false; +} + +void ImGuiFullscreen::ChoiceDialog::Draw() +{ + if (!IsOpen()) + return; + + const float width = LayoutScale(600.0f); + const float title_height = + UIStyle.LargeFont->FontSize + (LayoutScale(LAYOUT_MENU_BUTTON_Y_PADDING) * 2.0f) + (LayoutScale(10.0f) * 2.0f); + const float item_height = + (LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY) + LayoutScale(LAYOUT_MENU_BUTTON_Y_PADDING) * 2.0f); + const float height = title_height + (item_height * static_cast(std::min(9, m_options.size()))); + + if (!BeginRender(LayoutScale(10.0f), LayoutScale(20.0f), ImVec2(width, height))) + { + const ChoiceDialogCallback callback = std::move(m_callback); + ClearState(); + if (callback) + callback(-1, std::string(), false); + return; + } + + ResetFocusHere(); + + s32 choice = -1; + + if (m_checkable) + { + BeginMenuButtons(); + + for (s32 i = 0; i < static_cast(m_options.size()); i++) + { + auto& option = m_options[i]; + + const SmallString title = + SmallString::from_format("{0} {1}", option.second ? ICON_FA_CHECK_SQUARE : ICON_FA_SQUARE, option.first); + if (MenuButton(title.c_str(), nullptr, true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY)) { - SetFileSelectorDirectory(std::move(s_state.file_selector_items.front().full_path)); - s_state.file_selector_directory_changed = true; + choice = i; + option.second = !option.second; } } + + EndMenuButtons(); + } + else + { + // draw background first, because otherwise it'll obscure the frame border + for (s32 i = 0; i < static_cast(m_options.size()); i++) + { + const auto& option = m_options[i]; + if (!option.second) + continue; + + ImVec2 pos, size; + GetMenuButtonFrameBounds(LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, &pos, &size); + pos.y += size.y * static_cast(i); + ImGui::RenderFrame(pos, pos + size, ImGui::GetColorU32(UIStyle.PrimaryColor), false, + LayoutScale(MENU_ITEM_BORDER_ROUNDING)); + } + + BeginMenuButtons(); + + for (s32 i = 0; i < static_cast(m_options.size()); i++) + { + auto& option = m_options[i]; + if (option.second ? MenuButtonWithValue(option.first.c_str(), nullptr, ICON_FA_CHECK, true, + LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY) : + MenuButtonWithoutSummary(option.first.c_str())) + { + choice = i; + for (s32 j = 0; j < static_cast(m_options.size()); j++) + m_options[j].second = (j == i); + } + } + + EndMenuButtons(); + } + + GetChoiceDialogHelpText(s_state.fullscreen_footer_text); + + EndRender(); + + if (choice >= 0) + { + // immediately close dialog when selecting, save the callback doing it. have to take a copy in this instance, + // because the callback may open another dialog, and we don't want to close that one. + if (!m_checkable) + { + auto option = std::move(m_options[choice]); + const ChoiceDialogCallback callback = std::move(m_callback); + StartClose(); + callback(choice, option.first, option.second); + } + else + { + const auto& option = m_options[choice]; + m_callback(choice, option.first, option.second); + } } } bool ImGuiFullscreen::IsChoiceDialogOpen() { - return s_state.choice_dialog_open; + return s_state.choice_dialog.IsOpen(); } void ImGuiFullscreen::OpenChoiceDialog(std::string_view title, bool checkable, ChoiceDialogOptions options, ChoiceDialogCallback callback) { - if (s_state.choice_dialog_open) - CloseChoiceDialog(); - - s_state.choice_dialog_open = true; - s_state.choice_dialog_checkable = checkable; - s_state.choice_dialog_title = fmt::format("{}##choice_dialog", title); - s_state.choice_dialog_options = std::move(options); - s_state.choice_dialog_callback = std::move(callback); - QueueResetFocus(FocusResetType::PopupOpened); + s_state.choice_dialog.Open(title, std::move(options), std::move(callback), checkable); } void ImGuiFullscreen::CloseChoiceDialog() { - if (!s_state.choice_dialog_open) - return; - - if (ImGui::IsPopupOpen(s_state.choice_dialog_title.c_str(), 0)) - ImGui::ClosePopupToLevel(GImGui->OpenPopupStack.Size - 1, true); - - s_state.choice_dialog_open = false; - s_state.choice_dialog_checkable = false; - std::string().swap(s_state.choice_dialog_title); - ChoiceDialogOptions().swap(s_state.choice_dialog_options); - ChoiceDialogCallback().swap(s_state.choice_dialog_callback); - QueueResetFocus(FocusResetType::PopupClosed); -} - -void ImGuiFullscreen::DrawChoiceDialog() -{ - if (!s_state.choice_dialog_open) - return; - - PushPopupStyle(10.0f); - - const float width = LayoutScale(600.0f); - const float title_height = - UIStyle.LargeFont->FontSize + ImGui::GetStyle().FramePadding.y * 2.0f + ImGui::GetStyle().WindowPadding.y * 2.0f; - const float item_height = - (LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY) + LayoutScale(LAYOUT_MENU_BUTTON_Y_PADDING) * 2.0f); - const float height = - title_height + (item_height * static_cast(std::min(9, s_state.choice_dialog_options.size()))); - ImGui::SetNextWindowSize(ImVec2(width, height)); - ImGui::SetNextWindowPos((ImGui::GetIO().DisplaySize - LayoutScale(0.0f, LAYOUT_FOOTER_HEIGHT)) * 0.5f, - ImGuiCond_Always, ImVec2(0.5f, 0.5f)); - ImGui::OpenPopup(s_state.choice_dialog_title.c_str()); - - bool is_open = true; - s32 choice = -1; - - if (ImGui::BeginPopupModal(s_state.choice_dialog_title.c_str(), &is_open, - ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove)) - { - ImGui::PushStyleColor(ImGuiCol_Text, UIStyle.BackgroundTextColor); - - ResetFocusHere(); - - if (s_state.choice_dialog_checkable) - { - BeginMenuButtons(); - - for (s32 i = 0; i < static_cast(s_state.choice_dialog_options.size()); i++) - { - auto& option = s_state.choice_dialog_options[i]; - - const SmallString title = - SmallString::from_format("{0} {1}", option.second ? ICON_FA_CHECK_SQUARE : ICON_FA_SQUARE, option.first); - if (MenuButton(title.c_str(), nullptr, true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY)) - { - choice = i; - option.second = !option.second; - } - } - - EndMenuButtons(); - } - else - { - // draw background first, because otherwise it'll obscure the frame border - for (s32 i = 0; i < static_cast(s_state.choice_dialog_options.size()); i++) - { - const auto& option = s_state.choice_dialog_options[i]; - if (!option.second) - continue; - - ImVec2 pos, size; - GetMenuButtonFrameBounds(LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, &pos, &size); - pos.y += size.y * static_cast(i); - ImGui::RenderFrame(pos, pos + size, ImGui::GetColorU32(UIStyle.PrimaryColor), false, - LayoutScale(MENU_ITEM_BORDER_ROUNDING)); - } - - BeginMenuButtons(); - - for (s32 i = 0; i < static_cast(s_state.choice_dialog_options.size()); i++) - { - auto& option = s_state.choice_dialog_options[i]; - if (option.second ? MenuButtonWithValue(option.first.c_str(), nullptr, ICON_FA_CHECK, true, - LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY) : - MenuButtonWithoutSummary(option.first.c_str())) - { - choice = i; - for (s32 j = 0; j < static_cast(s_state.choice_dialog_options.size()); j++) - s_state.choice_dialog_options[j].second = (j == i); - } - } - - EndMenuButtons(); - } - - ImGui::PopStyleColor(1); - - ImGui::EndPopup(); - } - - PopPopupStyle(); - - is_open &= !WantsToCloseMenu(); - - if (choice >= 0) - { - // immediately close dialog when selecting, save the callback doing it. have to take a copy in this instance, - // because the callback may open another dialog, and we don't want to close that one. - if (!s_state.choice_dialog_checkable) - { - auto option = std::move(s_state.choice_dialog_options[choice]); - const ChoiceDialogCallback callback = std::move(s_state.choice_dialog_callback); - CloseChoiceDialog(); - callback(choice, option.first, option.second); - } - else - { - const auto& option = s_state.choice_dialog_options[choice]; - s_state.choice_dialog_callback(choice, option.first, option.second); - } - } - else if (!is_open) - { - const ChoiceDialogCallback callback = std::move(s_state.choice_dialog_callback); - CloseChoiceDialog(); - callback(-1, std::string(), false); - } - else - { - GetChoiceDialogHelpText(s_state.fullscreen_footer_text); - } + s_state.choice_dialog.StartClose(); } bool ImGuiFullscreen::IsInputDialogOpen() { - return s_state.input_dialog_open; + return s_state.input_string_dialog.IsOpen(); } -void ImGuiFullscreen::OpenInputStringDialog(std::string title, std::string message, std::string caption, +void ImGuiFullscreen::OpenInputStringDialog(std::string_view title, std::string message, std::string caption, std::string ok_button_text, InputStringDialogCallback callback) { - s_state.input_dialog_open = true; - s_state.input_dialog_title = std::move(title); - s_state.input_dialog_message = std::move(message); - s_state.input_dialog_caption = std::move(caption); - s_state.input_dialog_ok_text = std::move(ok_button_text); - s_state.input_dialog_callback = std::move(callback); + s_state.input_string_dialog.Open(title, std::move(message), std::move(caption), std::move(ok_button_text), + std::move(callback)); QueueResetFocus(FocusResetType::PopupOpened); } -void ImGuiFullscreen::DrawInputDialog() +ImGuiFullscreen::InputStringDialog::InputStringDialog() = default; + +ImGuiFullscreen::InputStringDialog::~InputStringDialog() = default; + +void ImGuiFullscreen::InputStringDialog::Open(std::string_view title, std::string message, std::string caption, + std::string ok_button_text, InputStringDialogCallback callback) { - if (!s_state.input_dialog_open) + SetTitleAndOpen(fmt::format("{}##input_string_dialog", title)); + m_message = std::move(message); + m_caption = std::move(caption); + m_ok_text = std::move(ok_button_text); + m_callback = std::move(callback); +} + +void ImGuiFullscreen::InputStringDialog::ClearState() +{ + PopupDialog::ClearState(); + m_message = {}; + m_caption = {}; + m_ok_text = {}; + m_callback = {}; +} + +void ImGuiFullscreen::InputStringDialog::Draw() +{ + if (!IsOpen()) return; - ImGui::SetNextWindowSize(LayoutScale(700.0f, 0.0f)); - ImGui::SetNextWindowPos((ImGui::GetIO().DisplaySize - LayoutScale(0.0f, LAYOUT_FOOTER_HEIGHT)) * 0.5f, - ImGuiCond_Always, ImVec2(0.5f, 0.5f)); - ImGui::OpenPopup(s_state.input_dialog_title.c_str()); - - PushPopupStyle(); - - bool is_open = true; - if (ImGui::BeginPopupModal(s_state.input_dialog_title.c_str(), &is_open, - ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | - ImGuiWindowFlags_NoMove)) + if (!BeginRender(LayoutScale(20.0f), LayoutScale(20.0f), LayoutScale(700.0f, 0.0f))) { - ImGui::PushStyleColor(ImGuiCol_Text, UIStyle.BackgroundTextColor); - - ResetFocusHere(); - ImGui::TextWrapped("%s", s_state.input_dialog_message.c_str()); - - BeginMenuButtons(); - - ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f)); - - if (!s_state.input_dialog_caption.empty()) - { - const float prev = ImGui::GetCursorPosX(); - ImGui::TextUnformatted(s_state.input_dialog_caption.c_str()); - ImGui::SetNextItemWidth(ImGui::GetCursorPosX() - prev); - } - else - { - ImGui::SetNextItemWidth(ImGui::GetCurrentWindow()->WorkRect.GetWidth()); - } - ImGui::InputText("##input", &s_state.input_dialog_text); - - ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f)); - - const bool ok_enabled = !s_state.input_dialog_text.empty(); - - if (MenuButtonWithoutSummary(s_state.input_dialog_ok_text.c_str(), ok_enabled) && ok_enabled) - { - // have to move out in case they open another dialog in the callback - InputStringDialogCallback cb(std::move(s_state.input_dialog_callback)); - std::string text(std::move(s_state.input_dialog_text)); - CloseInputDialog(); - ImGui::CloseCurrentPopup(); - cb(std::move(text)); - } - - if (MenuButtonWithoutSummary(ICON_FA_TIMES " Cancel")) - { - CloseInputDialog(); - - ImGui::CloseCurrentPopup(); - } - - EndMenuButtons(); - - ImGui::PopStyleColor(1); - - ImGui::EndPopup(); + InputStringDialogCallback cb = std::move(m_callback); + ClearState(); + if (cb) + cb(std::string()); + return; } - if (!is_open) - CloseInputDialog(); - else - GetInputDialogHelpText(s_state.fullscreen_footer_text); - PopPopupStyle(); + ResetFocusHere(); + ImGui::TextWrapped("%s", m_message.c_str()); + + BeginMenuButtons(); + + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f)); + + if (!m_caption.empty()) + { + const float prev = ImGui::GetCursorPosX(); + ImGui::TextUnformatted(m_caption.c_str()); + ImGui::SetNextItemWidth(ImGui::GetCursorPosX() - prev); + } + else + { + ImGui::SetNextItemWidth(ImGui::GetCurrentWindow()->WorkRect.GetWidth()); + } + ImGui::InputText("##input", &m_text); + + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f)); + + const bool ok_enabled = !m_text.empty(); + + if (MenuButtonWithoutSummary(m_ok_text.c_str(), ok_enabled) && ok_enabled) + { + // have to move out in case they open another dialog in the callback + InputStringDialogCallback cb = std::move(m_callback); + std::string text = std::move(m_text); + StartClose(); + cb(std::move(text)); + } + + if (MenuButtonWithoutSummary(ICON_FA_TIMES " Cancel")) + StartClose(); + + EndMenuButtons(); + + GetInputDialogHelpText(s_state.fullscreen_footer_text); + + EndRender(); } void ImGuiFullscreen::CloseInputDialog() { - if (!s_state.input_dialog_open) + s_state.input_string_dialog.StartClose(); +} + +ImGuiFullscreen::MessageDialog::MessageDialog() = default; + +ImGuiFullscreen::MessageDialog::~MessageDialog() = default; + +void ImGuiFullscreen::MessageDialog::Open(std::string_view title, std::string message, CallbackVariant callback, + std::string first_button_text, std::string second_button_text, + std::string third_button_text) +{ + SetTitleAndOpen(fmt::format("{}##message_dialog", title)); + m_message = std::move(message); + m_callback = std::move(callback); + m_buttons[0] = std::move(first_button_text); + m_buttons[1] = std::move(second_button_text); + m_buttons[2] = std::move(third_button_text); +} + +void ImGuiFullscreen::MessageDialog::ClearState() +{ + PopupDialog::ClearState(); + m_message = {}; + m_buttons = {}; + m_callback = {}; +} + +void ImGuiFullscreen::MessageDialog::Draw() +{ + if (!IsOpen()) return; - if (ImGui::IsPopupOpen(s_state.input_dialog_title.c_str(), 0)) - ImGui::ClosePopupToLevel(GImGui->OpenPopupStack.Size - 1, true); + if (!BeginRender(LayoutScale(20.0f), LayoutScale(20.0f), LayoutScale(700.0f, 0.0f))) + { + CallbackVariant cb = std::move(m_callback); + ClearState(); + InvokeCallback(cb, std::nullopt); + return; + } - s_state.input_dialog_open = false; - s_state.input_dialog_title = {}; - s_state.input_dialog_message = {}; - s_state.input_dialog_caption = {}; - s_state.input_dialog_ok_text = {}; - s_state.input_dialog_text = {}; - s_state.input_dialog_callback = {}; + std::optional result; + + ResetFocusHere(); + BeginMenuButtons(); + + ImGui::TextWrapped("%s", m_message.c_str()); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(20.0f)); + + for (s32 button_index = 0; button_index < static_cast(m_buttons.size()); button_index++) + { + if (!m_buttons[button_index].empty() && MenuButtonWithoutSummary(m_buttons[button_index].c_str())) + result = button_index; + } + + EndMenuButtons(); + + GetChoiceDialogHelpText(s_state.fullscreen_footer_text); + + EndRender(); + + if (result.has_value()) + { + // have to move out in case they open another dialog in the callback + StartClose(); + InvokeCallback(std::move(m_callback), result); + } +} + +void ImGuiFullscreen::MessageDialog::InvokeCallback(const CallbackVariant& cb, std::optional choice) +{ + if (std::holds_alternative(cb)) + { + const InfoMessageDialogCallback& func = std::get(cb); + if (func) + func(); + } + else if (std::holds_alternative(cb)) + { + const ConfirmMessageDialogCallback& func = std::get(cb); + if (func) + func(choice.value_or(1) == 0); + } } bool ImGuiFullscreen::IsMessageBoxDialogOpen() { - return s_state.message_dialog_open; + return s_state.message_dialog.IsOpen(); } -void ImGuiFullscreen::OpenConfirmMessageDialog(std::string title, std::string message, +void ImGuiFullscreen::OpenConfirmMessageDialog(std::string_view title, std::string message, ConfirmMessageDialogCallback callback, std::string yes_button_text, std::string no_button_text) { - CloseMessageDialog(); - - s_state.message_dialog_open = true; - s_state.message_dialog_title = std::move(title); - s_state.message_dialog_message = std::move(message); - s_state.message_dialog_callback = std::move(callback); - s_state.message_dialog_buttons[0] = std::move(yes_button_text); - s_state.message_dialog_buttons[1] = std::move(no_button_text); - QueueResetFocus(FocusResetType::PopupOpened); + s_state.message_dialog.Open(std::move(title), std::move(message), std::move(callback), std::move(yes_button_text), + std::move(no_button_text), std::string()); } -void ImGuiFullscreen::OpenInfoMessageDialog(std::string title, std::string message, InfoMessageDialogCallback callback, - std::string button_text) +void ImGuiFullscreen::OpenInfoMessageDialog(std::string_view title, std::string message, + InfoMessageDialogCallback callback, std::string button_text) { - CloseMessageDialog(); - - s_state.message_dialog_open = true; - s_state.message_dialog_title = std::move(title); - s_state.message_dialog_message = std::move(message); - s_state.message_dialog_callback = std::move(callback); - s_state.message_dialog_buttons[0] = std::move(button_text); - QueueResetFocus(FocusResetType::PopupOpened); + s_state.message_dialog.Open(std::move(title), std::move(message), std::move(callback), std::move(button_text), + std::string(), std::string()); } -void ImGuiFullscreen::OpenMessageDialog(std::string title, std::string message, MessageDialogCallback callback, +void ImGuiFullscreen::OpenMessageDialog(std::string_view title, std::string message, MessageDialogCallback callback, std::string first_button_text, std::string second_button_text, std::string third_button_text) { - CloseMessageDialog(); - - s_state.message_dialog_open = true; - s_state.message_dialog_title = std::move(title); - s_state.message_dialog_message = std::move(message); - s_state.message_dialog_callback = std::move(callback); - s_state.message_dialog_buttons[0] = std::move(first_button_text); - s_state.message_dialog_buttons[1] = std::move(second_button_text); - s_state.message_dialog_buttons[2] = std::move(third_button_text); - QueueResetFocus(FocusResetType::PopupOpened); + s_state.message_dialog.Open(std::move(title), std::move(message), std::move(callback), std::move(first_button_text), + std::move(second_button_text), std::move(third_button_text)); } void ImGuiFullscreen::CloseMessageDialog() { - if (!s_state.message_dialog_open) - return; - - if (ImGui::IsPopupOpen(s_state.message_dialog_title.c_str(), 0)) - ImGui::ClosePopupToLevel(GImGui->OpenPopupStack.Size - 1, true); - - s_state.message_dialog_open = false; - s_state.message_dialog_title = {}; - s_state.message_dialog_message = {}; - s_state.message_dialog_buttons = {}; - s_state.message_dialog_callback = {}; - QueueResetFocus(FocusResetType::PopupClosed); -} - -void ImGuiFullscreen::DrawMessageDialog() -{ - if (!s_state.message_dialog_open) - return; - - const char* win_id = s_state.message_dialog_title.empty() ? "##messagedialog" : s_state.message_dialog_title.c_str(); - - ImGui::SetNextWindowSize(LayoutScale(700.0f, 0.0f)); - ImGui::SetNextWindowPos((ImGui::GetIO().DisplaySize - LayoutScale(0.0f, LAYOUT_FOOTER_HEIGHT)) * 0.5f, - ImGuiCond_Always, ImVec2(0.5f, 0.5f)); - ImGui::OpenPopup(win_id); - - PushPopupStyle(); - - bool is_open = true; - const u32 flags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | - (s_state.message_dialog_title.empty() ? ImGuiWindowFlags_NoTitleBar : 0); - std::optional result; - - if (ImGui::BeginPopupModal(win_id, &is_open, flags)) - { - if (WantsToCloseMenu()) - is_open = false; - - ImGui::PushStyleColor(ImGuiCol_Text, UIStyle.BackgroundTextColor); - - ResetFocusHere(); - BeginMenuButtons(); - - ImGui::TextWrapped("%s", s_state.message_dialog_message.c_str()); - ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(20.0f)); - - for (s32 button_index = 0; button_index < static_cast(s_state.message_dialog_buttons.size()); button_index++) - { - if (!s_state.message_dialog_buttons[button_index].empty() && - MenuButtonWithoutSummary(s_state.message_dialog_buttons[button_index].c_str())) - { - result = button_index; - ImGui::CloseCurrentPopup(); - } - } - - EndMenuButtons(); - - ImGui::PopStyleColor(); - ImGui::EndPopup(); - } - - PopPopupStyle(); - - if (!is_open || result.has_value()) - { - // have to move out in case they open another dialog in the callback - auto cb = (std::move(s_state.message_dialog_callback)); - CloseMessageDialog(); - - if (std::holds_alternative(cb)) - { - const InfoMessageDialogCallback& func = std::get(cb); - if (func) - func(); - } - else if (std::holds_alternative(cb)) - { - const ConfirmMessageDialogCallback& func = std::get(cb); - if (func) - func(result.value_or(1) == 0); - } - } - else - { - GetChoiceDialogHelpText(s_state.fullscreen_footer_text); - } + s_state.message_dialog.StartClose(); } static float s_notification_vertical_position = 0.15f; diff --git a/src/util/imgui_fullscreen.h b/src/util/imgui_fullscreen.h index 478cbcabb..3b067514a 100644 --- a/src/util/imgui_fullscreen.h +++ b/src/util/imgui_fullscreen.h @@ -44,6 +44,9 @@ static constexpr float LAYOUT_HORIZONTAL_MENU_PADDING = 30.0f; static constexpr float LAYOUT_HORIZONTAL_MENU_ITEM_WIDTH = 250.0f; static constexpr float LAYOUT_HORIZONTAL_MENU_ITEM_IMAGE_SIZE = 150.0f; static constexpr float LAYOUT_SHADOW_OFFSET = 1.0f; +static constexpr float LAYOUT_SMALL_POPUP_PADDING = 20.0f; +static constexpr float LAYOUT_LARGE_POPUP_PADDING = 30.0f; +static constexpr float LAYOUT_LARGE_POPUP_ROUNDING = 40.0f; struct ALIGN_TO_CACHE_LINE UIStyles { @@ -185,8 +188,16 @@ void UploadAsyncTextures(); void BeginLayout(); void EndLayout(); -bool BeginFixedPopupModal(const char* name, bool* p_open = nullptr); -void EndFixedPopupModal(); + +bool IsAnyFixedPopupDialogOpen(); +bool IsFixedPopupDialogOpen(std::string_view name); +void OpenFixedPopupDialog(std::string_view name); +void CloseFixedPopupDialog(); +void CloseFixedPopupDialogImmediately(); +bool BeginFixedPopupDialog(float scaled_window_padding = LayoutScale(20.0f), + float scaled_window_rounding = LayoutScale(20.0f), + const ImVec2& scaled_window_size = ImVec2(0.0f, 0.0f)); +void EndFixedPopupDialog(); void RenderOverlays(); @@ -357,7 +368,7 @@ void CloseChoiceDialog(); using InputStringDialogCallback = std::function; bool IsInputDialogOpen(); -void OpenInputStringDialog(std::string title, std::string message, std::string caption, std::string ok_button_text, +void OpenInputStringDialog(std::string_view title, std::string message, std::string caption, std::string ok_button_text, InputStringDialogCallback callback); void CloseInputDialog(); @@ -365,12 +376,12 @@ using ConfirmMessageDialogCallback = std::function; using InfoMessageDialogCallback = std::function; using MessageDialogCallback = std::function; bool IsMessageBoxDialogOpen(); -void OpenConfirmMessageDialog(std::string title, std::string message, ConfirmMessageDialogCallback callback, +void OpenConfirmMessageDialog(std::string_view title, std::string message, ConfirmMessageDialogCallback callback, std::string yes_button_text = ICON_FA_CHECK " Yes", std::string no_button_text = ICON_FA_TIMES " No"); -void OpenInfoMessageDialog(std::string title, std::string message, InfoMessageDialogCallback callback = {}, +void OpenInfoMessageDialog(std::string_view title, std::string message, InfoMessageDialogCallback callback = {}, std::string button_text = ICON_FA_WINDOW_CLOSE " Close"); -void OpenMessageDialog(std::string title, std::string message, MessageDialogCallback callback, +void OpenMessageDialog(std::string_view title, std::string message, MessageDialogCallback callback, std::string first_button_text, std::string second_button_text, std::string third_button_text); void CloseMessageDialog(); @@ -405,6 +416,46 @@ void ClearToast(); void GetChoiceDialogHelpText(SmallStringBase& dest); void GetFileSelectorHelpText(SmallStringBase& dest); void GetInputDialogHelpText(SmallStringBase& dest); + +// Wrapper for an animated popup dialog. +class PopupDialog +{ +public: + PopupDialog(); + ~PopupDialog(); + + ALWAYS_INLINE const std::string& GetTitle() const { return m_title; } + ALWAYS_INLINE bool IsOpen() const { return (m_state != State::Inactive); } + + void StartClose(); + void CloseImmediately(); + void ClearState(); + +protected: + enum class State + { + Inactive, + ClosingTrigger, + Open, + OpeningTrigger, + Opening, + Closing, + }; + + static constexpr float OPEN_TIME = 0.2f; + static constexpr float CLOSE_TIME = 0.1f; + + void SetTitleAndOpen(std::string title); + + bool BeginRender(float scaled_window_padding = LayoutScale(20.0f), float scaled_window_rounding = LayoutScale(20.0f), + const ImVec2& scaled_window_size = ImVec2(0.0f, 0.0f)); + void EndRender(); + + std::string m_title; + float m_animation_time_remaining = 0.0f; + State m_state = State::Inactive; +}; + } // namespace ImGuiFullscreen // Host UI triggers from Big Picture mode.