Achievements: Allow changing position of overlays

This commit is contained in:
Stenzek
2026-01-08 17:45:31 +10:00
parent 212b0e6aed
commit 4e0c42100a
18 changed files with 1011 additions and 588 deletions

View File

@@ -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__)

View File

@@ -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;

View File

@@ -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);

View File

@@ -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:

View File

@@ -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, &note_size, ImVec2(0.0f, 0.0f), max_text_width, &note_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)

View File

@@ -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);

View File

@@ -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"));

View File

@@ -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.");

View File

@@ -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, &note_size, ImVec2(0.0f, 0.0f), max_text_width, &note_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,

View File

@@ -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.

View File

@@ -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();

View File

@@ -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));

View File

@@ -39,6 +39,7 @@ enum class RunIdleReason : u8
FullscreenUIActive = (1 << 2),
NotificationsActive = (1 << 3),
LoadingScreenActive = (1 << 4),
AchievementOverlaysActive = (1 << 5),
};
/// Starts Big Picture UI.

View File

@@ -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",

View File

@@ -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;

View File

@@ -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,

View File

@@ -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,

View File

@@ -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>