mirror of
https://github.com/stenzek/duckstation.git
synced 2026-04-10 16:13:06 +00:00
Achievements: Allow changing position of overlays
This commit is contained in:
@@ -11,7 +11,7 @@
|
||||
#include "core.h"
|
||||
#include "cpu_core.h"
|
||||
#include "fullscreenui.h"
|
||||
#include "fullscreenui_widgets.h"
|
||||
#include "fullscreenui_private.h"
|
||||
#include "game_list.h"
|
||||
#include "gpu_thread.h"
|
||||
#include "host.h"
|
||||
@@ -85,11 +85,6 @@ 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 CHALLENGE_INDICATOR_FADE_IN_TIME = 0.1f;
|
||||
static constexpr float CHALLENGE_INDICATOR_FADE_OUT_TIME = 0.3f;
|
||||
static constexpr float INDICATOR_FADE_IN_TIME = 0.2f;
|
||||
static constexpr float INDICATOR_FADE_OUT_TIME = 0.4f;
|
||||
|
||||
// Some API calls are really slow. Set a longer timeout.
|
||||
static constexpr float SERVER_CALL_TIMEOUT = 60.0f;
|
||||
|
||||
@@ -114,22 +109,6 @@ struct FetchGameTitlesParameters
|
||||
bool success;
|
||||
};
|
||||
|
||||
struct LeaderboardTrackerIndicator
|
||||
{
|
||||
u32 tracker_id;
|
||||
std::string text;
|
||||
float time;
|
||||
bool active;
|
||||
};
|
||||
|
||||
struct AchievementProgressIndicator
|
||||
{
|
||||
const rc_client_achievement_t* achievement;
|
||||
std::string badge_path;
|
||||
float time;
|
||||
bool active;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
static TinyString GameHashToString(const std::optional<GameHash>& hash);
|
||||
@@ -153,13 +132,11 @@ static bool IdentifyCurrentGame();
|
||||
static void BeginLoadGame();
|
||||
static void UpdateGameSummary(bool update_progress_database);
|
||||
static DynamicHeapArray<u8> SaveStateToBuffer();
|
||||
static void LoadStateFromBuffer(std::span<const u8> data);
|
||||
static void LoadStateFromBuffer(std::span<const u8> data, std::unique_lock<std::recursive_mutex>& lock);
|
||||
static bool SaveStateToBuffer(std::span<u8> data);
|
||||
static std::string GetImageURL(const char* image_name, u32 type);
|
||||
static std::string GetLocalImagePath(const std::string_view image_name, u32 type);
|
||||
static void DownloadImage(std::string url, std::string cache_path);
|
||||
static float IndicatorOpacity(float delta_time, bool active, float& opacity);
|
||||
static float IndicatorAnimation(bool active, float time, float width, float position_x, float* opacity);
|
||||
|
||||
static TinyString DecryptLoginToken(std::string_view encrypted_token, std::string_view username);
|
||||
static TinyString EncryptLoginToken(std::string_view token, std::string_view username);
|
||||
@@ -326,11 +303,21 @@ const rc_client_user_game_summary_t& Achievements::GetGameSummary()
|
||||
return s_state.game_summary;
|
||||
}
|
||||
|
||||
std::span<const Achievements::ActiveChallengeIndicator> Achievements::GetActiveChallengeIndicators()
|
||||
std::vector<Achievements::LeaderboardTrackerIndicator>& Achievements::GetLeaderboardTrackerIndicators()
|
||||
{
|
||||
return s_state.active_leaderboard_trackers;
|
||||
}
|
||||
|
||||
std::vector<Achievements::ActiveChallengeIndicator>& Achievements::GetActiveChallengeIndicators()
|
||||
{
|
||||
return s_state.active_challenge_indicators;
|
||||
}
|
||||
|
||||
std::optional<Achievements::AchievementProgressIndicator>& Achievements::GetActiveProgressIndicator()
|
||||
{
|
||||
return s_state.active_progress_indicator;
|
||||
}
|
||||
|
||||
void Achievements::ReportError(std::string_view sv)
|
||||
{
|
||||
ERROR_LOG(sv);
|
||||
@@ -737,7 +724,7 @@ void Achievements::UpdateSettings(const Settings& old_config)
|
||||
const DynamicHeapArray<u8> state_data = SaveStateToBuffer();
|
||||
ClearGameInfo();
|
||||
BeginLoadGame();
|
||||
LoadStateFromBuffer(state_data.cspan());
|
||||
LoadStateFromBuffer(state_data.cspan(), lock);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1311,14 +1298,14 @@ void Achievements::DisplayAchievementSummary()
|
||||
summary.assign(TRANSLATE_SV("Achievements", "This game has no achievements."));
|
||||
}
|
||||
|
||||
FullscreenUI::AddNotification("AchievementsSummary",
|
||||
IsHardcoreModeActive() ? ACHIEVEMENT_SUMMARY_NOTIFICATION_TIME_HC :
|
||||
ACHIEVEMENT_SUMMARY_NOTIFICATION_TIME,
|
||||
s_state.game_icon, s_state.game_title, std::string(summary), {});
|
||||
FullscreenUI::AddAchievementNotification("AchievementsSummary",
|
||||
IsHardcoreModeActive() ? ACHIEVEMENT_SUMMARY_NOTIFICATION_TIME_HC :
|
||||
ACHIEVEMENT_SUMMARY_NOTIFICATION_TIME,
|
||||
s_state.game_icon, s_state.game_title, std::string(summary), {});
|
||||
|
||||
if (s_state.game_summary.num_unsupported_achievements > 0)
|
||||
{
|
||||
FullscreenUI::AddNotification(
|
||||
FullscreenUI::AddAchievementNotification(
|
||||
"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",
|
||||
@@ -1376,10 +1363,10 @@ void Achievements::HandleUnlockEvent(const rc_client_event_t* event)
|
||||
if (cheevo->points > 0)
|
||||
note = fmt::format(ICON_EMOJI_TROPHY " {}", cheevo->points);
|
||||
|
||||
FullscreenUI::AddNotification(fmt::format("achievement_unlock_{}", cheevo->id),
|
||||
static_cast<float>(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<float>(g_settings.achievements_notification_duration),
|
||||
GetAchievementBadgePath(cheevo, false), std::move(title),
|
||||
std::string(cheevo->description), std::move(note));
|
||||
}
|
||||
|
||||
if (g_settings.achievements_sound_effects)
|
||||
@@ -1400,8 +1387,8 @@ void Achievements::HandleGameCompleteEvent(const rc_client_event_t* event)
|
||||
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::AddNotification("achievement_mastery", GAME_COMPLETE_NOTIFICATION_TIME, s_state.game_icon,
|
||||
s_state.game_title, std::move(message), std::move(note));
|
||||
FullscreenUI::AddAchievementNotification("achievement_mastery", GAME_COMPLETE_NOTIFICATION_TIME, s_state.game_icon,
|
||||
s_state.game_title, std::move(message), std::move(note));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1421,8 +1408,9 @@ void Achievements::HandleSubsetCompleteEvent(const rc_client_event_t* event)
|
||||
s_state.game_summary.num_unlocked_achievements),
|
||||
TRANSLATE_PLURAL_STR("Achievements", "%n points", "Achievement points", s_state.game_summary.points_unlocked));
|
||||
|
||||
FullscreenUI::AddNotification("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), {});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1432,7 +1420,7 @@ void Achievements::HandleLeaderboardStartedEvent(const rc_client_event_t* event)
|
||||
|
||||
if (g_settings.achievements_leaderboard_notifications)
|
||||
{
|
||||
FullscreenUI::AddNotification(
|
||||
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."), {});
|
||||
}
|
||||
@@ -1444,7 +1432,7 @@ void Achievements::HandleLeaderboardFailedEvent(const rc_client_event_t* event)
|
||||
|
||||
if (g_settings.achievements_leaderboard_notifications)
|
||||
{
|
||||
FullscreenUI::AddNotification(
|
||||
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."), {});
|
||||
}
|
||||
@@ -1469,9 +1457,10 @@ void Achievements::HandleLeaderboardSubmittedEvent(const rc_client_event_t* even
|
||||
event->leaderboard->tracker_value ? event->leaderboard->tracker_value : "Unknown",
|
||||
g_settings.achievements_spectator_mode ? std::string_view() : TRANSLATE_SV("Achievements", " (Submitting)"));
|
||||
|
||||
FullscreenUI::AddNotification(fmt::format("leaderboard_{}", event->leaderboard->id),
|
||||
static_cast<float>(g_settings.achievements_leaderboard_duration), s_state.game_icon,
|
||||
std::string(event->leaderboard->title), std::move(message), {});
|
||||
FullscreenUI::AddAchievementNotification(fmt::format("leaderboard_{}", event->leaderboard->id),
|
||||
static_cast<float>(g_settings.achievements_leaderboard_duration),
|
||||
s_state.game_icon, std::string(event->leaderboard->title),
|
||||
std::move(message), {});
|
||||
}
|
||||
|
||||
if (g_settings.achievements_sound_effects)
|
||||
@@ -1500,9 +1489,9 @@ void Achievements::HandleLeaderboardScoreboardEvent(const rc_client_event_t* eve
|
||||
event->leaderboard_scoreboard->submitted_score, event->leaderboard_scoreboard->best_score),
|
||||
event->leaderboard_scoreboard->new_rank, event->leaderboard_scoreboard->num_entries);
|
||||
|
||||
FullscreenUI::AddNotification(fmt::format("leaderboard_{}", event->leaderboard->id),
|
||||
static_cast<float>(g_settings.achievements_leaderboard_duration), s_state.game_icon,
|
||||
std::move(title), std::move(message), {});
|
||||
FullscreenUI::AddAchievementNotification(fmt::format("leaderboard_{}", event->leaderboard->id),
|
||||
static_cast<float>(g_settings.achievements_leaderboard_duration),
|
||||
s_state.game_icon, std::move(title), std::move(message), {});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1576,11 +1565,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::AddNotification(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 : ""),
|
||||
event->achievement->description, {});
|
||||
}
|
||||
|
||||
s_state.active_challenge_indicators.push_back(
|
||||
@@ -1605,11 +1594,11 @@ void Achievements::HandleAchievementChallengeIndicatorHideEvent(const rc_client_
|
||||
if (g_settings.achievements_challenge_indicator_mode == AchievementChallengeIndicatorMode::Notification &&
|
||||
event->achievement->state == RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE)
|
||||
{
|
||||
FullscreenUI::AddNotification(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 : ""),
|
||||
event->achievement->description, {});
|
||||
}
|
||||
if (g_settings.achievements_challenge_indicator_mode == AchievementChallengeIndicatorMode::Notification ||
|
||||
g_settings.achievements_challenge_indicator_mode == AchievementChallengeIndicatorMode::Disabled)
|
||||
@@ -1764,7 +1753,7 @@ void Achievements::OnHardcoreModeChanged(bool enabled, bool display_message, boo
|
||||
Host::OnAchievementsHardcoreModeChanged(enabled);
|
||||
}
|
||||
|
||||
void Achievements::LoadStateFromBuffer(std::span<const u8> data)
|
||||
void Achievements::LoadStateFromBuffer(std::span<const u8> data, std::unique_lock<std::recursive_mutex>& lock)
|
||||
{
|
||||
// if we're active, make sure we've downloaded and activated all the achievements
|
||||
// before deserializing, otherwise that state's going to get lost.
|
||||
@@ -1773,7 +1762,7 @@ void Achievements::LoadStateFromBuffer(std::span<const u8> data)
|
||||
FullscreenUI::OpenOrUpdateLoadingScreen(System::GetImageForLoadingScreen(System::GetGamePath()),
|
||||
TRANSLATE_SV("Achievements", "Downloading achievements data..."));
|
||||
|
||||
s_state.http_downloader->WaitForAllRequests();
|
||||
WaitForHTTPRequestsWithYield(lock);
|
||||
|
||||
FullscreenUI::CloseLoadingScreen();
|
||||
}
|
||||
@@ -1843,7 +1832,7 @@ bool Achievements::DoState(StateWrapper& sw)
|
||||
if (sw.HasError())
|
||||
return false;
|
||||
|
||||
LoadStateFromBuffer(data);
|
||||
LoadStateFromBuffer(data, lock);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
@@ -2066,7 +2055,7 @@ void Achievements::ClientLoginWithTokenCallback(int result, const char* error_me
|
||||
// only display user error if they've started a game
|
||||
if (System::IsValid())
|
||||
{
|
||||
FullscreenUI::AddNotification(
|
||||
FullscreenUI::AddAchievementNotification(
|
||||
"AchievementsLoginFailed", 15.0f, "images/warning.svg",
|
||||
TRANSLATE_STR("Achievements", "RetroAchievements Login Failed"),
|
||||
fmt::format(
|
||||
@@ -2111,8 +2100,8 @@ void Achievements::FinishLogin()
|
||||
std::string summary = fmt::format(TRANSLATE_FS("Achievements", "Score: {} ({} softcore)\nUnread messages: {}"),
|
||||
user->score, user->score_softcore, user->num_unread_messages);
|
||||
|
||||
FullscreenUI::AddNotification("achievements_login", LOGIN_NOTIFICATION_TIME, s_state.user_badge_path,
|
||||
user->display_name, std::move(summary), {});
|
||||
FullscreenUI::AddAchievementNotification("achievements_login", LOGIN_NOTIFICATION_TIME, s_state.user_badge_path,
|
||||
user->display_name, std::move(summary), {});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2316,231 +2305,6 @@ void Achievements::ConfirmHardcoreModeDisableAsync(std::string_view trigger, std
|
||||
});
|
||||
}
|
||||
|
||||
float Achievements::IndicatorOpacity(float delta_time, bool active, float& opacity)
|
||||
{
|
||||
float target, rate;
|
||||
if (active)
|
||||
{
|
||||
target = 1.0f;
|
||||
rate = CHALLENGE_INDICATOR_FADE_IN_TIME;
|
||||
}
|
||||
else
|
||||
{
|
||||
target = 0.0f;
|
||||
rate = -CHALLENGE_INDICATOR_FADE_OUT_TIME;
|
||||
}
|
||||
|
||||
if (opacity != target)
|
||||
opacity = ImSaturate(opacity + (delta_time / rate));
|
||||
|
||||
return opacity;
|
||||
}
|
||||
|
||||
float Achievements::IndicatorAnimation(bool active, float time, float width, float position_x, float* opacity)
|
||||
{
|
||||
static constexpr float MOVE_WIDTH = 0.3f;
|
||||
|
||||
if (active)
|
||||
{
|
||||
if (time < INDICATOR_FADE_IN_TIME)
|
||||
{
|
||||
const float pct = time / INDICATOR_FADE_IN_TIME;
|
||||
const float eased_pct = std::clamp(Easing::OutExpo(pct), 0.0f, 1.0f);
|
||||
*opacity = pct;
|
||||
return ImFloor(position_x + (width * MOVE_WIDTH * (1.0f - eased_pct)));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (time < INDICATOR_FADE_OUT_TIME)
|
||||
{
|
||||
const float pct = time / INDICATOR_FADE_OUT_TIME;
|
||||
const float eased_pct = std::clamp(Easing::InExpo(pct), 0.0f, 1.0f);
|
||||
*opacity = eased_pct;
|
||||
return ImFloor(position_x + (width * MOVE_WIDTH * (1.0f - eased_pct)));
|
||||
}
|
||||
}
|
||||
|
||||
*opacity = 1.0f;
|
||||
return position_x;
|
||||
}
|
||||
|
||||
void Achievements::DrawGameOverlays()
|
||||
{
|
||||
using FullscreenUI::DarkerColor;
|
||||
using FullscreenUI::DrawRoundedGradientRect;
|
||||
using FullscreenUI::LayoutScale;
|
||||
using FullscreenUI::ModAlpha;
|
||||
using FullscreenUI::RenderShadowedTextClipped;
|
||||
using FullscreenUI::UIStyle;
|
||||
|
||||
static constexpr const float& font_size = UIStyle.MediumFontSize;
|
||||
static constexpr const float& font_weight = UIStyle.BoldFontWeight;
|
||||
|
||||
static constexpr float bg_opacity = 0.8f;
|
||||
|
||||
if (!HasActiveGame())
|
||||
return;
|
||||
|
||||
const auto lock = GetLock();
|
||||
|
||||
const float margin =
|
||||
std::max(ImCeil(ImGuiManager::GetScreenMargin() * ImGuiManager::GetGlobalScale()), LayoutScale(10.0f));
|
||||
const float spacing = LayoutScale(10.0f);
|
||||
const float padding = LayoutScale(10.0f);
|
||||
const float rounding = LayoutScale(10.0f);
|
||||
const ImVec2 image_size = LayoutScale(50.0f, 50.0f);
|
||||
const ImGuiIO& io = ImGui::GetIO();
|
||||
ImVec2 position = ImVec2(io.DisplaySize.x - margin, io.DisplaySize.y - margin);
|
||||
ImDrawList* dl = ImGui::GetBackgroundDrawList();
|
||||
|
||||
if (!s_state.active_challenge_indicators.empty() &&
|
||||
(g_settings.achievements_challenge_indicator_mode == AchievementChallengeIndicatorMode::PersistentIcon ||
|
||||
g_settings.achievements_challenge_indicator_mode == AchievementChallengeIndicatorMode::TemporaryIcon))
|
||||
{
|
||||
const bool use_time_remaining =
|
||||
(g_settings.achievements_challenge_indicator_mode == AchievementChallengeIndicatorMode::TemporaryIcon);
|
||||
const float x_advance = image_size.x + spacing;
|
||||
ImVec2 current_position = ImVec2(position.x - image_size.x, position.y - image_size.y);
|
||||
|
||||
for (auto it = s_state.active_challenge_indicators.begin(); it != s_state.active_challenge_indicators.end();)
|
||||
{
|
||||
ActiveChallengeIndicator& indicator = *it;
|
||||
bool active = indicator.active;
|
||||
if (use_time_remaining)
|
||||
{
|
||||
indicator.time_remaining = std::max(indicator.time_remaining - io.DeltaTime, 0.0f);
|
||||
active = (indicator.time_remaining > 0.0f);
|
||||
}
|
||||
|
||||
const float opacity = IndicatorOpacity(io.DeltaTime, active, indicator.opacity);
|
||||
|
||||
GPUTexture* badge = FullscreenUI::GetCachedTextureAsync(indicator.badge_path);
|
||||
if (badge)
|
||||
{
|
||||
dl->AddImage(badge, current_position, current_position + image_size, ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f),
|
||||
ImGui::GetColorU32(ImVec4(1.0f, 1.0f, 1.0f, opacity)));
|
||||
current_position.x -= x_advance;
|
||||
}
|
||||
|
||||
if (!indicator.active && opacity <= 0.01f)
|
||||
{
|
||||
DEV_LOG("Remove challenge indicator");
|
||||
it = s_state.active_challenge_indicators.erase(it);
|
||||
}
|
||||
else
|
||||
{
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
position.y -= image_size.y + padding;
|
||||
}
|
||||
|
||||
if (s_state.active_progress_indicator.has_value())
|
||||
{
|
||||
AchievementProgressIndicator& indicator = s_state.active_progress_indicator.value();
|
||||
indicator.time += indicator.active ? io.DeltaTime : -io.DeltaTime;
|
||||
|
||||
const ImVec4 left_background_color = DarkerColor(UIStyle.ToastBackgroundColor, 1.3f);
|
||||
const ImVec4 right_background_color = DarkerColor(UIStyle.ToastBackgroundColor, 0.8f);
|
||||
const ImVec2 progress_image_size = LayoutScale(32.0f, 32.0f);
|
||||
const std::string_view text = s_state.active_progress_indicator->achievement->measured_progress;
|
||||
const ImVec2 text_size = UIStyle.Font->CalcTextSizeA(font_size, font_weight, FLT_MAX, 0.0f, IMSTR_START_END(text));
|
||||
const float box_width = progress_image_size.x + text_size.x + spacing + padding * 2.0f;
|
||||
|
||||
float opacity;
|
||||
const ImVec2 box_min =
|
||||
ImVec2(IndicatorAnimation(indicator.active, indicator.time, box_width, position.x - box_width, &opacity),
|
||||
position.y - progress_image_size.y - padding * 2.0f);
|
||||
const ImVec2 box_max = position;
|
||||
|
||||
DrawRoundedGradientRect(dl, box_min, box_max,
|
||||
ImGui::GetColorU32(ModAlpha(left_background_color, opacity * bg_opacity)),
|
||||
ImGui::GetColorU32(ModAlpha(right_background_color, opacity * bg_opacity)), rounding);
|
||||
|
||||
GPUTexture* const badge = FullscreenUI::GetCachedTextureAsync(indicator.badge_path);
|
||||
if (badge)
|
||||
{
|
||||
const ImVec2 badge_pos = box_min + ImVec2(padding, padding);
|
||||
dl->AddImage(badge, badge_pos, badge_pos + progress_image_size, ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f),
|
||||
ImGui::GetColorU32(ImVec4(1.0f, 1.0f, 1.0f, opacity)));
|
||||
}
|
||||
|
||||
const ImVec2 text_pos =
|
||||
box_min + ImVec2(padding + progress_image_size.x + spacing, (box_max.y - box_min.y - text_size.y) * 0.5f);
|
||||
const ImRect text_clip_rect(text_pos, box_max);
|
||||
RenderShadowedTextClipped(dl, UIStyle.Font, font_size, font_weight, text_pos, box_max,
|
||||
ImGui::GetColorU32(ModAlpha(UIStyle.ToastTextColor, opacity)), text, &text_size,
|
||||
ImVec2(0.0f, 0.0f), 0.0f, &text_clip_rect);
|
||||
|
||||
if (!indicator.active && opacity <= 0.01f)
|
||||
{
|
||||
DEV_LOG("Remove progress indicator");
|
||||
s_state.active_progress_indicator.reset();
|
||||
}
|
||||
|
||||
position.y -= progress_image_size.y + padding * 3.0f;
|
||||
}
|
||||
|
||||
if (!s_state.active_leaderboard_trackers.empty())
|
||||
{
|
||||
const ImVec4 left_background_color = DarkerColor(UIStyle.ToastBackgroundColor, 1.3f);
|
||||
const ImVec4 right_background_color = DarkerColor(UIStyle.ToastBackgroundColor, 0.8f);
|
||||
TinyString tstr;
|
||||
|
||||
for (auto it = s_state.active_leaderboard_trackers.begin(); it != s_state.active_leaderboard_trackers.end();)
|
||||
{
|
||||
LeaderboardTrackerIndicator& indicator = *it;
|
||||
indicator.time += indicator.active ? io.DeltaTime : -io.DeltaTime;
|
||||
|
||||
tstr.assign(ICON_FA_STOPWATCH " ");
|
||||
for (u32 i = 0; i < indicator.text.length(); i++)
|
||||
{
|
||||
// 8 is typically the widest digit
|
||||
if (indicator.text[i] >= '0' && indicator.text[i] <= '9')
|
||||
tstr.append('8');
|
||||
else
|
||||
tstr.append(indicator.text[i]);
|
||||
}
|
||||
|
||||
const ImVec2 size = UIStyle.Font->CalcTextSizeA(font_size, font_weight, FLT_MAX, 0.0f, IMSTR_START_END(tstr));
|
||||
const float box_width = size.x + padding * 2.0f;
|
||||
float opacity;
|
||||
const ImRect box(
|
||||
ImVec2(IndicatorAnimation(indicator.active, indicator.time, box_width, position.x - box_width, &opacity),
|
||||
position.y - size.y - padding * 2.0f),
|
||||
position);
|
||||
|
||||
DrawRoundedGradientRect(dl, box.Min, box.Max,
|
||||
ImGui::GetColorU32(ModAlpha(left_background_color, opacity * bg_opacity)),
|
||||
ImGui::GetColorU32(ModAlpha(right_background_color, opacity * bg_opacity)), rounding);
|
||||
|
||||
tstr.format(ICON_FA_STOPWATCH " {}", indicator.text);
|
||||
|
||||
const u32 text_col = ImGui::GetColorU32(ModAlpha(UIStyle.ToastTextColor, opacity));
|
||||
RenderShadowedTextClipped(dl, UIStyle.Font, font_size, font_weight,
|
||||
ImVec2(box.Min.x + padding, box.Min.y + padding), box.Max, text_col, tstr, nullptr,
|
||||
ImVec2(0.0f, 0.0f), 0.0f, &box);
|
||||
|
||||
if (!indicator.active && opacity <= 0.01f)
|
||||
{
|
||||
DEV_LOG("Remove tracker indicator");
|
||||
it = s_state.active_leaderboard_trackers.erase(it);
|
||||
}
|
||||
else
|
||||
{
|
||||
++it;
|
||||
}
|
||||
|
||||
position.x = box.Min.x - padding;
|
||||
}
|
||||
|
||||
// Uncomment if there are any other overlays above this one.
|
||||
// position.y -= image_size.y - padding * 3.0f;
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include "common/windows_headers.h"
|
||||
#elif !defined(__ANDROID__)
|
||||
|
||||
@@ -192,9 +192,6 @@ bool DownloadGameIcons(ProgressCallback* progress, Error* error);
|
||||
/// Returns 0 if pausing is allowed, otherwise the number of frames until pausing is allowed.
|
||||
u32 GetPauseThrottleFrames();
|
||||
|
||||
/// Draws ImGui overlays when not paused.
|
||||
void DrawGameOverlays();
|
||||
|
||||
/// The name of the RetroAchievements icon, which can be used in notifications.
|
||||
extern const char* const RA_LOGO_ICON_NAME;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin <stenzek@gmail.com>
|
||||
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
|
||||
|
||||
#pragma once
|
||||
@@ -9,9 +9,21 @@
|
||||
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Achievements {
|
||||
|
||||
static constexpr float INDICATOR_FADE_IN_TIME = 0.2f;
|
||||
static constexpr float INDICATOR_FADE_OUT_TIME = 0.4f;
|
||||
|
||||
struct LeaderboardTrackerIndicator
|
||||
{
|
||||
u32 tracker_id;
|
||||
std::string text;
|
||||
float time;
|
||||
bool active;
|
||||
};
|
||||
|
||||
struct ActiveChallengeIndicator
|
||||
{
|
||||
const rc_client_achievement_t* achievement;
|
||||
@@ -21,12 +33,22 @@ struct ActiveChallengeIndicator
|
||||
bool active;
|
||||
};
|
||||
|
||||
struct AchievementProgressIndicator
|
||||
{
|
||||
const rc_client_achievement_t* achievement;
|
||||
std::string badge_path;
|
||||
float time;
|
||||
bool active;
|
||||
};
|
||||
|
||||
/// Returns the rc_client instance. Should have the lock held.
|
||||
rc_client_t* GetClient();
|
||||
|
||||
const rc_client_user_game_summary_t& GetGameSummary();
|
||||
|
||||
std::span<const ActiveChallengeIndicator> GetActiveChallengeIndicators();
|
||||
std::vector<LeaderboardTrackerIndicator>& GetLeaderboardTrackerIndicators();
|
||||
std::vector<ActiveChallengeIndicator>& GetActiveChallengeIndicators();
|
||||
std::optional<AchievementProgressIndicator>& GetActiveProgressIndicator();
|
||||
|
||||
std::string GetAchievementBadgePath(const rc_client_achievement_t* achievement, bool locked,
|
||||
bool download_if_missing = true);
|
||||
|
||||
@@ -483,14 +483,8 @@ void FullscreenUI::Shutdown(bool preserve_fsui_state)
|
||||
|
||||
void FullscreenUI::Render()
|
||||
{
|
||||
UploadAsyncTextures();
|
||||
|
||||
if (!s_locals.initialized)
|
||||
{
|
||||
// achievement overlays still need to get drawn
|
||||
Achievements::DrawGameOverlays();
|
||||
return;
|
||||
}
|
||||
|
||||
// draw background before any overlays
|
||||
if (!GPUThread::HasGPUBackend() && s_locals.current_main_window != MainWindowType::None)
|
||||
@@ -498,10 +492,6 @@ void FullscreenUI::Render()
|
||||
|
||||
BeginLayout();
|
||||
|
||||
// Primed achievements must come first, because we don't want the pause screen to be behind them.
|
||||
if (s_locals.current_main_window == MainWindowType::None)
|
||||
Achievements::DrawGameOverlays();
|
||||
|
||||
switch (s_locals.current_main_window)
|
||||
{
|
||||
case MainWindowType::Landing:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin <stenzek@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin <stenzek@gmail.com>
|
||||
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
|
||||
|
||||
#include "achievements_private.h"
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "util/translation.h"
|
||||
|
||||
#include "common/assert.h"
|
||||
#include "common/easing.h"
|
||||
#include "common/log.h"
|
||||
#include "common/string_util.h"
|
||||
#include "common/time_helpers.h"
|
||||
@@ -37,8 +38,29 @@ static constexpr u32 LEADERBOARD_ALL_FETCH_SIZE = 20;
|
||||
// How long the last progress update is shown in the pause menu.
|
||||
static constexpr float PAUSE_MENU_PROGRESS_DISPLAY_TIME = 60.0f;
|
||||
|
||||
// Notification animation times.
|
||||
static constexpr float NOTIFICATION_APPEAR_ANIMATION_TIME = 0.2f;
|
||||
static constexpr float NOTIFICATION_DISAPPEAR_ANIMATION_TIME = 0.5f;
|
||||
|
||||
static constexpr float CHALLENGE_INDICATOR_FADE_IN_TIME = 0.1f;
|
||||
static constexpr float CHALLENGE_INDICATOR_FADE_OUT_TIME = 0.3f;
|
||||
|
||||
namespace {
|
||||
|
||||
struct Notification
|
||||
{
|
||||
std::string key;
|
||||
std::string title;
|
||||
std::string text;
|
||||
std::string note;
|
||||
std::string badge_path;
|
||||
u64 start_time;
|
||||
u64 move_time;
|
||||
float duration;
|
||||
float target_y;
|
||||
float last_y;
|
||||
};
|
||||
|
||||
struct PauseMenuAchievementInfo
|
||||
{
|
||||
std::string title;
|
||||
@@ -66,6 +88,10 @@ struct PauseMenuTimedMeasuredAchievementInfo : PauseMenuMeasuredAchievementInfo
|
||||
|
||||
} // namespace
|
||||
|
||||
static void DrawNotifications(NotificationLayout& layout);
|
||||
static void DrawIndicators(NotificationLayout& layout);
|
||||
static void UpdateAchievementOverlaysRunIdle();
|
||||
|
||||
static void AddSubsetInfo(const rc_client_subset_t* subset);
|
||||
static bool IsCoreSubsetOpen();
|
||||
static void SetCurrentSubsetID(u32 subset_id);
|
||||
@@ -111,6 +137,8 @@ struct SubsetInfo
|
||||
|
||||
struct AchievementsLocals
|
||||
{
|
||||
std::vector<Notification> notifications;
|
||||
|
||||
// Shared by both achievements and leaderboards, TODO: add all filter
|
||||
std::vector<SubsetInfo> subset_info_list;
|
||||
const SubsetInfo* open_subset = nullptr;
|
||||
@@ -149,6 +177,8 @@ void FullscreenUI::ClearAchievementsState()
|
||||
|
||||
CloseLeaderboard();
|
||||
|
||||
s_achievements_locals.notifications = {};
|
||||
|
||||
s_achievements_locals.achievement_badge_paths = {};
|
||||
|
||||
s_achievements_locals.leaderboard_user_icon_paths = {};
|
||||
@@ -171,6 +201,448 @@ void FullscreenUI::ClearAchievementsState()
|
||||
|
||||
s_achievements_locals.most_recent_unlock.reset();
|
||||
s_achievements_locals.achievement_nearest_completion.reset();
|
||||
|
||||
UpdateAchievementOverlaysRunIdle();
|
||||
}
|
||||
|
||||
void FullscreenUI::DrawAchievementsOverlays()
|
||||
{
|
||||
if (!Achievements::IsActive())
|
||||
return;
|
||||
|
||||
const auto lock = Achievements::GetLock();
|
||||
|
||||
NotificationLayout layout(g_settings.achievements_notification_location);
|
||||
DrawNotifications(layout);
|
||||
|
||||
if (Achievements::HasActiveGame())
|
||||
{
|
||||
// need to group them together if they're in the same location
|
||||
if (g_settings.achievements_indicator_location != layout.GetLocation())
|
||||
layout = NotificationLayout(g_settings.achievements_indicator_location);
|
||||
|
||||
DrawIndicators(layout);
|
||||
}
|
||||
}
|
||||
|
||||
void FullscreenUI::AddAchievementNotification(std::string key, float duration, std::string image_path,
|
||||
std::string title, std::string text, std::string note)
|
||||
{
|
||||
const bool prev_had_notifications = s_achievements_locals.notifications.empty();
|
||||
const Timer::Value current_time = Timer::GetCurrentValue();
|
||||
|
||||
if (!key.empty())
|
||||
{
|
||||
for (auto it = s_achievements_locals.notifications.begin(); it != s_achievements_locals.notifications.end(); ++it)
|
||||
{
|
||||
if (it->key == key)
|
||||
{
|
||||
it->duration = duration;
|
||||
it->title = std::move(title);
|
||||
it->text = std::move(text);
|
||||
it->note = std::move(note);
|
||||
it->badge_path = std::move(image_path);
|
||||
|
||||
// Don't fade it in again
|
||||
const float time_passed = static_cast<float>(Timer::ConvertValueToSeconds(current_time - it->start_time));
|
||||
it->start_time =
|
||||
current_time - Timer::ConvertSecondsToValue(std::min(time_passed, NOTIFICATION_APPEAR_ANIMATION_TIME));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Notification notif;
|
||||
notif.key = std::move(key);
|
||||
notif.duration = duration;
|
||||
notif.title = std::move(title);
|
||||
notif.text = std::move(text);
|
||||
notif.note = std::move(note);
|
||||
notif.badge_path = std::move(image_path);
|
||||
notif.start_time = current_time;
|
||||
notif.move_time = current_time;
|
||||
notif.target_y = -1.0f;
|
||||
notif.last_y = -1.0f;
|
||||
s_achievements_locals.notifications.push_back(std::move(notif));
|
||||
|
||||
if (!prev_had_notifications)
|
||||
UpdateAchievementOverlaysRunIdle();
|
||||
}
|
||||
|
||||
void FullscreenUI::DrawNotifications(NotificationLayout& layout)
|
||||
{
|
||||
if (s_achievements_locals.notifications.empty())
|
||||
return;
|
||||
|
||||
static constexpr float MOVE_DURATION = 0.5f;
|
||||
const Timer::Value current_time = Timer::GetCurrentValue();
|
||||
|
||||
const float horizontal_padding = FullscreenUI::LayoutScale(20.0f);
|
||||
const float vertical_padding = FullscreenUI::LayoutScale(15.0f);
|
||||
const float horizontal_spacing = FullscreenUI::LayoutScale(10.0f);
|
||||
const float larger_horizontal_spacing = FullscreenUI::LayoutScale(18.0f);
|
||||
const float vertical_spacing = FullscreenUI::LayoutScale(4.0f);
|
||||
const float badge_size = FullscreenUI::LayoutScale(48.0f);
|
||||
const float min_width = FullscreenUI::LayoutScale(200.0f);
|
||||
const float max_width = FullscreenUI::LayoutScale(600.0f);
|
||||
const float max_text_width = max_width - badge_size - (horizontal_padding * 2.0f) - horizontal_spacing;
|
||||
const float min_height = (vertical_padding * 2.0f) + badge_size;
|
||||
const float rounding = FullscreenUI::LayoutScale(20.0f);
|
||||
const float min_rounded_width = rounding * 2.0f;
|
||||
|
||||
ImFont*& font = UIStyle.Font;
|
||||
const float& title_font_size = UIStyle.LargeFontSize;
|
||||
const float& title_font_weight = UIStyle.BoldFontWeight;
|
||||
const float& text_font_size = UIStyle.MediumFontSize;
|
||||
const float& text_font_weight = UIStyle.NormalFontWeight;
|
||||
const float& note_font_size = UIStyle.MediumFontSize;
|
||||
const float& note_font_weight = UIStyle.BoldFontWeight;
|
||||
|
||||
const ImVec4 left_background_color = DarkerColor(UIStyle.ToastBackgroundColor, 1.3f);
|
||||
const ImVec4 right_background_color = DarkerColor(UIStyle.ToastBackgroundColor, 0.8f);
|
||||
ImDrawList* const dl = ImGui::GetForegroundDrawList();
|
||||
|
||||
for (auto iter = s_achievements_locals.notifications.begin(); iter != s_achievements_locals.notifications.end();)
|
||||
{
|
||||
Notification& notif = *iter;
|
||||
const float time_passed = static_cast<float>(Timer::ConvertValueToSeconds(current_time - notif.start_time));
|
||||
if (time_passed >= notif.duration)
|
||||
{
|
||||
iter = s_achievements_locals.notifications.erase(iter);
|
||||
continue;
|
||||
}
|
||||
|
||||
// place note to the right of the title
|
||||
const ImVec2 note_size = notif.note.empty() ? ImVec2() :
|
||||
font->CalcTextSizeA(note_font_size, note_font_weight, FLT_MAX, 0.0f,
|
||||
IMSTR_START_END(notif.note));
|
||||
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)),
|
||||
min_width);
|
||||
const float box_height =
|
||||
std::max((vertical_padding * 2.0f) + ImCeil(title_size.y) + vertical_spacing + ImCeil(text_size.y), min_height);
|
||||
|
||||
const auto& [expected_pos, opacity] =
|
||||
layout.GetNextPosition(box_width, box_height, time_passed, notif.duration, NOTIFICATION_APPEAR_ANIMATION_TIME,
|
||||
NOTIFICATION_DISAPPEAR_ANIMATION_TIME, 0.2f);
|
||||
|
||||
float actual_y;
|
||||
if (!layout.IsVerticalAnimation() || opacity == 1.0f)
|
||||
{
|
||||
actual_y = notif.last_y;
|
||||
if (notif.target_y != expected_pos.y)
|
||||
{
|
||||
notif.move_time = current_time;
|
||||
notif.target_y = expected_pos.y;
|
||||
notif.last_y = (notif.last_y < 0.0f) ? expected_pos.y : notif.last_y;
|
||||
actual_y = notif.last_y;
|
||||
}
|
||||
else if (actual_y != expected_pos.y)
|
||||
{
|
||||
const float time_since_move = static_cast<float>(Timer::ConvertValueToSeconds(current_time - notif.move_time));
|
||||
if (time_since_move >= MOVE_DURATION)
|
||||
{
|
||||
notif.move_time = current_time;
|
||||
notif.last_y = notif.target_y;
|
||||
actual_y = notif.last_y;
|
||||
}
|
||||
else
|
||||
{
|
||||
const float frac = Easing::OutExpo(time_since_move / MOVE_DURATION);
|
||||
actual_y = notif.last_y - ((notif.last_y - notif.target_y) * frac);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
actual_y = expected_pos.y;
|
||||
}
|
||||
|
||||
const ImVec2 box_min(expected_pos.x, actual_y);
|
||||
const ImVec2 box_max(box_min.x + box_width, box_min.y + box_height);
|
||||
const float background_opacity = opacity * 0.95f;
|
||||
|
||||
DrawRoundedGradientRect(
|
||||
dl, box_min, box_max, ImGui::GetColorU32(ModAlpha(left_background_color, background_opacity)),
|
||||
ImGui::GetColorU32(ModAlpha(ImLerp(left_background_color, right_background_color,
|
||||
(box_width - min_rounded_width) / (max_width - min_rounded_width)),
|
||||
background_opacity)),
|
||||
rounding);
|
||||
|
||||
const ImVec2 badge_min(box_min.x + horizontal_padding, box_min.y + vertical_padding);
|
||||
const ImVec2 badge_max(badge_min.x + badge_size, badge_min.y + badge_size);
|
||||
if (!notif.badge_path.empty())
|
||||
{
|
||||
GPUTexture* tex = GetCachedTexture(notif.badge_path, static_cast<u32>(badge_size), static_cast<u32>(badge_size));
|
||||
if (tex)
|
||||
{
|
||||
dl->AddImage(tex, badge_min, badge_max, ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f),
|
||||
ImGui::GetColorU32(ImVec4(1.0f, 1.0f, 1.0f, opacity)));
|
||||
}
|
||||
}
|
||||
|
||||
const u32 title_col = ImGui::GetColorU32(ModAlpha(UIStyle.ToastTextColor, opacity));
|
||||
const u32 text_col = ImGui::GetColorU32(ModAlpha(DarkerColor(UIStyle.ToastTextColor), opacity));
|
||||
|
||||
const ImVec2 title_pos = ImVec2(badge_max.x + horizontal_spacing, box_min.y + vertical_padding);
|
||||
const ImRect title_bb = ImRect(title_pos, title_pos + title_size);
|
||||
RenderShadowedTextClipped(dl, font, title_font_size, title_font_weight, title_bb.Min, title_bb.Max, title_col,
|
||||
notif.title, &title_size, ImVec2(0.0f, 0.0f), max_text_width - note_size.x, &title_bb);
|
||||
|
||||
const ImVec2 text_pos = ImVec2(badge_max.x + horizontal_spacing, title_bb.Max.y + vertical_spacing);
|
||||
const ImRect text_bb = ImRect(text_pos, text_pos + text_size);
|
||||
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.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);
|
||||
}
|
||||
|
||||
++iter;
|
||||
}
|
||||
|
||||
// cleared?
|
||||
if (s_achievements_locals.notifications.empty())
|
||||
GPUThread::SetRunIdleReason(GPUThread::RunIdleReason::AchievementOverlaysActive, false);
|
||||
}
|
||||
|
||||
void FullscreenUI::DrawIndicators(NotificationLayout& layout)
|
||||
{
|
||||
static constexpr float INDICATOR_WIDTH_COEFF = 0.3f;
|
||||
|
||||
static constexpr const float& font_size = UIStyle.MediumFontSize;
|
||||
static constexpr const float& font_weight = UIStyle.BoldFontWeight;
|
||||
|
||||
static constexpr float bg_opacity = 0.8f;
|
||||
|
||||
const float spacing = LayoutScale(10.0f);
|
||||
const float padding = LayoutScale(10.0f);
|
||||
const float rounding = LayoutScale(10.0f);
|
||||
const ImVec2 image_size = LayoutScale(50.0f, 50.0f);
|
||||
const ImGuiIO& io = ImGui::GetIO();
|
||||
ImDrawList* dl = ImGui::GetBackgroundDrawList();
|
||||
|
||||
if (std::vector<Achievements::ActiveChallengeIndicator>& indicators = Achievements::GetActiveChallengeIndicators();
|
||||
!indicators.empty() &&
|
||||
(g_settings.achievements_challenge_indicator_mode == AchievementChallengeIndicatorMode::PersistentIcon ||
|
||||
g_settings.achievements_challenge_indicator_mode == AchievementChallengeIndicatorMode::TemporaryIcon))
|
||||
{
|
||||
const bool use_time_remaining =
|
||||
(g_settings.achievements_challenge_indicator_mode == AchievementChallengeIndicatorMode::TemporaryIcon);
|
||||
const float x_advance = image_size.x + spacing;
|
||||
const float total_width = image_size.x + (static_cast<float>(indicators.size() - 1) * x_advance);
|
||||
ImVec2 current_position = layout.GetFixedPosition(total_width, image_size.y);
|
||||
|
||||
for (auto it = indicators.begin(); it != indicators.end();)
|
||||
{
|
||||
Achievements::ActiveChallengeIndicator& indicator = *it;
|
||||
bool active = indicator.active;
|
||||
if (use_time_remaining)
|
||||
{
|
||||
indicator.time_remaining = std::max(indicator.time_remaining - io.DeltaTime, 0.0f);
|
||||
active = (indicator.time_remaining > 0.0f);
|
||||
}
|
||||
|
||||
const float target_opacity = active ? 1.0f : 0.0f;
|
||||
const float rate = active ? CHALLENGE_INDICATOR_FADE_IN_TIME : -CHALLENGE_INDICATOR_FADE_OUT_TIME;
|
||||
indicator.opacity =
|
||||
(indicator.opacity != target_opacity) ? ImSaturate(indicator.opacity + (io.DeltaTime / rate)) : target_opacity;
|
||||
|
||||
GPUTexture* badge = FullscreenUI::GetCachedTextureAsync(indicator.badge_path);
|
||||
if (badge)
|
||||
{
|
||||
dl->AddImage(badge, current_position, current_position + image_size, ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f),
|
||||
ImGui::GetColorU32(ImVec4(1.0f, 1.0f, 1.0f, indicator.opacity)));
|
||||
}
|
||||
|
||||
current_position.x += x_advance;
|
||||
|
||||
if (!indicator.active && indicator.opacity <= 0.01f)
|
||||
{
|
||||
DEV_LOG("Remove challenge indicator");
|
||||
it = indicators.erase(it);
|
||||
}
|
||||
else
|
||||
{
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (std::optional<Achievements::AchievementProgressIndicator>& indicator = Achievements::GetActiveProgressIndicator();
|
||||
indicator.has_value())
|
||||
{
|
||||
indicator->time += indicator->active ? io.DeltaTime : -io.DeltaTime;
|
||||
|
||||
const ImVec4 left_background_color = DarkerColor(UIStyle.ToastBackgroundColor, 1.3f);
|
||||
const ImVec4 right_background_color = DarkerColor(UIStyle.ToastBackgroundColor, 0.8f);
|
||||
const ImVec2 progress_image_size = LayoutScale(32.0f, 32.0f);
|
||||
const std::string_view text = indicator->achievement->measured_progress;
|
||||
const ImVec2 text_size = UIStyle.Font->CalcTextSizeA(font_size, font_weight, FLT_MAX, 0.0f, IMSTR_START_END(text));
|
||||
const float box_width = progress_image_size.x + text_size.x + spacing + padding * 2.0f;
|
||||
const float box_height = progress_image_size.y + padding * 2.0f;
|
||||
|
||||
const auto& [box_min, opacity] = layout.GetNextPosition(
|
||||
box_width, box_height, indicator->active, indicator->time, Achievements::INDICATOR_FADE_IN_TIME,
|
||||
Achievements::INDICATOR_FADE_OUT_TIME, INDICATOR_WIDTH_COEFF);
|
||||
const ImVec2 box_max = box_min + ImVec2(box_width, box_height);
|
||||
|
||||
DrawRoundedGradientRect(dl, box_min, box_max,
|
||||
ImGui::GetColorU32(ModAlpha(left_background_color, opacity * bg_opacity)),
|
||||
ImGui::GetColorU32(ModAlpha(right_background_color, opacity * bg_opacity)), rounding);
|
||||
|
||||
GPUTexture* const badge = FullscreenUI::GetCachedTextureAsync(indicator->badge_path);
|
||||
if (badge)
|
||||
{
|
||||
const ImVec2 badge_pos = box_min + ImVec2(padding, padding);
|
||||
dl->AddImage(badge, badge_pos, badge_pos + progress_image_size, ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f),
|
||||
ImGui::GetColorU32(ImVec4(1.0f, 1.0f, 1.0f, opacity)));
|
||||
}
|
||||
|
||||
const ImVec2 text_pos =
|
||||
box_min + ImVec2(padding + progress_image_size.x + spacing, (box_max.y - box_min.y - text_size.y) * 0.5f);
|
||||
const ImRect text_clip_rect(text_pos, box_max);
|
||||
RenderShadowedTextClipped(dl, UIStyle.Font, font_size, font_weight, text_pos, box_max,
|
||||
ImGui::GetColorU32(ModAlpha(UIStyle.ToastTextColor, opacity)), text, &text_size,
|
||||
ImVec2(0.0f, 0.0f), 0.0f, &text_clip_rect);
|
||||
|
||||
if (!indicator->active && opacity <= 0.01f)
|
||||
{
|
||||
DEV_LOG("Remove progress indicator");
|
||||
indicator.reset();
|
||||
}
|
||||
}
|
||||
|
||||
ImVec2 position;
|
||||
if (std::vector<Achievements::LeaderboardTrackerIndicator>& trackers =
|
||||
Achievements::GetLeaderboardTrackerIndicators();
|
||||
!trackers.empty())
|
||||
{
|
||||
const ImVec4 left_background_color = DarkerColor(UIStyle.ToastBackgroundColor, 1.3f);
|
||||
const ImVec4 right_background_color = DarkerColor(UIStyle.ToastBackgroundColor, 0.8f);
|
||||
TinyString tstr;
|
||||
|
||||
const auto measure_tracker = [&tstr](const Achievements::LeaderboardTrackerIndicator& indicator) {
|
||||
tstr.assign(ICON_FA_STOPWATCH " ");
|
||||
for (u32 i = 0; i < indicator.text.length(); i++)
|
||||
{
|
||||
// 8 is typically the widest digit
|
||||
if (indicator.text[i] >= '0' && indicator.text[i] <= '9')
|
||||
tstr.append('8');
|
||||
else
|
||||
tstr.append(indicator.text[i]);
|
||||
}
|
||||
|
||||
return UIStyle.Font->CalcTextSizeA(font_size, font_weight, FLT_MAX, 0.0f, IMSTR_START_END(tstr));
|
||||
};
|
||||
|
||||
const auto draw_tracker = [&padding, &rounding, &dl, &left_background_color, &right_background_color, &tstr,
|
||||
&measure_tracker](Achievements::LeaderboardTrackerIndicator& indicator,
|
||||
const ImVec2& pos, float opacity) {
|
||||
const ImVec2 size = measure_tracker(indicator);
|
||||
const float box_width = size.x + padding * 2.0f;
|
||||
const float box_height = size.y + padding * 2.0f;
|
||||
const ImRect box(pos, ImVec2(pos.x + box_width, pos.y + box_height));
|
||||
|
||||
DrawRoundedGradientRect(dl, box.Min, box.Max,
|
||||
ImGui::GetColorU32(ModAlpha(left_background_color, opacity * bg_opacity)),
|
||||
ImGui::GetColorU32(ModAlpha(right_background_color, opacity * bg_opacity)), rounding);
|
||||
|
||||
tstr.format(ICON_FA_STOPWATCH " {}", indicator.text);
|
||||
|
||||
const u32 text_col = ImGui::GetColorU32(ModAlpha(UIStyle.ToastTextColor, opacity));
|
||||
RenderShadowedTextClipped(dl, UIStyle.Font, font_size, font_weight,
|
||||
ImVec2(box.Min.x + padding, box.Min.y + padding), box.Max, text_col, tstr, nullptr,
|
||||
ImVec2(0.0f, 0.0f), 0.0f, &box);
|
||||
|
||||
return size.x;
|
||||
};
|
||||
|
||||
// animations are not currently handled for more than one tracker... but this should be rare
|
||||
if (trackers.size() > 1)
|
||||
{
|
||||
float total_width = 0.0f;
|
||||
float max_height = 0.0f;
|
||||
for (const Achievements::LeaderboardTrackerIndicator& indicator : trackers)
|
||||
{
|
||||
const ImVec2 size = measure_tracker(indicator);
|
||||
total_width += ((total_width > 0.0f) ? spacing : 0.0f) + size.x + padding * 2.0f;
|
||||
max_height = std::max(max_height, size.y);
|
||||
}
|
||||
|
||||
ImVec2 current_pos = layout.GetFixedPosition(total_width, max_height + padding * 2.0f);
|
||||
for (auto it = trackers.begin(); it != trackers.end();)
|
||||
{
|
||||
Achievements::LeaderboardTrackerIndicator& indicator = *it;
|
||||
indicator.time += indicator.active ? io.DeltaTime : -io.DeltaTime;
|
||||
|
||||
current_pos.x += draw_tracker(indicator, current_pos, 1.0f);
|
||||
|
||||
if (!indicator.active)
|
||||
{
|
||||
DEV_LOG("Remove tracker indicator");
|
||||
it = trackers.erase(it);
|
||||
}
|
||||
else
|
||||
{
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// don't need to precalc size here either :D
|
||||
Achievements::LeaderboardTrackerIndicator& indicator = trackers.front();
|
||||
indicator.time += indicator.active ? io.DeltaTime : -io.DeltaTime;
|
||||
|
||||
const ImVec2 size = measure_tracker(indicator);
|
||||
const float box_width = size.x + padding * 2.0f;
|
||||
const float box_height = size.y + padding * 2.0f;
|
||||
const auto& [box_pos, opacity] = layout.GetNextPosition(
|
||||
box_width, box_height, indicator.active, indicator.time, Achievements::INDICATOR_FADE_IN_TIME,
|
||||
Achievements::INDICATOR_FADE_OUT_TIME, INDICATOR_WIDTH_COEFF);
|
||||
draw_tracker(indicator, box_pos, opacity);
|
||||
|
||||
if (!indicator.active && opacity <= 0.01f)
|
||||
{
|
||||
DEV_LOG("Remove tracker indicator");
|
||||
trackers.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FullscreenUI::UpdateAchievementOverlaysRunIdle()
|
||||
{
|
||||
// early out if we're already on the GPU thread
|
||||
if (GPUThread::IsOnThread())
|
||||
{
|
||||
GPUThread::SetRunIdleReason(GPUThread::RunIdleReason::AchievementOverlaysActive,
|
||||
!s_achievements_locals.notifications.empty());
|
||||
return;
|
||||
}
|
||||
|
||||
// need to check it again once we're executing on the gpu thread, it could've changed since
|
||||
GPUThread::RunOnThread([]() {
|
||||
bool is_active;
|
||||
{
|
||||
const auto lock = Achievements::GetLock();
|
||||
is_active = !s_achievements_locals.notifications.empty();
|
||||
}
|
||||
GPUThread::SetRunIdleReason(GPUThread::RunIdleReason::AchievementOverlaysActive, is_active);
|
||||
});
|
||||
}
|
||||
|
||||
const std::string& FullscreenUI::GetCachedAchievementBadgePath(const rc_client_achievement_t* achievement, bool locked)
|
||||
|
||||
@@ -120,6 +120,13 @@ bool IsInputBindingDialogOpen();
|
||||
// Achievements
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/// 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);
|
||||
|
||||
/// Draws ImGui overlays when ingame.
|
||||
void DrawAchievementsOverlays();
|
||||
|
||||
/// Draws ImGui overlays when paused.
|
||||
void DrawAchievementsPauseMenuOverlays(float start_pos_y);
|
||||
|
||||
|
||||
@@ -2229,9 +2229,7 @@ void FullscreenUI::DrawInterfaceSettingsPage()
|
||||
// have to queue because we're holding the settings lock, and UpdateWidgetsSettings() reads it
|
||||
if (widgets_settings_changed)
|
||||
{
|
||||
Host::RunOnCoreThread([]() {
|
||||
GPUThread::RunOnThread(&FullscreenUI::UpdateWidgetsSettings);
|
||||
});
|
||||
Host::RunOnCoreThread([]() { GPUThread::RunOnThread(&FullscreenUI::UpdateWidgetsSettings); });
|
||||
}
|
||||
|
||||
MenuHeading(FSUI_VSTR("Behavior"));
|
||||
@@ -4830,6 +4828,8 @@ void FullscreenUI::DrawAchievementsSettingsPage(std::unique_lock<std::mutex>& se
|
||||
"tracked by RetroAchievements."),
|
||||
"Cheevos", "UnofficialTestMode", false, enabled);
|
||||
|
||||
MenuHeading(FSUI_VSTR("Notifications"));
|
||||
|
||||
DrawToggleSetting(
|
||||
bsi, FSUI_ICONVSTR(ICON_FA_BELL, "Achievement Notifications"),
|
||||
FSUI_VSTR("Displays popup messages on events such as achievement unlocks and leaderboard submissions."), "Cheevos",
|
||||
@@ -4837,27 +4837,40 @@ void FullscreenUI::DrawAchievementsSettingsPage(std::unique_lock<std::mutex>& se
|
||||
DrawToggleSetting(bsi, FSUI_ICONVSTR(ICON_FA_LIST_OL, "Leaderboard Notifications"),
|
||||
FSUI_VSTR("Displays popup messages when starting, submitting, or failing a leaderboard challenge."),
|
||||
"Cheevos", "LeaderboardNotifications", true, enabled);
|
||||
DrawToggleSetting(
|
||||
bsi, FSUI_ICONVSTR(ICON_FA_CLOCK, "Leaderboard Trackers"),
|
||||
FSUI_VSTR("Shows a timer in the bottom-right corner of the screen when leaderboard challenges are active."),
|
||||
"Cheevos", "LeaderboardTrackers", true, enabled);
|
||||
DrawToggleSetting(bsi, FSUI_ICONVSTR(ICON_FA_CLOCK, "Leaderboard Trackers"),
|
||||
FSUI_VSTR("Shows a timer in the selected location when leaderboard challenges are active."),
|
||||
"Cheevos", "LeaderboardTrackers", true, enabled);
|
||||
DrawToggleSetting(
|
||||
bsi, FSUI_ICONVSTR(ICON_FA_MUSIC, "Sound Effects"),
|
||||
FSUI_VSTR("Plays sound effects for events such as achievement unlocks and leaderboard submissions."), "Cheevos",
|
||||
"SoundEffects", true, enabled);
|
||||
DrawToggleSetting(
|
||||
bsi, FSUI_ICONVSTR(ICON_FA_BARS_PROGRESS, "Progress Indicators"),
|
||||
FSUI_VSTR(
|
||||
"Shows a popup in the lower-right corner of the screen when progress towards a measured achievement changes."),
|
||||
"Cheevos", "ProgressIndicators", true, enabled);
|
||||
|
||||
DrawEnumSetting(bsi, FSUI_ICONVSTR(ICON_FA_ENVELOPE, "Notification Location"),
|
||||
FSUI_VSTR("Selects the screen location for achievement and leaderboard notifications."), "Cheevos",
|
||||
"NotificationLocation", Settings::DEFAULT_ACHIEVEMENT_NOTIFICATION_LOCATION,
|
||||
&Settings::ParseNotificationLocation, &Settings::GetNotificationLocationName,
|
||||
&Settings::GetNotificationLocationDisplayName, NotificationLocation::MaxCount, enabled);
|
||||
|
||||
MenuHeading(FSUI_VSTR("Progress Tracking"));
|
||||
|
||||
DrawEnumSetting(
|
||||
bsi, FSUI_ICONVSTR(ICON_FA_TEMPERATURE_ARROW_UP, "Challenge Indicators"),
|
||||
FSUI_VSTR("Shows a notification or icons in the lower-right corner of the screen when a challenge/primed "
|
||||
"achievement is active."),
|
||||
FSUI_VSTR("Shows a notification or icons in the selected location when a challenge/primed achievement is active."),
|
||||
"Cheevos", "ChallengeIndicatorMode", Settings::DEFAULT_ACHIEVEMENT_CHALLENGE_INDICATOR_MODE,
|
||||
&Settings::ParseAchievementChallengeIndicatorMode, &Settings::GetAchievementChallengeIndicatorModeName,
|
||||
&Settings::GetAchievementChallengeIndicatorModeDisplayName, AchievementChallengeIndicatorMode::MaxCount, enabled);
|
||||
|
||||
DrawEnumSetting(bsi, FSUI_ICONVSTR(ICON_FA_LOCATION_DOT, "Indicator Location"),
|
||||
FSUI_VSTR("Selects the screen location for challenge/progress indicators, and leaderboard trackers."),
|
||||
"Cheevos", "IndicatorLocation", Settings::DEFAULT_ACHIEVEMENT_INDICATOR_LOCATION,
|
||||
&Settings::ParseNotificationLocation, &Settings::GetNotificationLocationName,
|
||||
&Settings::GetNotificationLocationDisplayName, NotificationLocation::MaxCount, enabled);
|
||||
|
||||
DrawToggleSetting(
|
||||
bsi, FSUI_ICONVSTR(ICON_FA_BARS_PROGRESS, "Progress Indicators"),
|
||||
FSUI_VSTR("Shows a popup in the selected location when progress towards a measured achievement changes."),
|
||||
"Cheevos", "ProgressIndicators", true, enabled);
|
||||
|
||||
if (!IsEditingGameSettings(bsi))
|
||||
{
|
||||
MenuHeading(FSUI_VSTR("Operations"));
|
||||
|
||||
@@ -423,6 +423,7 @@ TRANSLATE_NOOP("FullscreenUI", "Includes the elapsed time since the application
|
||||
TRANSLATE_NOOP("FullscreenUI", "Includes the elapsed time since the application start in window and console logs.");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Increases the field of view from 4:3 to the chosen display aspect ratio in 3D games.");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Increases the precision of polygon culling, reducing the number of holes in geometry.");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Indicator Location");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Informational Message Duration");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Inhibit Screensaver");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Input Sources");
|
||||
@@ -510,6 +511,8 @@ TRANSLATE_NOOP("FullscreenUI", "None (Double Speed)");
|
||||
TRANSLATE_NOOP("FullscreenUI", "None (Normal Speed)");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Not Logged In");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Not Scanning Subdirectories");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Notification Location");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Notifications");
|
||||
TRANSLATE_NOOP("FullscreenUI", "OK");
|
||||
TRANSLATE_NOOP("FullscreenUI", "OSD Scale");
|
||||
TRANSLATE_NOOP("FullscreenUI", "On-Screen Display");
|
||||
@@ -561,6 +564,7 @@ TRANSLATE_NOOP("FullscreenUI", "Prevents resizing of the window while a game is
|
||||
TRANSLATE_NOOP("FullscreenUI", "Prevents the emulator from producing any audible sound.");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Prevents the screen saver from activating and the host from sleeping while emulation is running.");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Progress Indicators");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Progress Tracking");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Progress database updated.");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Provides vibration and LED control support over Bluetooth.");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Purple Rain");
|
||||
@@ -663,6 +667,8 @@ TRANSLATE_NOOP("FullscreenUI", "Selects the percentage of the normal clock speed
|
||||
TRANSLATE_NOOP("FullscreenUI", "Selects the quality at which screenshots will be compressed.");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Selects the resolution scale that will be applied to the final image. 1x will downsample to the original console resolution.");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Selects the resolution to use in fullscreen modes.");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Selects the screen location for achievement and leaderboard notifications.");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Selects the screen location for challenge/progress indicators, and leaderboard trackers.");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Selects the type of emulated controller for this port.");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Selects the view that the game list will open to.");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Serial");
|
||||
@@ -702,9 +708,9 @@ TRANSLATE_NOOP("FullscreenUI", "Show Resolution");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Show Speed");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Show Status Indicators");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Shows a background image or shader when a game isn't running. Backgrounds are located in resources/fullscreenui/backgrounds in the data directory.");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Shows a notification or icons in the lower-right corner of the screen when a challenge/primed achievement is active.");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Shows a popup in the lower-right corner of the screen when progress towards a measured achievement changes.");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Shows a timer in the bottom-right corner of the screen when leaderboard challenges are active.");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Shows a notification or icons in the selected location when a challenge/primed achievement is active.");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Shows a popup in the selected location when progress towards a measured achievement changes.");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Shows a timer in the selected location when leaderboard challenges are active.");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Shows a visual history of frame times in the upper-left corner of the display.");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Shows enhancement settings in the bottom-right corner of the screen.");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Shows information about input and audio latency in the top-right corner of the display.");
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
#include "util/shadergen.h"
|
||||
|
||||
#include "common/assert.h"
|
||||
#include "common/easing.h"
|
||||
#include "common/error.h"
|
||||
#include "common/file_system.h"
|
||||
#include "common/log.h"
|
||||
@@ -63,7 +62,7 @@ static bool CompileTransitionPipelines(Error* error);
|
||||
static void CreateFooterTextString(SmallStringBase& dest,
|
||||
std::span<const std::pair<const char*, std::string_view>> items);
|
||||
|
||||
static void DrawBackgroundProgressDialogs(ImVec2& position, float spacing);
|
||||
static void DrawBackgroundProgressDialogs(float& current_y);
|
||||
static void UpdateLoadingScreenProgress(s32 progress_min, s32 progress_max, s32 progress_value);
|
||||
static bool GetLoadingScreenTimeEstimate(SmallString& out_str);
|
||||
static void DrawLoadingScreen(std::string_view image, std::string_view title, std::string_view caption,
|
||||
@@ -76,8 +75,7 @@ static bool AreAnyNotificationsActive();
|
||||
static void UpdateNotificationsRunIdle();
|
||||
static void UpdateLoadingScreenRunIdle();
|
||||
|
||||
static void DrawNotifications(ImVec2& position, float spacing);
|
||||
static void DrawToast();
|
||||
static void DrawToast(float& current_y);
|
||||
static void DrawLoadingScreen();
|
||||
|
||||
static ImGuiID GetBackgroundProgressID(std::string_view str_id);
|
||||
@@ -135,20 +133,6 @@ enum class CloseButtonState : u8
|
||||
Cancelled,
|
||||
};
|
||||
|
||||
struct Notification
|
||||
{
|
||||
std::string key;
|
||||
std::string title;
|
||||
std::string text;
|
||||
std::string note;
|
||||
std::string badge_path;
|
||||
Timer::Value start_time;
|
||||
Timer::Value move_time;
|
||||
float duration;
|
||||
float target_y;
|
||||
float last_y;
|
||||
};
|
||||
|
||||
struct BackgroundProgressDialogData
|
||||
{
|
||||
std::string message;
|
||||
@@ -362,8 +346,6 @@ struct WidgetsState
|
||||
ProgressDialog progress_dialog;
|
||||
MessageDialog message_dialog;
|
||||
|
||||
std::vector<Notification> notifications;
|
||||
|
||||
std::string toast_title;
|
||||
std::string toast_message;
|
||||
Timer::Value toast_start_time;
|
||||
@@ -448,7 +430,6 @@ void FullscreenUI::ShutdownWidgets(bool preserve_fsui_state)
|
||||
if (!preserve_fsui_state)
|
||||
{
|
||||
s_state.fullscreen_footer_icon_mapping = {};
|
||||
s_state.notifications.clear();
|
||||
s_state.background_progress_dialogs.clear();
|
||||
s_state.fullscreen_footer_text.clear();
|
||||
s_state.last_fullscreen_footer_text.clear();
|
||||
@@ -1067,6 +1048,12 @@ void FullscreenUI::EnqueueSoundEffect(std::string_view sound_effect)
|
||||
s_state.had_sound_effect = true;
|
||||
}
|
||||
|
||||
float FullscreenUI::GetScreenBottomMargin()
|
||||
{
|
||||
return std::max(ImGuiManager::GetScreenMargin(),
|
||||
LayoutScale(20.0f + (s_state.last_fullscreen_footer_text.empty() ? 0.0f : LAYOUT_FOOTER_HEIGHT)));
|
||||
}
|
||||
|
||||
FullscreenUI::FixedPopupDialog::FixedPopupDialog() = default;
|
||||
|
||||
FullscreenUI::FixedPopupDialog::~FixedPopupDialog() = default;
|
||||
@@ -1140,15 +1127,9 @@ void FullscreenUI::RenderOverlays()
|
||||
|
||||
DrawLoadingScreen();
|
||||
|
||||
const float margin = std::max(ImGuiManager::GetScreenMargin(), LayoutScale(10.0f));
|
||||
const float spacing = LayoutScale(10.0f);
|
||||
const float notification_vertical_pos = GetNotificationVerticalPosition();
|
||||
ImVec2 position(margin, notification_vertical_pos * ImGui::GetIO().DisplaySize.y +
|
||||
((notification_vertical_pos >= 0.5f) ? -margin : margin));
|
||||
position.y = std::max(position.y, ImGuiManager::GetOSDMessageEndPosition());
|
||||
DrawBackgroundProgressDialogs(position, spacing);
|
||||
DrawNotifications(position, spacing);
|
||||
DrawToast();
|
||||
float bottom_center_y = ImGui::GetIO().DisplaySize.y - GetScreenBottomMargin();
|
||||
DrawBackgroundProgressDialogs(bottom_center_y);
|
||||
DrawToast(bottom_center_y);
|
||||
|
||||
// cleared?
|
||||
if (!AreAnyNotificationsActive())
|
||||
@@ -4359,25 +4340,6 @@ std::unique_ptr<ProgressCallbackWithPrompt> FullscreenUI::OpenModalProgressDialo
|
||||
return s_state.progress_dialog.GetProgressCallback(std::move(title), window_unscaled_width);
|
||||
}
|
||||
|
||||
static float s_notification_vertical_position = 0.15f;
|
||||
static float s_notification_vertical_direction = 1.0f;
|
||||
|
||||
float FullscreenUI::GetNotificationVerticalPosition()
|
||||
{
|
||||
return s_notification_vertical_position;
|
||||
}
|
||||
|
||||
float FullscreenUI::GetNotificationVerticalDirection()
|
||||
{
|
||||
return s_notification_vertical_direction;
|
||||
}
|
||||
|
||||
void FullscreenUI::SetNotificationVerticalPosition(float position, float direction)
|
||||
{
|
||||
s_notification_vertical_position = position;
|
||||
s_notification_vertical_direction = direction;
|
||||
}
|
||||
|
||||
ImGuiID FullscreenUI::GetBackgroundProgressID(std::string_view str_id)
|
||||
{
|
||||
return ImHashStr(str_id.data(), str_id.length());
|
||||
@@ -4465,20 +4427,21 @@ bool FullscreenUI::IsBackgroundProgressDialogOpen(std::string_view str_id)
|
||||
return false;
|
||||
}
|
||||
|
||||
void FullscreenUI::DrawBackgroundProgressDialogs(ImVec2& position, float spacing)
|
||||
void FullscreenUI::DrawBackgroundProgressDialogs(float& current_y)
|
||||
{
|
||||
if (s_state.background_progress_dialogs.empty())
|
||||
return;
|
||||
|
||||
const float window_width = LayoutScale(500.0f);
|
||||
const float window_height = LayoutScale(75.0f);
|
||||
const float window_pos_x = (ImGui::GetIO().DisplaySize.x - window_width) * 0.5f;
|
||||
const float window_spacing = LayoutScale(10.0f);
|
||||
|
||||
ImDrawList* dl = ImGui::GetForegroundDrawList();
|
||||
|
||||
for (const BackgroundProgressDialogData& data : s_state.background_progress_dialogs)
|
||||
{
|
||||
const float window_pos_x = position.x;
|
||||
const float window_pos_y = position.y - ((s_notification_vertical_direction < 0.0f) ? window_height : 0.0f);
|
||||
const float window_pos_y = current_y - window_height;
|
||||
|
||||
dl->AddRectFilled(ImVec2(window_pos_x, window_pos_y),
|
||||
ImVec2(window_pos_x + window_width, window_pos_y + window_height),
|
||||
@@ -4517,7 +4480,7 @@ void FullscreenUI::DrawBackgroundProgressDialogs(ImVec2& position, float spacing
|
||||
ImGui::GetColorU32(UIStyle.SecondaryColor));
|
||||
}
|
||||
|
||||
position.y += s_notification_vertical_direction * (window_height + spacing);
|
||||
current_y = current_y - window_height - window_spacing;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4983,12 +4946,9 @@ void FullscreenUI::LoadingScreenProgressCallback::Redraw(bool force)
|
||||
// Notifications
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
static constexpr float NOTIFICATION_APPEAR_ANIMATION_TIME = 0.2f;
|
||||
static constexpr float NOTIFICATION_DISAPPEAR_ANIMATION_TIME = 0.5f;
|
||||
|
||||
bool FullscreenUI::AreAnyNotificationsActive()
|
||||
{
|
||||
return (!s_state.notifications.empty() || !s_state.toast_title.empty() || !s_state.toast_message.empty() ||
|
||||
return (!s_state.toast_title.empty() || !s_state.toast_message.empty() ||
|
||||
!s_state.background_progress_dialogs.empty() || s_state.loading_screen_open);
|
||||
}
|
||||
|
||||
@@ -5006,214 +4966,288 @@ void FullscreenUI::UpdateNotificationsRunIdle()
|
||||
[]() { GPUThread::SetRunIdleReason(GPUThread::RunIdleReason::NotificationsActive, AreAnyNotificationsActive()); });
|
||||
}
|
||||
|
||||
void FullscreenUI::AddNotification(std::string key, float duration, std::string image_path, std::string title,
|
||||
std::string text, std::string note)
|
||||
FullscreenUI::NotificationLayout::NotificationLayout(NotificationLocation location)
|
||||
: m_spacing(LayoutScale(10.0f)), m_location(location)
|
||||
{
|
||||
const std::unique_lock lock(s_state.shared_state_mutex);
|
||||
if (!s_state.has_initialized)
|
||||
return;
|
||||
const float screen_margin = std::max(ImGuiManager::GetScreenMargin(), LayoutScale(10.0f));
|
||||
|
||||
const bool prev_had_notifications = AreAnyNotificationsActive();
|
||||
const Timer::Value current_time = Timer::GetCurrentValue();
|
||||
// android goes a little lower due to on-screen buttons
|
||||
#ifndef __ANDROID__
|
||||
static constexpr float top_start_pct = 0.1f;
|
||||
#else
|
||||
static constexpr float top_start_pct = 0.15f;
|
||||
#endif
|
||||
|
||||
if (!key.empty())
|
||||
const ImGuiIO& io = ImGui::GetIO();
|
||||
switch (m_location)
|
||||
{
|
||||
for (auto it = s_state.notifications.begin(); it != s_state.notifications.end(); ++it)
|
||||
case NotificationLocation::TopLeft:
|
||||
{
|
||||
if (it->key == key)
|
||||
{
|
||||
it->duration = duration;
|
||||
it->title = std::move(title);
|
||||
it->text = std::move(text);
|
||||
it->note = std::move(note);
|
||||
it->badge_path = std::move(image_path);
|
||||
m_current_position.x = screen_margin;
|
||||
|
||||
// Don't fade it in again
|
||||
const float time_passed = static_cast<float>(Timer::ConvertValueToSeconds(current_time - it->start_time));
|
||||
it->start_time =
|
||||
current_time - Timer::ConvertSecondsToValue(std::min(time_passed, NOTIFICATION_APPEAR_ANIMATION_TIME));
|
||||
return;
|
||||
}
|
||||
// need to consider osd message size
|
||||
m_current_position.y = std::max(std::max(screen_margin, top_start_pct * io.DisplaySize.y),
|
||||
ImGuiManager::GetOSDMessageEndPosition() + m_spacing);
|
||||
}
|
||||
break;
|
||||
|
||||
case NotificationLocation::TopCenter:
|
||||
{
|
||||
m_current_position.x = io.DisplaySize.x * 0.5f;
|
||||
m_current_position.y = screen_margin;
|
||||
}
|
||||
break;
|
||||
|
||||
case NotificationLocation::TopRight:
|
||||
{
|
||||
m_current_position.x = io.DisplaySize.x - screen_margin;
|
||||
m_current_position.y = std::max(screen_margin, top_start_pct * io.DisplaySize.y);
|
||||
}
|
||||
break;
|
||||
|
||||
case NotificationLocation::BottomLeft:
|
||||
{
|
||||
m_current_position.x = screen_margin;
|
||||
m_current_position.y = io.DisplaySize.y - GetScreenBottomMargin();
|
||||
}
|
||||
break;
|
||||
|
||||
case NotificationLocation::BottomCenter:
|
||||
{
|
||||
m_current_position.x = io.DisplaySize.x * 0.5f;
|
||||
m_current_position.y = io.DisplaySize.y - GetScreenBottomMargin();
|
||||
}
|
||||
break;
|
||||
|
||||
case NotificationLocation::BottomRight:
|
||||
{
|
||||
m_current_position.x = io.DisplaySize.x - screen_margin;
|
||||
m_current_position.y = io.DisplaySize.y - GetScreenBottomMargin();
|
||||
}
|
||||
break;
|
||||
|
||||
DefaultCaseIsUnreachable();
|
||||
}
|
||||
|
||||
Notification notif;
|
||||
notif.key = std::move(key);
|
||||
notif.duration = duration;
|
||||
notif.title = std::move(title);
|
||||
notif.text = std::move(text);
|
||||
notif.note = std::move(note);
|
||||
notif.badge_path = std::move(image_path);
|
||||
notif.start_time = current_time;
|
||||
notif.move_time = current_time;
|
||||
notif.target_y = -1.0f;
|
||||
notif.last_y = -1.0f;
|
||||
s_state.notifications.push_back(std::move(notif));
|
||||
|
||||
if (!prev_had_notifications)
|
||||
UpdateNotificationsRunIdle();
|
||||
}
|
||||
|
||||
void FullscreenUI::DrawNotifications(ImVec2& position, float spacing)
|
||||
bool FullscreenUI::NotificationLayout::IsVerticalAnimation() const
|
||||
{
|
||||
if (s_state.notifications.empty())
|
||||
return;
|
||||
return (m_location == NotificationLocation::TopCenter || m_location == NotificationLocation::BottomCenter);
|
||||
}
|
||||
|
||||
static constexpr float MOVE_DURATION = 0.5f;
|
||||
const Timer::Value current_time = Timer::GetCurrentValue();
|
||||
|
||||
const float horizontal_padding = FullscreenUI::LayoutScale(20.0f);
|
||||
const float vertical_padding = FullscreenUI::LayoutScale(15.0f);
|
||||
const float horizontal_spacing = FullscreenUI::LayoutScale(10.0f);
|
||||
const float larger_horizontal_spacing = FullscreenUI::LayoutScale(18.0f);
|
||||
const float vertical_spacing = FullscreenUI::LayoutScale(4.0f);
|
||||
const float badge_size = FullscreenUI::LayoutScale(48.0f);
|
||||
const float min_width = FullscreenUI::LayoutScale(200.0f);
|
||||
const float max_width = FullscreenUI::LayoutScale(600.0f);
|
||||
const float max_text_width = max_width - badge_size - (horizontal_padding * 2.0f) - horizontal_spacing;
|
||||
const float min_height = (vertical_padding * 2.0f) + badge_size;
|
||||
const float shadow_size = FullscreenUI::LayoutScale(2.0f);
|
||||
const float rounding = FullscreenUI::LayoutScale(20.0f);
|
||||
const float min_rounded_width = rounding * 2.0f;
|
||||
|
||||
ImFont*& font = UIStyle.Font;
|
||||
const float& title_font_size = UIStyle.LargeFontSize;
|
||||
const float& title_font_weight = UIStyle.BoldFontWeight;
|
||||
const float& text_font_size = UIStyle.MediumFontSize;
|
||||
const float& text_font_weight = UIStyle.NormalFontWeight;
|
||||
const float& note_font_size = UIStyle.MediumFontSize;
|
||||
const float& note_font_weight = UIStyle.BoldFontWeight;
|
||||
|
||||
const ImVec4 left_background_color = DarkerColor(UIStyle.ToastBackgroundColor, 1.3f);
|
||||
const ImVec4 right_background_color = DarkerColor(UIStyle.ToastBackgroundColor, 0.8f);
|
||||
|
||||
for (u32 index = 0; index < static_cast<u32>(s_state.notifications.size());)
|
||||
ImVec2 FullscreenUI::NotificationLayout::GetFixedPosition(float width, float height)
|
||||
{
|
||||
switch (m_location)
|
||||
{
|
||||
Notification& notif = s_state.notifications[index];
|
||||
const float time_passed = static_cast<float>(Timer::ConvertValueToSeconds(current_time - notif.start_time));
|
||||
if (time_passed >= notif.duration)
|
||||
case NotificationLocation::TopLeft:
|
||||
{
|
||||
s_state.notifications.erase(s_state.notifications.begin() + index);
|
||||
continue;
|
||||
const ImVec2 pos = ImVec2(m_current_position.x, m_current_position.y);
|
||||
m_current_position.y += height + m_spacing;
|
||||
return pos;
|
||||
}
|
||||
|
||||
// place note to the right of the title
|
||||
const ImVec2 note_size = notif.note.empty() ? ImVec2() :
|
||||
font->CalcTextSizeA(note_font_size, note_font_weight, FLT_MAX, 0.0f,
|
||||
IMSTR_START_END(notif.note));
|
||||
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)),
|
||||
min_width);
|
||||
const float box_height =
|
||||
std::max((vertical_padding * 2.0f) + ImCeil(title_size.y) + vertical_spacing + ImCeil(text_size.y), min_height);
|
||||
|
||||
float opacity = 1.0f;
|
||||
float box_start = position.x;
|
||||
if (time_passed < NOTIFICATION_APPEAR_ANIMATION_TIME)
|
||||
case NotificationLocation::TopCenter:
|
||||
{
|
||||
const float pct = time_passed / NOTIFICATION_APPEAR_ANIMATION_TIME;
|
||||
const float eased_pct = Easing::OutExpo(pct);
|
||||
box_start = ImFloor(position.x - (box_width * 0.2f * (1.0f - eased_pct)));
|
||||
opacity = pct;
|
||||
}
|
||||
else if (time_passed >= (notif.duration - NOTIFICATION_DISAPPEAR_ANIMATION_TIME))
|
||||
{
|
||||
const float pct = (notif.duration - time_passed) / NOTIFICATION_DISAPPEAR_ANIMATION_TIME;
|
||||
const float eased_pct = Easing::OutExpo(pct);
|
||||
box_start = ImFloor(position.x - (box_width * 0.2f * (1.0f - eased_pct)));
|
||||
opacity = eased_pct;
|
||||
const ImVec2 pos = ImVec2(ImFloor((ImGui::GetIO().DisplaySize.x - width) * 0.5f), m_current_position.y);
|
||||
m_current_position.y += height + m_spacing;
|
||||
return pos;
|
||||
}
|
||||
|
||||
const float expected_y = position.y - ((s_notification_vertical_direction < 0.0f) ? box_height : 0.0f);
|
||||
float actual_y = notif.last_y;
|
||||
if (notif.target_y != expected_y)
|
||||
case NotificationLocation::TopRight:
|
||||
{
|
||||
notif.move_time = current_time;
|
||||
notif.target_y = expected_y;
|
||||
notif.last_y = (notif.last_y < 0.0f) ? expected_y : notif.last_y;
|
||||
actual_y = notif.last_y;
|
||||
const ImVec2 pos = ImVec2(m_current_position.x - width, m_current_position.y);
|
||||
m_current_position.y += height + m_spacing;
|
||||
return pos;
|
||||
}
|
||||
else if (actual_y != expected_y)
|
||||
|
||||
case NotificationLocation::BottomLeft:
|
||||
{
|
||||
const float time_since_move = static_cast<float>(Timer::ConvertValueToSeconds(current_time - notif.move_time));
|
||||
if (time_since_move >= MOVE_DURATION)
|
||||
const ImVec2 pos = ImVec2(m_current_position.x, m_current_position.y - height);
|
||||
m_current_position.y -= height + m_spacing;
|
||||
return pos;
|
||||
}
|
||||
|
||||
case NotificationLocation::BottomCenter:
|
||||
{
|
||||
const ImVec2 pos = ImVec2(ImFloor((ImGui::GetIO().DisplaySize.x - width) * 0.5f), m_current_position.y - height);
|
||||
m_current_position.y -= height + m_spacing;
|
||||
return pos;
|
||||
}
|
||||
|
||||
case NotificationLocation::BottomRight:
|
||||
{
|
||||
const ImVec2 pos = ImVec2(m_current_position.x - width, m_current_position.y - height);
|
||||
m_current_position.y -= height + m_spacing;
|
||||
return pos;
|
||||
}
|
||||
|
||||
DefaultCaseIsUnreachable();
|
||||
}
|
||||
}
|
||||
|
||||
std::pair<ImVec2, float> FullscreenUI::NotificationLayout::GetNextPosition(float width, float height, bool active,
|
||||
float anim_coeff, float width_coeff)
|
||||
{
|
||||
switch (m_location)
|
||||
{
|
||||
case NotificationLocation::TopLeft:
|
||||
case NotificationLocation::BottomLeft:
|
||||
case NotificationLocation::TopRight:
|
||||
case NotificationLocation::BottomRight:
|
||||
{
|
||||
float opacity;
|
||||
ImVec2 pos;
|
||||
if (m_location == NotificationLocation::TopLeft || m_location == NotificationLocation::BottomLeft)
|
||||
{
|
||||
notif.move_time = current_time;
|
||||
notif.last_y = notif.target_y;
|
||||
actual_y = notif.last_y;
|
||||
if (anim_coeff != 1.0f)
|
||||
{
|
||||
if (active)
|
||||
{
|
||||
const float eased_pct = Easing::OutExpo(anim_coeff);
|
||||
pos.x = ImFloor(m_current_position.x - (width * width_coeff * (1.0f - eased_pct)));
|
||||
opacity = anim_coeff;
|
||||
}
|
||||
else
|
||||
{
|
||||
const float eased_pct = Easing::OutExpo(anim_coeff);
|
||||
pos.x = ImFloor(m_current_position.x - (width * width_coeff * (1.0f - eased_pct)));
|
||||
opacity = eased_pct;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
pos.x = m_current_position.x;
|
||||
opacity = 1.0f;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
const float frac = Easing::OutExpo(time_since_move / MOVE_DURATION);
|
||||
actual_y = notif.last_y - ((notif.last_y - notif.target_y) * frac);
|
||||
pos.x = m_current_position.x - width;
|
||||
if (anim_coeff != 1.0f)
|
||||
{
|
||||
if (active)
|
||||
{
|
||||
const float eased_pct = std::clamp(Easing::OutExpo(anim_coeff), 0.0f, 1.0f);
|
||||
pos.x = ImFloor(pos.x + (width * width_coeff * (1.0f - eased_pct)));
|
||||
opacity = anim_coeff;
|
||||
}
|
||||
else
|
||||
{
|
||||
const float eased_pct = std::clamp(Easing::InExpo(anim_coeff), 0.0f, 1.0f);
|
||||
pos.x = ImFloor(pos.x + (width * width_coeff * (1.0f - eased_pct)));
|
||||
opacity = eased_pct;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
opacity = 1.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ImVec2 box_min(box_start, actual_y);
|
||||
const ImVec2 box_max(box_min.x + box_width, box_min.y + box_height);
|
||||
const float background_opacity = opacity * 0.95f;
|
||||
|
||||
ImDrawList* const dl = ImGui::GetForegroundDrawList();
|
||||
const u32 left_background_color32 = ImGui::GetColorU32(ModAlpha(left_background_color, background_opacity));
|
||||
const u32 right_background_color32 =
|
||||
ImGui::GetColorU32(ModAlpha(ImLerp(left_background_color, right_background_color,
|
||||
(box_width - min_rounded_width) / (max_width - min_rounded_width)),
|
||||
background_opacity));
|
||||
|
||||
dl->AddRectFilled(box_min, ImVec2(box_min.x + rounding, box_max.y), left_background_color32, rounding,
|
||||
ImDrawFlags_RoundCornersLeft);
|
||||
dl->AddRectFilledMultiColor(ImVec2(box_min.x + rounding, box_min.y), ImVec2(box_max.x - rounding, box_max.y),
|
||||
left_background_color32, right_background_color32, right_background_color32,
|
||||
left_background_color32);
|
||||
dl->AddRectFilled(ImVec2(box_max.x - rounding, box_min.y), box_max, right_background_color32, rounding,
|
||||
ImDrawFlags_RoundCornersRight);
|
||||
|
||||
const ImVec2 badge_min(box_min.x + horizontal_padding, box_min.y + vertical_padding);
|
||||
const ImVec2 badge_max(badge_min.x + badge_size, badge_min.y + badge_size);
|
||||
if (!notif.badge_path.empty())
|
||||
{
|
||||
GPUTexture* tex = GetCachedTexture(notif.badge_path, static_cast<u32>(badge_size), static_cast<u32>(badge_size));
|
||||
if (tex)
|
||||
if (m_location == NotificationLocation::TopLeft || m_location == NotificationLocation::TopRight)
|
||||
{
|
||||
dl->AddImage(tex, badge_min, badge_max, ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f),
|
||||
ImGui::GetColorU32(ImVec4(1.0f, 1.0f, 1.0f, opacity)));
|
||||
pos.y = m_current_position.y;
|
||||
m_current_position.y = m_current_position.y + height + m_spacing;
|
||||
}
|
||||
else
|
||||
{
|
||||
pos.y = m_current_position.y - height;
|
||||
m_current_position.y = m_current_position.y - height - m_spacing;
|
||||
}
|
||||
|
||||
return std::make_pair(pos, opacity);
|
||||
}
|
||||
|
||||
const u32 title_col = ImGui::GetColorU32(ModAlpha(UIStyle.ToastTextColor, opacity));
|
||||
const u32 text_col = ImGui::GetColorU32(ModAlpha(DarkerColor(UIStyle.ToastTextColor), opacity));
|
||||
|
||||
const ImVec2 title_pos = ImVec2(badge_max.x + horizontal_spacing, box_min.y + vertical_padding);
|
||||
const ImRect title_bb = ImRect(title_pos, title_pos + title_size);
|
||||
RenderShadowedTextClipped(dl, font, title_font_size, title_font_weight, title_bb.Min, title_bb.Max, title_col,
|
||||
notif.title, &title_size, ImVec2(0.0f, 0.0f), max_text_width - note_size.x, &title_bb);
|
||||
|
||||
const ImVec2 text_pos = ImVec2(badge_max.x + horizontal_spacing, title_bb.Max.y + vertical_spacing);
|
||||
const ImRect text_bb = ImRect(text_pos, text_pos + text_size);
|
||||
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.empty())
|
||||
case NotificationLocation::TopCenter:
|
||||
{
|
||||
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);
|
||||
float opacity;
|
||||
ImVec2 pos;
|
||||
|
||||
pos.x = ImFloor((ImGui::GetIO().DisplaySize.x - width) * 0.5f);
|
||||
pos.y = m_current_position.y;
|
||||
if (anim_coeff != 1.0f)
|
||||
{
|
||||
if (active)
|
||||
{
|
||||
const float eased_pct = std::clamp(Easing::OutExpo(anim_coeff), 0.0f, 1.0f);
|
||||
// pos.x = ImFloor(pos.x - (width * width_coeff * (1.0f - eased_pct)));
|
||||
pos.y = ImFloor(pos.y - (height * width_coeff * (1.0f - eased_pct)));
|
||||
opacity = anim_coeff;
|
||||
}
|
||||
else
|
||||
{
|
||||
const float eased_pct = std::clamp(Easing::InExpo(anim_coeff), 0.0f, 1.0f);
|
||||
// pos.x = ImFloor(pos.x + (width * width_coeff * (1.0f - eased_pct)));
|
||||
pos.y = ImFloor(pos.y - (height * width_coeff * (1.0f - eased_pct)));
|
||||
opacity = eased_pct;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
opacity = 1.0f;
|
||||
}
|
||||
|
||||
m_current_position.y = m_current_position.y + height + m_spacing;
|
||||
|
||||
return std::make_pair(pos, opacity);
|
||||
}
|
||||
|
||||
position.y += s_notification_vertical_direction * (box_height + shadow_size + spacing);
|
||||
index++;
|
||||
case NotificationLocation::BottomCenter:
|
||||
{
|
||||
float opacity;
|
||||
ImVec2 pos;
|
||||
|
||||
pos.x = ImFloor((ImGui::GetIO().DisplaySize.x - width) * 0.5f);
|
||||
pos.y = m_current_position.y - height;
|
||||
if (anim_coeff != 1.0f)
|
||||
{
|
||||
if (active)
|
||||
{
|
||||
const float eased_pct = std::clamp(Easing::OutExpo(anim_coeff), 0.0f, 1.0f);
|
||||
// pos.x = ImFloor(pos.x - (width * width_coeff * (1.0f - eased_pct)));
|
||||
pos.y = ImFloor(pos.y + (height * width_coeff * (1.0f - eased_pct)));
|
||||
opacity = anim_coeff;
|
||||
}
|
||||
else
|
||||
{
|
||||
const float eased_pct = std::clamp(Easing::InExpo(anim_coeff), 0.0f, 1.0f);
|
||||
// pos.x = ImFloor(pos.x + (width * width_coeff * (1.0f - eased_pct)));
|
||||
pos.y = ImFloor(pos.y + (height * width_coeff * (1.0f - eased_pct)));
|
||||
opacity = eased_pct;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
opacity = 1.0f;
|
||||
}
|
||||
|
||||
m_current_position.y = m_current_position.y - height - m_spacing;
|
||||
|
||||
return std::make_pair(pos, opacity);
|
||||
}
|
||||
|
||||
DefaultCaseIsUnreachable();
|
||||
}
|
||||
}
|
||||
|
||||
std::pair<ImVec2, float> FullscreenUI::NotificationLayout::GetNextPosition(float width, float height, bool active,
|
||||
float time, float in_duration,
|
||||
float out_duration, float width_coeff)
|
||||
{
|
||||
const float anim_coeff = active ? std::min(time / in_duration, 1.0f) : std::min(time / out_duration, 1.0f);
|
||||
return GetNextPosition(width, height, active, anim_coeff, width_coeff);
|
||||
}
|
||||
|
||||
std::pair<ImVec2, float> FullscreenUI::NotificationLayout::GetNextPosition(float width, float height, float time_passed,
|
||||
float total_duration, float in_duration,
|
||||
float out_duration, float width_coeff)
|
||||
{
|
||||
const bool active = (time_passed < (total_duration - out_duration));
|
||||
const float anim_coeff =
|
||||
active ? std::min(time_passed / in_duration, 1.0f) : std::max((total_duration - time_passed) / out_duration, 0.0f);
|
||||
return GetNextPosition(width, height, active, anim_coeff, width_coeff);
|
||||
}
|
||||
|
||||
void FullscreenUI::ShowToast(OSDMessageType type, std::string title, std::string message)
|
||||
{
|
||||
const std::unique_lock lock(s_state.shared_state_mutex);
|
||||
@@ -5230,7 +5264,7 @@ void FullscreenUI::ShowToast(OSDMessageType type, std::string title, std::string
|
||||
UpdateNotificationsRunIdle();
|
||||
}
|
||||
|
||||
void FullscreenUI::DrawToast()
|
||||
void FullscreenUI::DrawToast(float& current_y)
|
||||
{
|
||||
if (s_state.toast_title.empty() && s_state.toast_message.empty())
|
||||
return;
|
||||
@@ -5259,9 +5293,7 @@ void FullscreenUI::DrawToast()
|
||||
const float message_font_weight = UIStyle.NormalFontWeight;
|
||||
const float padding = LayoutScale(20.0f);
|
||||
const float total_padding = padding * 2.0f;
|
||||
const float margin = LayoutScale(20.0f + (s_state.fullscreen_footer_text.empty() ? 0.0f : LAYOUT_FOOTER_HEIGHT));
|
||||
const float spacing = s_state.toast_title.empty() ? 0.0f : LayoutScale(10.0f);
|
||||
const ImVec2 display_size = ImGui::GetIO().DisplaySize;
|
||||
const ImVec2 title_size = s_state.toast_title.empty() ?
|
||||
ImVec2(0.0f, 0.0f) :
|
||||
title_font->CalcTextSizeA(title_font_size, title_font_weight, FLT_MAX, max_width,
|
||||
@@ -5273,7 +5305,7 @@ void FullscreenUI::DrawToast()
|
||||
const ImVec2 comb_size(std::max(title_size.x, message_size.x), title_size.y + spacing + message_size.y);
|
||||
|
||||
const ImVec2 box_size(comb_size.x + total_padding, comb_size.y + total_padding);
|
||||
const ImVec2 box_pos((display_size.x - box_size.x) * 0.5f, (display_size.y - margin - box_size.y));
|
||||
const ImVec2 box_pos((ImGui::GetIO().DisplaySize.x - box_size.x) * 0.5f, current_y - box_size.y);
|
||||
|
||||
ImDrawList* dl = ImGui::GetForegroundDrawList();
|
||||
dl->AddRectFilled(box_pos, box_pos + box_size,
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "types.h"
|
||||
|
||||
#include "util/translation.h"
|
||||
|
||||
#include "common/small_string.h"
|
||||
@@ -271,6 +273,8 @@ void EndLayout();
|
||||
/// Enqueues a sound effect, and prevents any other sound effects from being started this frame.
|
||||
void EnqueueSoundEffect(std::string_view sound_effect);
|
||||
|
||||
float GetScreenBottomMargin();
|
||||
|
||||
bool IsAnyFixedPopupDialogOpen();
|
||||
bool IsFixedPopupDialogOpen(std::string_view name);
|
||||
void OpenFixedPopupDialog(std::string_view name);
|
||||
@@ -494,10 +498,6 @@ void CloseMessageDialog();
|
||||
std::unique_ptr<ProgressCallbackWithPrompt> OpenModalProgressDialog(std::string title,
|
||||
float window_unscaled_width = 500.0f);
|
||||
|
||||
float GetNotificationVerticalPosition();
|
||||
float GetNotificationVerticalDirection();
|
||||
void SetNotificationVerticalPosition(float position, float direction);
|
||||
|
||||
void OpenBackgroundProgressDialog(std::string_view str_id, std::string message, s32 min, s32 max, s32 value);
|
||||
void UpdateBackgroundProgressDialog(std::string_view str_id, std::string message, s32 min, s32 max, s32 value);
|
||||
void CloseBackgroundProgressDialog(std::string_view str_id);
|
||||
@@ -515,8 +515,29 @@ bool IsLoadingScreenOpen();
|
||||
void CloseLoadingScreen();
|
||||
|
||||
/// Notification and toast support.
|
||||
void AddNotification(std::string key, float duration, std::string image_path, std::string title, std::string text,
|
||||
std::string note);
|
||||
class NotificationLayout
|
||||
{
|
||||
public:
|
||||
NotificationLayout(NotificationLocation location);
|
||||
|
||||
ALWAYS_INLINE NotificationLocation GetLocation() const { return m_location; }
|
||||
bool IsVerticalAnimation() const;
|
||||
|
||||
ImVec2 GetFixedPosition(float width, float height);
|
||||
|
||||
// [position, opacity]
|
||||
std::pair<ImVec2, float> GetNextPosition(float width, float height, bool active, float anim_coeff, float width_coeff);
|
||||
std::pair<ImVec2, float> GetNextPosition(float width, float height, bool active, float time, float in_duration,
|
||||
float out_duration, float width_coeff);
|
||||
std::pair<ImVec2, float> GetNextPosition(float width, float height, float time_passed, float total_duration,
|
||||
float in_duration, float out_duration, float width_coeff);
|
||||
|
||||
private:
|
||||
ImVec2 m_current_position;
|
||||
float m_spacing;
|
||||
NotificationLocation m_location;
|
||||
};
|
||||
|
||||
void ShowToast(OSDMessageType type, std::string title, std::string message);
|
||||
|
||||
// Wrapper for an animated popup dialog.
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "gpu_presenter.h"
|
||||
#include "core.h"
|
||||
#include "fullscreenui.h"
|
||||
#include "fullscreenui_private.h"
|
||||
#include "fullscreenui_widgets.h"
|
||||
#include "gpu.h"
|
||||
#include "gpu_backend.h"
|
||||
@@ -1198,13 +1199,17 @@ bool GPUPresenter::PresentFrame(GPUPresenter* presenter, GPUBackend* backend, bo
|
||||
|
||||
ImGuiManager::RenderDebugWindows();
|
||||
|
||||
FullscreenUI::Render();
|
||||
FullscreenUI::DrawAchievementsOverlays();
|
||||
|
||||
FullscreenUI::UploadAsyncTextures();
|
||||
|
||||
if (backend)
|
||||
ImGuiManager::RenderTextOverlays(backend);
|
||||
|
||||
ImGuiManager::RenderOverlayWindows();
|
||||
|
||||
FullscreenUI::Render();
|
||||
|
||||
FullscreenUI::RenderOverlays();
|
||||
|
||||
ImGuiManager::RenderOSDMessages();
|
||||
|
||||
@@ -1507,9 +1507,9 @@ void GPUThread::UpdateRunIdle()
|
||||
static constexpr u8 REQUIRE_MASK = static_cast<u8>(RunIdleReason::NoGPUBackend) |
|
||||
static_cast<u8>(RunIdleReason::SystemPaused) |
|
||||
static_cast<u8>(RunIdleReason::LoadingScreenActive);
|
||||
static constexpr u8 ACTIVATE_MASK = static_cast<u8>(RunIdleReason::FullscreenUIActive) |
|
||||
static_cast<u8>(RunIdleReason::NotificationsActive) |
|
||||
static_cast<u8>(RunIdleReason::LoadingScreenActive);
|
||||
static constexpr u8 ACTIVATE_MASK =
|
||||
static_cast<u8>(RunIdleReason::FullscreenUIActive) | static_cast<u8>(RunIdleReason::NotificationsActive) |
|
||||
static_cast<u8>(RunIdleReason::LoadingScreenActive) | static_cast<u8>(RunIdleReason::AchievementOverlaysActive);
|
||||
|
||||
const bool new_flag = (g_gpu_device && ((s_state.run_idle_reasons & REQUIRE_MASK) != 0) &&
|
||||
((s_state.run_idle_reasons & ACTIVATE_MASK) != 0));
|
||||
|
||||
@@ -39,6 +39,7 @@ enum class RunIdleReason : u8
|
||||
FullscreenUIActive = (1 << 2),
|
||||
NotificationsActive = (1 << 3),
|
||||
LoadingScreenActive = (1 << 4),
|
||||
AchievementOverlaysActive = (1 << 5),
|
||||
};
|
||||
|
||||
/// Starts Big Picture UI.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin <stenzek@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin <stenzek@gmail.com>
|
||||
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
|
||||
|
||||
#include "settings.h"
|
||||
@@ -503,11 +503,13 @@ void Settings::Load(const SettingsInterface& si, const SettingsInterface& contro
|
||||
achievements_leaderboard_trackers = si.GetBoolValue("Cheevos", "LeaderboardTrackers", true);
|
||||
achievements_sound_effects = si.GetBoolValue("Cheevos", "SoundEffects", true);
|
||||
achievements_progress_indicators = si.GetBoolValue("Cheevos", "ProgressIndicators", true);
|
||||
achievements_notification_location =
|
||||
ParseNotificationLocation(si.GetStringValue("Cheevos", "NotificationLocation").c_str())
|
||||
.value_or(DEFAULT_ACHIEVEMENT_NOTIFICATION_LOCATION);
|
||||
achievements_indicator_location = ParseNotificationLocation(si.GetStringValue("Cheevos", "IndicatorLocation").c_str())
|
||||
.value_or(DEFAULT_ACHIEVEMENT_INDICATOR_LOCATION);
|
||||
achievements_challenge_indicator_mode =
|
||||
ParseAchievementChallengeIndicatorMode(
|
||||
si.GetStringValue("Cheevos", "ChallengeIndicatorMode",
|
||||
GetAchievementChallengeIndicatorModeName(DEFAULT_ACHIEVEMENT_CHALLENGE_INDICATOR_MODE))
|
||||
.c_str())
|
||||
ParseAchievementChallengeIndicatorMode(si.GetStringValue("Cheevos", "ChallengeIndicatorMode").c_str())
|
||||
.value_or(DEFAULT_ACHIEVEMENT_CHALLENGE_INDICATOR_MODE);
|
||||
achievements_notification_duration =
|
||||
Truncate8(std::min<u32>(si.GetUIntValue("Cheevos", "NotificationsDuration", DEFAULT_ACHIEVEMENT_NOTIFICATION_TIME),
|
||||
@@ -829,6 +831,8 @@ void Settings::Save(SettingsInterface& si, bool ignore_base) const
|
||||
si.SetBoolValue("Cheevos", "LeaderboardTrackers", achievements_leaderboard_trackers);
|
||||
si.SetBoolValue("Cheevos", "SoundEffects", achievements_sound_effects);
|
||||
si.SetBoolValue("Cheevos", "ProgressIndicators", achievements_progress_indicators);
|
||||
si.SetStringValue("Cheevos", "NotificationLocation", GetNotificationLocationName(achievements_notification_location));
|
||||
si.SetStringValue("Cheevos", "IndicatorLocation", GetNotificationLocationName(achievements_indicator_location));
|
||||
si.SetStringValue("Cheevos", "ChallengeIndicatorMode",
|
||||
GetAchievementChallengeIndicatorModeName(achievements_challenge_indicator_mode));
|
||||
si.SetUIntValue("Cheevos", "NotificationsDuration", achievements_notification_duration);
|
||||
@@ -2342,6 +2346,45 @@ const char* Settings::GetDisplayOSDMessageTypeName(OSDMessageType type)
|
||||
return s_display_osd_message_type_names[static_cast<size_t>(type)];
|
||||
}
|
||||
|
||||
static constexpr const std::array s_notification_location_names = {
|
||||
"TopLeft", "TopCenter", "TopRight", "BottomLeft", "BottomCenter", "BottomRight",
|
||||
};
|
||||
static_assert(s_notification_location_names.size() == static_cast<size_t>(NotificationLocation::MaxCount));
|
||||
static constexpr const std::array s_notification_location_display_names = {
|
||||
TRANSLATE_DISAMBIG_NOOP("Settings", "Top Left", "NotificationLocation"),
|
||||
TRANSLATE_DISAMBIG_NOOP("Settings", "Top Center", "NotificationLocation"),
|
||||
TRANSLATE_DISAMBIG_NOOP("Settings", "Top Right", "NotificationLocation"),
|
||||
TRANSLATE_DISAMBIG_NOOP("Settings", "Bottom Left", "NotificationLocation"),
|
||||
TRANSLATE_DISAMBIG_NOOP("Settings", "Bottom Center", "NotificationLocation"),
|
||||
TRANSLATE_DISAMBIG_NOOP("Settings", "Bottom Right", "NotificationLocation"),
|
||||
};
|
||||
static_assert(s_notification_location_display_names.size() == static_cast<size_t>(NotificationLocation::MaxCount));
|
||||
|
||||
std::optional<NotificationLocation> Settings::ParseNotificationLocation(const char* str)
|
||||
{
|
||||
int index = 0;
|
||||
for (const char* name : s_notification_location_names)
|
||||
{
|
||||
if (StringUtil::Strcasecmp(name, str) == 0)
|
||||
return static_cast<NotificationLocation>(index);
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const char* Settings::GetNotificationLocationName(NotificationLocation location)
|
||||
{
|
||||
return s_notification_location_names[static_cast<size_t>(location)];
|
||||
}
|
||||
|
||||
const char* Settings::GetNotificationLocationDisplayName(NotificationLocation location)
|
||||
{
|
||||
return Host::TranslateToCString("Settings", s_notification_location_display_names[static_cast<size_t>(location)],
|
||||
"NotificationLocation");
|
||||
}
|
||||
|
||||
static constexpr const std::array s_achievement_challenge_indicator_mode_names = {
|
||||
"Disabled",
|
||||
"PersistentIcon",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin <stenzek@gmail.com>
|
||||
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
|
||||
|
||||
#pragma once
|
||||
@@ -344,6 +344,8 @@ struct Settings : public GPUSettings
|
||||
bool achievements_leaderboard_trackers : 1 = true;
|
||||
bool achievements_sound_effects : 1 = true;
|
||||
bool achievements_progress_indicators : 1 = true;
|
||||
NotificationLocation achievements_notification_location = DEFAULT_ACHIEVEMENT_NOTIFICATION_LOCATION;
|
||||
NotificationLocation achievements_indicator_location = DEFAULT_ACHIEVEMENT_INDICATOR_LOCATION;
|
||||
AchievementChallengeIndicatorMode achievements_challenge_indicator_mode =
|
||||
DEFAULT_ACHIEVEMENT_CHALLENGE_INDICATOR_MODE;
|
||||
u8 achievements_notification_duration = DEFAULT_ACHIEVEMENT_NOTIFICATION_TIME;
|
||||
@@ -544,6 +546,10 @@ struct Settings : public GPUSettings
|
||||
static const char* GetDisplayScreenshotModeName(DisplayScreenshotMode mode);
|
||||
static const char* GetDisplayScreenshotModeDisplayName(DisplayScreenshotMode mode);
|
||||
|
||||
static std::optional<NotificationLocation> ParseNotificationLocation(const char* str);
|
||||
static const char* GetNotificationLocationName(NotificationLocation location);
|
||||
static const char* GetNotificationLocationDisplayName(NotificationLocation location);
|
||||
|
||||
static std::optional<AchievementChallengeIndicatorMode> ParseAchievementChallengeIndicatorMode(const char* str);
|
||||
static const char* GetAchievementChallengeIndicatorModeName(AchievementChallengeIndicatorMode mode);
|
||||
static const char* GetAchievementChallengeIndicatorModeDisplayName(AchievementChallengeIndicatorMode mode);
|
||||
@@ -609,6 +615,8 @@ struct Settings : public GPUSettings
|
||||
|
||||
static constexpr AchievementChallengeIndicatorMode DEFAULT_ACHIEVEMENT_CHALLENGE_INDICATOR_MODE =
|
||||
AchievementChallengeIndicatorMode::Notification;
|
||||
static constexpr NotificationLocation DEFAULT_ACHIEVEMENT_NOTIFICATION_LOCATION = NotificationLocation::TopLeft;
|
||||
static constexpr NotificationLocation DEFAULT_ACHIEVEMENT_INDICATOR_LOCATION = NotificationLocation::BottomRight;
|
||||
static constexpr u8 DEFAULT_ACHIEVEMENT_NOTIFICATION_TIME = 5;
|
||||
static constexpr u8 DEFAULT_LEADERBOARD_NOTIFICATION_TIME = 10;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin <stenzek@gmail.com>
|
||||
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
|
||||
|
||||
#pragma once
|
||||
@@ -225,6 +225,17 @@ enum class DisplayScreenshotFormat : u8
|
||||
Count
|
||||
};
|
||||
|
||||
enum class NotificationLocation : u8
|
||||
{
|
||||
TopLeft,
|
||||
TopCenter,
|
||||
TopRight,
|
||||
BottomLeft,
|
||||
BottomCenter,
|
||||
BottomRight,
|
||||
MaxCount
|
||||
};
|
||||
|
||||
enum class AchievementChallengeIndicatorMode : u8
|
||||
{
|
||||
Disabled,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin <stenzek@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin <stenzek@gmail.com>
|
||||
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
|
||||
|
||||
#include "achievementsettingswidget.h"
|
||||
@@ -42,6 +42,10 @@ AchievementSettingsWidget::AchievementSettingsWidget(SettingsWindow* dialog, QWi
|
||||
SettingWidgetBinder::BindWidgetToFloatSetting(sif, m_ui.leaderboardNotificationsDuration, "Cheevos",
|
||||
"LeaderboardsDuration",
|
||||
Settings::DEFAULT_LEADERBOARD_NOTIFICATION_TIME);
|
||||
SettingWidgetBinder::BindWidgetToEnumSetting(
|
||||
sif, m_ui.notificationLocation, "Cheevos", "NotificationLocation", &Settings::ParseNotificationLocation,
|
||||
&Settings::GetNotificationLocationName, &Settings::GetNotificationLocationDisplayName,
|
||||
Settings::DEFAULT_ACHIEVEMENT_NOTIFICATION_LOCATION, NotificationLocation::MaxCount);
|
||||
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.leaderboardTrackers, "Cheevos", "LeaderboardTrackers", true);
|
||||
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.soundEffects, "Cheevos", "SoundEffects", true);
|
||||
SettingWidgetBinder::BindWidgetToEnumSetting(
|
||||
@@ -50,6 +54,10 @@ AchievementSettingsWidget::AchievementSettingsWidget(SettingsWindow* dialog, QWi
|
||||
&Settings::GetAchievementChallengeIndicatorModeDisplayName, Settings::DEFAULT_ACHIEVEMENT_CHALLENGE_INDICATOR_MODE,
|
||||
AchievementChallengeIndicatorMode::MaxCount);
|
||||
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.progressIndicators, "Cheevos", "ProgressIndicators", true);
|
||||
SettingWidgetBinder::BindWidgetToEnumSetting(
|
||||
sif, m_ui.indicatorLocation, "Cheevos", "IndicatorLocation", &Settings::ParseNotificationLocation,
|
||||
&Settings::GetNotificationLocationName, &Settings::GetNotificationLocationDisplayName,
|
||||
Settings::DEFAULT_ACHIEVEMENT_INDICATOR_LOCATION, NotificationLocation::MaxCount);
|
||||
|
||||
m_ui.changeSoundsLink->setText(
|
||||
QStringLiteral("<a href=\"https://github.com/stenzek/duckstation/wiki/Resource-Overrides\"><span "
|
||||
@@ -75,18 +83,21 @@ AchievementSettingsWidget::AchievementSettingsWidget(SettingsWindow* dialog, QWi
|
||||
dialog->registerWidgetHelp(
|
||||
m_ui.leaderboardNotifications, tr("Show Leaderboard Notifications"), tr("Checked"),
|
||||
tr("Displays popup messages when starting, submitting, or failing a leaderboard challenge."));
|
||||
dialog->registerWidgetHelp(
|
||||
m_ui.leaderboardTrackers, tr("Show Leaderboard Trackers"), tr("Checked"),
|
||||
tr("Shows a timer in the bottom-right corner of the screen when leaderboard challenges are active."));
|
||||
dialog->registerWidgetHelp(m_ui.leaderboardTrackers, tr("Show Leaderboard Trackers"), tr("Checked"),
|
||||
tr("Shows a timer in the selected location when leaderboard challenges are active."));
|
||||
dialog->registerWidgetHelp(
|
||||
m_ui.soundEffects, tr("Enable Sound Effects"), tr("Checked"),
|
||||
tr("Plays sound effects for events such as achievement unlocks and leaderboard submissions."));
|
||||
dialog->registerWidgetHelp(m_ui.challengeIndicatorMode, tr("Challenge Indicators"), tr("Show Notifications"),
|
||||
tr("Shows a notification or icons in the lower-right corner of the screen when a "
|
||||
"challenge/primed achievement is active."));
|
||||
dialog->registerWidgetHelp(m_ui.notificationLocation, tr("Notification Location"), tr("Top Left"),
|
||||
tr("Selects the screen location for achievement and leaderboard notifications."));
|
||||
dialog->registerWidgetHelp(
|
||||
m_ui.challengeIndicatorMode, tr("Challenge Indicators"), tr("Show Notifications"),
|
||||
tr("Shows a notification or icons in the selected location when a challenge/primed achievement is active."));
|
||||
dialog->registerWidgetHelp(m_ui.indicatorLocation, tr("Indicator Location"), tr("Bottom Right"),
|
||||
tr("Selects the screen location for challenge/progress indicators, and leaderboard trackers."));
|
||||
dialog->registerWidgetHelp(
|
||||
m_ui.progressIndicators, tr("Show Progress Indicators"), tr("Checked"),
|
||||
tr("Shows a popup in the lower-right corner of the screen when progress towards a measured achievement changes."));
|
||||
tr("Shows a popup in the selected location when progress towards a measured achievement changes."));
|
||||
|
||||
connect(m_ui.enable, &QCheckBox::checkStateChanged, this, &AchievementSettingsWidget::updateEnableState);
|
||||
connect(m_ui.hardcoreMode, &QCheckBox::checkStateChanged, this,
|
||||
|
||||
@@ -227,15 +227,8 @@
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QCheckBox" name="leaderboardTrackers">
|
||||
<property name="text">
|
||||
<string>Show Leaderboard Trackers</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_4">
|
||||
<item row="2" column="0" colspan="2">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_4" stretch="1,1">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="soundEffects">
|
||||
<property name="text">
|
||||
@@ -246,7 +239,7 @@
|
||||
<item>
|
||||
<widget class="QLabel" name="changeSoundsLink">
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
|
||||
<set>Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
|
||||
</property>
|
||||
<property name="openExternalLinks">
|
||||
<bool>true</bool>
|
||||
@@ -258,6 +251,16 @@
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Notification Location:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QComboBox" name="notificationLocation"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -278,12 +281,29 @@
|
||||
<widget class="QComboBox" name="challengeIndicatorMode"/>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Indicator Location:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QComboBox" name="indicatorLocation"/>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QCheckBox" name="progressIndicators">
|
||||
<property name="text">
|
||||
<string>Show Progress Indicators</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QCheckBox" name="leaderboardTrackers">
|
||||
<property name="text">
|
||||
<string>Show Leaderboard Trackers</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
|
||||
Reference in New Issue
Block a user