diff --git a/dep/imgui/include/IconsEmoji.h b/dep/imgui/include/IconsEmoji.h index 349783afb..56d2391cf 100644 --- a/dep/imgui/include/IconsEmoji.h +++ b/dep/imgui/include/IconsEmoji.h @@ -51,3 +51,4 @@ #define ICON_EMOJI_BAR_CHART "\xf0\x9f\x93\x8a" #define ICON_EMOJI_CHART_UPWARDS_TREND "\xf0\x9f\x93\x88" #define ICON_EMOJI_DIRECT_HIT "\xf0\x9f\x8e\xaf" +#define ICON_EMOJI_RED_FLAG "\xf0\x9f\x9a\xa9" diff --git a/src/core/achievements.cpp b/src/core/achievements.cpp index 05517d810..20515ac1c 100644 --- a/src/core/achievements.cpp +++ b/src/core/achievements.cpp @@ -74,7 +74,6 @@ static constexpr const char* UNLOCK_SOUND_NAME = "sounds/achievements/unlock.wav static constexpr const char* LBSUBMIT_SOUND_NAME = "sounds/achievements/lbsubmit.wav"; static constexpr const char* CACHE_SUBDIRECTORY_NAME = "achievement_images"; constexpr const char* const RA_LOGO_ICON_NAME = "images/ra-icon.webp"; -constexpr const std::string_view NOTIFICATION_SPINNER_NOTE = "__spinner__"; static constexpr float LOGIN_NOTIFICATION_TIME = 5.0f; static constexpr float ACHIEVEMENT_SUMMARY_NOTIFICATION_TIME = 5.0f; @@ -85,7 +84,7 @@ static constexpr float CHALLENGE_STARTED_NOTIFICATION_TIME = 5.0f; static constexpr float CHALLENGE_FAILED_NOTIFICATION_TIME = 5.0f; static constexpr float LEADERBOARD_STARTED_NOTIFICATION_TIME = 3.0f; static constexpr float LEADERBOARD_FAILED_NOTIFICATION_TIME = 3.0f; -static constexpr float LEADERBOARD_NOTIFICATION_MIN_WIDTH = 380.0f; +static constexpr u16 LEADERBOARD_NOTIFICATION_MIN_WIDTH = 380; // Some API calls are really slow. Set a longer timeout. static constexpr float SERVER_CALL_TIMEOUT = 60.0f; @@ -1303,7 +1302,8 @@ void Achievements::DisplayAchievementSummary() FullscreenUI::AddAchievementNotification("AchievementsSummary", IsHardcoreModeActive() ? ACHIEVEMENT_SUMMARY_NOTIFICATION_TIME_HC : ACHIEVEMENT_SUMMARY_NOTIFICATION_TIME, - s_state.game_icon, s_state.game_title, std::string(summary), {}); + s_state.game_icon, s_state.game_title, std::string(summary), + RA_LOGO_ICON_NAME, FullscreenUI::AchievementNotificationNoteType::Image); if (s_state.game_summary.num_unsupported_achievements > 0) { @@ -1311,8 +1311,7 @@ void Achievements::DisplayAchievementSummary() "UnsupportedAchievements", ACHIEVEMENT_SUMMARY_UNSUPPORTED_TIME, "images/warning.svg", TRANSLATE_STR("Achievements", "Unsupported Achievements"), TRANSLATE_PLURAL_STR("Achievements", "%n achievements are not supported by DuckStation.", "Achievement popup", - s_state.game_summary.num_unsupported_achievements), - {}); + s_state.game_summary.num_unsupported_achievements)); } } @@ -1365,10 +1364,12 @@ void Achievements::HandleUnlockEvent(const rc_client_event_t* event) if (cheevo->points > 0) note = fmt::format(ICON_EMOJI_TROPHY " {}", cheevo->points); - FullscreenUI::AddAchievementNotification(fmt::format("achievement_unlock_{}", cheevo->id), - static_cast(g_settings.achievements_notification_duration), - GetAchievementBadgePath(cheevo, false), std::move(title), - std::string(cheevo->description), std::move(note)); + FullscreenUI::AddAchievementNotification( + fmt::format("achievement_unlock_{}", cheevo->id), + static_cast(g_settings.achievements_notification_duration), GetAchievementBadgePath(cheevo, false), + std::move(title), std::string(cheevo->description), std::move(note), + (cheevo->points > 0) ? FullscreenUI::AchievementNotificationNoteType::Text : + FullscreenUI::AchievementNotificationNoteType::None); } if (g_settings.achievements_sound_effects) @@ -1383,14 +1384,14 @@ void Achievements::HandleGameCompleteEvent(const rc_client_event_t* event) if (g_settings.achievements_notifications) { std::string message = fmt::format( - TRANSLATE_FS("Achievements", "Game complete.\n{0}, {1}."), + TRANSLATE_FS("Achievements", "Game complete.\n{0} and {1}."), TRANSLATE_PLURAL_STR("Achievements", "%n achievements", "Mastery popup", s_state.game_summary.num_unlocked_achievements), TRANSLATE_PLURAL_STR("Achievements", "%n points", "Achievement points", s_state.game_summary.points_unlocked)); - std::string note = fmt::format(ICON_EMOJI_TROPHY " {}", s_state.game_summary.points_unlocked); FullscreenUI::AddAchievementNotification("achievement_mastery", GAME_COMPLETE_NOTIFICATION_TIME, s_state.game_icon, - s_state.game_title, std::move(message), std::move(note)); + s_state.game_title, std::move(message), ICON_EMOJI_TROPHY, + FullscreenUI::AchievementNotificationNoteType::IconText); } } @@ -1405,14 +1406,14 @@ void Achievements::HandleSubsetCompleteEvent(const rc_client_event_t* event) std::string badge_path = GetSubsetBadgePath(event->subset); std::string message = fmt::format( - TRANSLATE_FS("Achievements", "Subset complete.\n{0}, {1}."), + TRANSLATE_FS("Achievements", "Subset complete.\n{0} and {1}."), TRANSLATE_PLURAL_STR("Achievements", "%n achievements", "Mastery popup", s_state.game_summary.num_unlocked_achievements), TRANSLATE_PLURAL_STR("Achievements", "%n points", "Achievement points", s_state.game_summary.points_unlocked)); - FullscreenUI::AddAchievementNotification("achievement_mastery", GAME_COMPLETE_NOTIFICATION_TIME, - std::move(badge_path), std::string(event->subset->title), - std::move(message), {}); + FullscreenUI::AddAchievementNotification( + "achievement_mastery", GAME_COMPLETE_NOTIFICATION_TIME, std::move(badge_path), std::string(event->subset->title), + std::move(message), ICON_EMOJI_CHECKMARK_BUTTON, FullscreenUI::AchievementNotificationNoteType::IconText); } } @@ -1424,7 +1425,8 @@ void Achievements::HandleLeaderboardStartedEvent(const rc_client_event_t* event) { FullscreenUI::AddAchievementNotification( fmt::format("leaderboard_{}", event->leaderboard->id), LEADERBOARD_STARTED_NOTIFICATION_TIME, s_state.game_icon, - std::string(event->leaderboard->title), TRANSLATE_STR("Achievements", "Leaderboard attempt started."), {}); + std::string(event->leaderboard->title), TRANSLATE_STR("Achievements", "Leaderboard attempt started."), + ICON_EMOJI_RED_FLAG, FullscreenUI::AchievementNotificationNoteType::IconText); } } @@ -1436,7 +1438,8 @@ void Achievements::HandleLeaderboardFailedEvent(const rc_client_event_t* event) { FullscreenUI::AddAchievementNotification( fmt::format("leaderboard_{}", event->leaderboard->id), LEADERBOARD_FAILED_NOTIFICATION_TIME, s_state.game_icon, - std::string(event->leaderboard->title), TRANSLATE_STR("Achievements", "Leaderboard attempt failed."), {}); + std::string(event->leaderboard->title), TRANSLATE_STR("Achievements", "Leaderboard attempt failed."), + ICON_EMOJI_CROSS_MARK_BUTTON, FullscreenUI::AchievementNotificationNoteType::IconText); } } @@ -1475,7 +1478,9 @@ void Achievements::HandleLeaderboardSubmittedEvent(const rc_client_event_t* even fmt::format("leaderboard_{}", event->leaderboard->id), static_cast(g_settings.achievements_leaderboard_duration), s_state.game_icon, std::string(event->leaderboard->title), std::move(message), - std::string(g_settings.achievements_spectator_mode ? std::string_view() : NOTIFICATION_SPINNER_NOTE), + g_settings.achievements_spectator_mode ? std::string(ICON_EMOJI_CHART_UPWARDS_TREND) : std::string(), + g_settings.achievements_spectator_mode ? FullscreenUI::AchievementNotificationNoteType::IconText : + FullscreenUI::AchievementNotificationNoteType::Spinner, LEADERBOARD_NOTIFICATION_MIN_WIDTH); } @@ -1497,7 +1502,7 @@ void Achievements::HandleLeaderboardScoreboardEvent(const rc_client_event_t* eve }; std::string message = fmt::format( - "{} {}\n" ICON_EMOJI_BAR_CHART " {}", GetLeaderboardFormatIcon(event->leaderboard->format), + "{} {}\n" ICON_EMOJI_CHART_UPWARDS_TREND " {}", GetLeaderboardFormatIcon(event->leaderboard->format), TinyString::from_format( fmt::runtime(Host::TranslateToStringView( "Achievements", @@ -1506,11 +1511,11 @@ void Achievements::HandleLeaderboardScoreboardEvent(const rc_client_event_t* eve TinyString::from_format(TRANSLATE_FS("Achievements", "Leaderboard Position: {0} of {1}"), event->leaderboard_scoreboard->new_rank, event->leaderboard_scoreboard->num_entries)); - FullscreenUI::AddAchievementNotification(fmt::format("leaderboard_{}", event->leaderboard->id), - static_cast(g_settings.achievements_leaderboard_duration), - s_state.game_icon, std::string(event->leaderboard->title), - std::move(message), ICON_EMOJI_CHART_UPWARDS_TREND, - LEADERBOARD_NOTIFICATION_MIN_WIDTH); + FullscreenUI::AddAchievementNotification( + fmt::format("leaderboard_{}", event->leaderboard->id), + static_cast(g_settings.achievements_leaderboard_duration), s_state.game_icon, + std::string(event->leaderboard->title), std::move(message), ICON_EMOJI_CHECKMARK_BUTTON, + FullscreenUI::AchievementNotificationNoteType::IconText, LEADERBOARD_NOTIFICATION_MIN_WIDTH); } } @@ -1584,11 +1589,11 @@ void Achievements::HandleAchievementChallengeIndicatorShowEvent(const rc_client_ // we still track these even if the option is disabled, so that they can be displayed in the pause menu if (g_settings.achievements_challenge_indicator_mode == AchievementChallengeIndicatorMode::Notification) { - FullscreenUI::AddAchievementNotification(fmt::format("AchievementChallenge{}", event->achievement->id), - CHALLENGE_STARTED_NOTIFICATION_TIME, badge_path, - fmt::format(TRANSLATE_FS("Achievements", "Challenge Started: {}"), - event->achievement->title ? event->achievement->title : ""), - event->achievement->description, {}); + FullscreenUI::AddAchievementNotification( + fmt::format("AchievementChallenge{}", event->achievement->id), CHALLENGE_STARTED_NOTIFICATION_TIME, badge_path, + fmt::format(TRANSLATE_FS("Achievements", "Challenge Started: {}"), + event->achievement->title ? event->achievement->title : ""), + fmt::format(ICON_EMOJI_DIRECT_HIT " {}", event->achievement->description ? event->achievement->description : "")); } s_state.active_challenge_indicators.push_back( @@ -1613,11 +1618,12 @@ void Achievements::HandleAchievementChallengeIndicatorHideEvent(const rc_client_ if (g_settings.achievements_challenge_indicator_mode == AchievementChallengeIndicatorMode::Notification && event->achievement->state == RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE) { - FullscreenUI::AddAchievementNotification(fmt::format("AchievementChallenge{}", event->achievement->id), - CHALLENGE_FAILED_NOTIFICATION_TIME, it->badge_path, - fmt::format(TRANSLATE_FS("Achievements", "Challenge Failed: {}"), - event->achievement->title ? event->achievement->title : ""), - event->achievement->description, {}); + FullscreenUI::AddAchievementNotification( + fmt::format("AchievementChallenge{}", event->achievement->id), CHALLENGE_FAILED_NOTIFICATION_TIME, it->badge_path, + fmt::format(TRANSLATE_FS("Achievements", "Challenge Failed: {}"), + event->achievement->title ? event->achievement->title : ""), + fmt::format(ICON_EMOJI_CROSS_MARK_BUTTON " {}", + event->achievement->description ? event->achievement->description : "")); } if (g_settings.achievements_challenge_indicator_mode == AchievementChallengeIndicatorMode::Notification || g_settings.achievements_challenge_indicator_mode == AchievementChallengeIndicatorMode::Disabled) @@ -2079,8 +2085,7 @@ void Achievements::ClientLoginWithTokenCallback(int result, const char* error_me TRANSLATE_STR("Achievements", "RetroAchievements Login Failed"), fmt::format( TRANSLATE_FS("Achievements", "Achievement unlocks will not be submitted for this session.\nError: {}"), - error_message), - {}); + error_message)); } return; @@ -2120,7 +2125,8 @@ void Achievements::FinishLogin() user->score, user->score_softcore, user->num_unread_messages); FullscreenUI::AddAchievementNotification("achievements_login", LOGIN_NOTIFICATION_TIME, s_state.user_badge_path, - user->display_name, std::move(summary), {}); + user->display_name, std::move(summary), RA_LOGO_ICON_NAME, + FullscreenUI::AchievementNotificationNoteType::Image); } } diff --git a/src/core/achievements_private.h b/src/core/achievements_private.h index 55760cf38..785ca81e6 100644 --- a/src/core/achievements_private.h +++ b/src/core/achievements_private.h @@ -16,7 +16,6 @@ namespace Achievements { inline constexpr float INDICATOR_FADE_IN_TIME = 0.2f; inline constexpr float INDICATOR_FADE_OUT_TIME = 0.4f; -extern const std::string_view NOTIFICATION_SPINNER_NOTE; struct LeaderboardTrackerIndicator { diff --git a/src/core/fullscreenui_achievements.cpp b/src/core/fullscreenui_achievements.cpp index ffaf9da06..c233ab0a8 100644 --- a/src/core/fullscreenui_achievements.cpp +++ b/src/core/fullscreenui_achievements.cpp @@ -7,6 +7,7 @@ #include "host.h" #include "system.h" +#include "util/gpu_texture.h" #include "util/imgui_manager.h" #include "util/translation.h" @@ -59,7 +60,8 @@ struct Notification float duration; float target_y; float last_y; - float min_width; + u16 min_width; + AchievementNotificationNoteType note_type; }; struct PauseMenuAchievementInfo @@ -238,7 +240,8 @@ void FullscreenUI::DrawAchievementsOverlays() } void FullscreenUI::AddAchievementNotification(std::string key, float duration, std::string image_path, - std::string title, std::string text, std::string note, float min_width) + std::string title, std::string text, std::string note, + AchievementNotificationNoteType note_type, u16 min_width) { const bool prev_had_notifications = s_achievements_locals.notifications.empty(); const Timer::Value current_time = Timer::GetCurrentValue(); @@ -255,6 +258,7 @@ void FullscreenUI::AddAchievementNotification(std::string key, float duration, s it->note = std::move(note); it->badge_path = std::move(image_path); it->min_width = min_width; + it->note_type = note_type; // Don't fade it in again const float time_passed = static_cast(Timer::ConvertValueToSeconds(current_time - it->start_time)); @@ -277,6 +281,7 @@ void FullscreenUI::AddAchievementNotification(std::string key, float duration, s notif.target_y = -1.0f; notif.last_y = -1.0f; notif.min_width = min_width; + notif.note_type = note_type; s_achievements_locals.notifications.push_back(std::move(notif)); if (!prev_had_notifications) @@ -302,8 +307,8 @@ void FullscreenUI::DrawNotifications(NotificationLayout& layout) const float max_text_width = max_width - badge_size - (horizontal_padding * 2.0f) - horizontal_spacing; const float min_height = (vertical_padding * 2.0f) + badge_size; const float rounding = FullscreenUI::LayoutScale(20.0f); - const float spinner_size = FullscreenUI::LayoutScale(16.0f); const float min_rounded_width = rounding * 2.0f; + const float note_icon_padding = LayoutScale(30.0f); ImFont*& font = UIStyle.Font; const float& title_font_size = UIStyle.LargeFontSize; @@ -329,26 +334,67 @@ void FullscreenUI::DrawNotifications(NotificationLayout& layout) } // place note to the right of the title - const bool is_spinner = (notif.note == Achievements::NOTIFICATION_SPINNER_NOTE); - const bool is_icon_note = - (!is_spinner && !notif.note.empty() && (StringUtil::GetUTF8CharacterCount(notif.note) == 1)); - const float note_font_size = is_spinner ? spinner_size : (is_icon_note ? note_icon_size : note_text_size); - const float note_font_weight = is_icon_note ? UIStyle.NormalFontWeight : note_text_weight; - const ImVec2 note_size = is_spinner ? - LayoutScale(note_font_size, note_font_size) : - (notif.note.empty() ? ImVec2() : - font->CalcTextSizeA(note_font_size, note_font_weight, FLT_MAX, - 0.0f, IMSTR_START_END(notif.note))); + GPUTexture* note_image = nullptr; + float note_font_size, note_font_weight, note_offset_y, note_spacing; + ImVec2 note_size; + switch (notif.note_type) + { + case AchievementNotificationNoteType::Text: + note_font_size = note_text_size; + note_font_weight = note_text_weight; + note_size = font->CalcTextSizeA(note_font_size, note_font_weight, FLT_MAX, 0.0f, IMSTR_START_END(notif.note)); + note_offset_y = 0.0f; + note_spacing = larger_horizontal_spacing; + break; + + case AchievementNotificationNoteType::IconText: + note_font_size = note_icon_size; + note_font_weight = UIStyle.NormalFontWeight; + note_size = font->CalcTextSizeA(note_font_size, note_font_weight, FLT_MAX, 0.0f, IMSTR_START_END(notif.note)); + note_offset_y = 0.0f; + note_spacing = note_icon_padding; + break; + + case AchievementNotificationNoteType::Spinner: + note_font_size = 0.0f; + note_font_weight = 0.0f; + note_size = ImVec2(note_text_size, note_text_size); + note_offset_y = ImFloor((note_icon_size - note_text_size) * 0.5f); + note_spacing = note_icon_padding; + break; + + case AchievementNotificationNoteType::Image: + note_font_size = 0.0f; + note_font_weight = 0.0f; + note_image = GetCachedTexture(notif.note, static_cast(note_text_size), static_cast(note_text_size)); + note_size = (note_image && note_image->GetWidth() > note_image->GetHeight()) ? + ImVec2(note_text_size * (static_cast(note_image->GetWidth()) / + static_cast(note_image->GetHeight())), + note_text_size) : + ImVec2(note_text_size, note_text_size * (static_cast(note_image->GetHeight()) / + static_cast(note_image->GetWidth()))); + note_offset_y = ImFloor((note_icon_size - note_text_size) * 0.5f); + note_spacing = note_icon_padding; + break; + + case AchievementNotificationNoteType::None: + default: + note_font_size = 0.0f; + note_font_weight = 0.0f; + note_size = ImVec2(); + note_offset_y = 0.0f; + note_spacing = 0.0f; + break; + } + const ImVec2 title_size = font->CalcTextSizeA(title_font_size, title_font_weight, max_text_width - note_size.x, max_text_width - note_size.x, IMSTR_START_END(notif.title)); const ImVec2 text_size = font->CalcTextSizeA(text_font_size, text_font_weight, max_text_width, max_text_width, IMSTR_START_END(notif.text)); - const float box_width = - std::max((horizontal_padding * 2.0f) + badge_size + horizontal_spacing + - ImCeil(std::max(title_size.x + (notif.note.empty() ? 0.0f : (larger_horizontal_spacing + note_size.x)), - text_size.x)), - std::max(LayoutScale(notif.min_width), min_width)); + const float box_width = std::max((horizontal_padding * 2.0f) + badge_size + horizontal_spacing + + ImCeil(std::max(title_size.x + note_spacing + note_size.x, text_size.x)), + std::max(static_cast(LayoutScale(notif.min_width)), min_width)); const float box_height = std::max((vertical_padding * 2.0f) + ImCeil(title_size.y) + vertical_spacing + ImCeil(text_size.y), min_height); @@ -424,18 +470,38 @@ void FullscreenUI::DrawNotifications(NotificationLayout& layout) RenderShadowedTextClipped(dl, font, text_font_size, text_font_weight, text_bb.Min, text_bb.Max, text_col, notif.text, &text_size, ImVec2(0.0f, 0.0f), max_text_width, &text_bb); - if (notif.note == Achievements::NOTIFICATION_SPINNER_NOTE) + const ImVec2 note_pos = + ImVec2((box_min.x + box_width) - horizontal_padding - note_size.x, box_min.y + vertical_padding + note_offset_y); + switch (notif.note_type) { - DrawSpinner(dl, ImVec2((box_min.x + box_width) - horizontal_padding - note_size.x, box_min.y + vertical_padding), - title_col, note_size.x, LayoutScale(4.0f)); - } - else if (!notif.note.empty()) - { - const ImVec2 note_pos = - ImVec2((box_min.x + box_width) - horizontal_padding - note_size.x, box_min.y + vertical_padding); - const ImRect note_bb = ImRect(note_pos, note_pos + note_size); - RenderShadowedTextClipped(dl, font, note_font_size, note_font_weight, note_bb.Min, note_bb.Max, title_col, - notif.note, ¬e_size, ImVec2(0.0f, 0.0f), max_text_width, ¬e_bb); + case AchievementNotificationNoteType::Text: + case AchievementNotificationNoteType::IconText: + { + const ImRect note_bb = ImRect(note_pos, note_pos + note_size); + RenderShadowedTextClipped(dl, font, note_font_size, note_font_weight, note_bb.Min, note_bb.Max, title_col, + notif.note, ¬e_size, ImVec2(0.0f, 0.0f), max_text_width, ¬e_bb); + } + break; + + case AchievementNotificationNoteType::Spinner: + { + DrawSpinner(dl, note_pos, title_col, note_size.x, LayoutScale(4.0f)); + } + break; + + case AchievementNotificationNoteType::Image: + { + if (note_image) + { + const ImRect image_rect = CenterImage(note_size, note_image); + dl->AddImage(note_image, note_pos + image_rect.Min, note_pos + image_rect.Max); + } + } + break; + + case AchievementNotificationNoteType::None: + default: + break; } ++iter; diff --git a/src/core/fullscreenui_private.h b/src/core/fullscreenui_private.h index c0953ca51..8c85d29ac 100644 --- a/src/core/fullscreenui_private.h +++ b/src/core/fullscreenui_private.h @@ -119,9 +119,20 @@ bool IsInputBindingDialogOpen(); // Achievements ////////////////////////////////////////////////////////////////////////// +enum class AchievementNotificationNoteType +{ + None, + Text, + IconText, + Spinner, + Image, +}; + /// Schedules an achievement notification to be shown. void AddAchievementNotification(std::string key, float duration, std::string image_path, std::string title, - std::string text, std::string note = {}, float min_width = 0.0f); + std::string text, std::string note = {}, + AchievementNotificationNoteType note_type = AchievementNotificationNoteType::None, + u16 min_width = 0); /// Draws ImGui overlays when ingame. void DrawAchievementsOverlays();