Repository: RetroAchievements/RAIntegration
Pull request #627 (2020-11-07 23:08:23)

Allow repositioning overlay elements
Replaces most of the checkboxes in the Overlay Settings dialog with positional choice dropboxes:
![image](https://user-images.githubusercontent.com/32680403/98453134-0eb88100-2113-11eb-88e4-cc04e8273ed9.png)

All elements can be placed in any of the four corners and will stack. Here's an example of an achievement notification and a leaderboard tracker both in the Top Right corner:
![image](https://user-images.githubusercontent.com/32680403/98453149-33145d80-2113-11eb-8a02-fa569ef230dc.png)

Setting an element to None will disable it (mimics the behavior of unchecking the box in the previous dialog).




Patch:
diff --git a/src/Exports.cpp b/src/Exports.cpp
index 9f1ae39a..3837b459 100644
--- a/src/Exports.cpp
+++ b/src/Exports.cpp
@@ -409,10 +409,11 @@ static void ProcessAchievements()
                 if (pLeaderboard)
                 {
                     const auto& pConfiguration = ra::services::ServiceLocator::Get<ra::services::IConfiguration>();
-                    if (pConfiguration.IsFeatureEnabled(ra::services::Feature::LeaderboardNotifications))
+                    if (pConfiguration.GetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardStarted) != ra::ui::viewmodels::PopupLocation::None)
                     {
                         ra::services::ServiceLocator::Get<ra::services::IAudioSystem>().PlayAudioFile(L"Overlay\\lb.wav");
-                        ra::services::ServiceLocator::GetMutable<ra::ui::viewmodels::OverlayManager>().QueueMessage(
+                        auto& pOverlayManager = ra::services::ServiceLocator::GetMutable<ra::ui::viewmodels::OverlayManager>();
+                        const int nPopup = pOverlayManager.QueueMessage(ra::ui::viewmodels::Popup::LeaderboardStarted,
                             L"Leaderboard Attempt Started", ra::Widen(pLeaderboard->Title()), ra::Widen(pLeaderboard->Description()));
                     }
 
@@ -448,10 +449,12 @@ static void ProcessAchievements()
                 if (pLeaderboard)
                 {
                     const auto& pConfiguration = ra::services::ServiceLocator::Get<ra::services::IConfiguration>();
-                    if (pConfiguration.IsFeatureEnabled(ra::services::Feature::LeaderboardCancelNotifications))
+                    if (pConfiguration.GetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardCanceled) != ra::ui::viewmodels::PopupLocation::None)
                     {
                         ra::services::ServiceLocator::Get<ra::services::IAudioSystem>().PlayAudioFile(L"Overlay\\lbcancel.wav");
-                        ra::services::ServiceLocator::GetMutable<ra::ui::viewmodels::OverlayManager>().QueueMessage(
+
+                        auto& pOverlayManager = ra::services::ServiceLocator::GetMutable<ra::ui::viewmodels::OverlayManager>();
+                        const int nPopup = pOverlayManager.QueueMessage(ra::ui::viewmodels::Popup::LeaderboardCanceled,
                             L"Leaderboard Attempt Canceled", ra::Widen(pLeaderboard->Title()), ra::Widen(pLeaderboard->Description()));
                     }
 
diff --git a/src/RA_Resource.h b/src/RA_Resource.h
index e4f306a5..1d5620d3 100644
--- a/src/RA_Resource.h
+++ b/src/RA_Resource.h
@@ -66,6 +66,7 @@
 #define IDC_RA_LBL_AUTHOR               1161
 #define IDC_RA_LBL_POINTS               1162
 #define IDC_RA_FREEZE                   1163
+#define IDC_RA_DISPLAY_INFORMATION      1164
 #define IDD_RA_MEMORY                   1501
 #define IDD_RA_ACHIEVEMENTS             1502
 #define IDD_RA_ACHIEVEMENTEDITOR        1503
@@ -170,7 +171,7 @@
 #ifndef APSTUDIO_READONLY_SYMBOLS
 #define _APS_NEXT_RESOURCE_VALUE        121
 #define _APS_NEXT_COMMAND_VALUE         40001
-#define _APS_NEXT_CONTROL_VALUE         1164
+#define _APS_NEXT_CONTROL_VALUE         1165
 #define _APS_NEXT_SYMED_VALUE           101
 #endif
 #endif
diff --git a/src/RA_Shared.rc b/src/RA_Shared.rc
index 53794935..d6994578 100644
--- a/src/RA_Shared.rc
+++ b/src/RA_Shared.rc
@@ -143,7 +143,7 @@ BEGIN
     CONTROL         "Pause on Trigger",IDC_RA_CHK_ACH_PAUSE_ON_TRIGGER,
                     "Button",BS_AUTOCHECKBOX | WS_TABSTOP,336,63,65,10
     CONTROL         "Active",IDC_RA_CHK_ACH_ACTIVE,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,406,63,32,10
-    CONTROL         "",IDC_RA_LBX_CONDITIONS,"SysListView32",LVS_REPORT | LVS_ALIGNLEFT | LVS_NOSORTHEADER | LVS_SHOWSELALWAYS | WS_BORDER | WS_TABSTOP,50,74,388,80
+    CONTROL         "",IDC_RA_LBX_CONDITIONS,"SysListView32",LVS_REPORT | LVS_ALIGNLEFT | LVS_NOSORTHEADER | WS_BORDER | WS_TABSTOP,50,74,388,80
     PUSHBUTTON      "Add New Requirement",IDC_RA_ADDCOND,50,159,48,24,BS_MULTILINE
     PUSHBUTTON      "Delete Selected",IDC_RA_DELETECOND,102,159,48,24,BS_MULTILINE
     PUSHBUTTON      "Copy",IDC_RA_COPYCOND,154,159,32,12,BS_MULTILINE
@@ -273,33 +273,35 @@ BEGIN
     CONTROL         "",IDC_RA_LBX_ADDRESSES,"SysListView32",LVS_REPORT | LVS_SINGLESEL | LVS_ALIGNLEFT | LVS_NOSORTHEADER | WS_CLIPCHILDREN | WS_BORDER,4,37,279,189
 END
 
-IDD_RA_OVERLAYSETTINGS DIALOGEX 0, 0, 199, 144
+IDD_RA_OVERLAYSETTINGS DIALOGEX 0, 0, 240, 170
 STYLE DS_SYSMODAL | DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU
 EXSTYLE WS_EX_TOPMOST
 CAPTION "Overlay Settings"
 FONT 8, "MS Shell Dlg", 400, 0, 0x1
 BEGIN
-    CONTROL         "Display &achievement triggered notification",IDC_RA_DISPLAY_TRIGGER,
-                    "Button",BS_AUTOCHECKBOX | BS_LEFT | WS_TABSTOP,6,6,187,10
+    LTEXT           "&Achievement triggered notification",IDC_STATIC,4,7,168,10
+    COMBOBOX        IDC_RA_DISPLAY_TRIGGER,176,9,60,30,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP
     CONTROL         "&Capture achievement triggered screenshot",IDC_RA_SCREENSHOT_TRIGGER,
-                    "Button",BS_AUTOCHECKBOX | BS_LEFT | WS_TABSTOP,12,17,165,10
-    CONTROL         "Display game &mastery notification",IDC_RA_DISPLAY_MASTERY,
-                    "Button",BS_AUTOCHECKBOX | BS_LEFT | WS_TABSTOP,6,28,187,10
+                    "Button",BS_AUTOCHECKBOX | BS_LEFT | WS_TABSTOP,6,17,165,10
+    LTEXT           "Game &mastery notification",IDC_STATIC,4,33,168,10
+    COMBOBOX        IDC_RA_DISPLAY_MASTERY,176,36,60,30,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP
     CONTROL         "Capture &game mastery screenshot",IDC_RA_SCREENSHOT_MASTERY,
-                    "Button",BS_AUTOCHECKBOX | BS_LEFT | WS_TABSTOP,12,39,165,10
-    CONTROL         "Display &leaderboard started notification",IDC_RA_DISPLAY_LBOARD_START,
-                    "Button",BS_AUTOCHECKBOX | BS_LEFT | WS_TABSTOP,6,54,187,10
-    CONTROL         "Display leader&board canceled notification",IDC_RA_DISPLAY_LBOARD_CANCEL,
-                    "Button",BS_AUTOCHECKBOX | BS_LEFT | WS_TABSTOP,6,65,187,10
-    CONTROL         "Display active leaderboard &time/score values",IDC_RA_DISPLAY_LBOARD_VALUE,
-                    "Button",BS_AUTOCHECKBOX | BS_LEFT | WS_TABSTOP,6,76,187,10
-    CONTROL         "Display &scoreboard after submitting leaderboard entry",IDC_RA_DISPLAY_LBOARD_SCOREBOARD,
-                    "Button",BS_AUTOCHECKBOX | BS_LEFT | WS_TABSTOP,6,87,187,10
-    LTEXT           "Screenshot &Location",IDC_RA_LOCATION,6,99,94,8
-    EDITTEXT        IDC_RA_SCREENSHOT_LOCATION,6,107,172,14,ES_AUTOHSCROLL | ES_READONLY
-    PUSHBUTTON      " ...",IDC_RA_BROWSE,178,107,15,14
-    DEFPUSHBUTTON   "OK",IDOK,100,123,44,15
-    PUSHBUTTON      "Cancel",IDCANCEL,148,123,44,15
+                    "Button",BS_AUTOCHECKBOX | BS_LEFT | WS_TABSTOP,6,43,165,10
+    LTEXT           "&Leaderboard started notification",IDC_STATIC,4,58,168,10
+    COMBOBOX        IDC_RA_DISPLAY_LBOARD_START,176,56,60,30,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP
+    LTEXT           "Leader&board canceled notification",IDC_STATIC,4,72,168,10
+    COMBOBOX        IDC_RA_DISPLAY_LBOARD_CANCEL,176,70,60,30,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP
+    LTEXT           "Active leaderboard &time/score values",IDC_STATIC,4,87,168,10
+    COMBOBOX        IDC_RA_DISPLAY_LBOARD_VALUE,176,84,60,30,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP
+    LTEXT           "&Scoreboard after submitting leaderboard entry",IDC_STATIC,4,101,168,10
+    COMBOBOX        IDC_RA_DISPLAY_LBOARD_SCOREBOARD,176,99,60,30,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP
+    LTEXT           "&Informational notifications",IDC_STATIC,4,115,168,10
+    COMBOBOX        IDC_RA_DISPLAY_INFORMATION,176,113,60,30,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP
+    LTEXT           "Sc&reenshot Location",IDC_RA_LOCATION,4,128,94,8
+    EDITTEXT        IDC_RA_SCREENSHOT_LOCATION,4,137,217,14,ES_AUTOHSCROLL | ES_READONLY
+    PUSHBUTTON      " ...",IDC_RA_BROWSE,221,137,15,14
+    DEFPUSHBUTTON   "OK",IDOK,146,153,44,15
+    PUSHBUTTON      "Cancel",IDCANCEL,193,153,44,15
 END
 
 
diff --git a/src/data/GameContext.cpp b/src/data/GameContext.cpp
index 63f1a0eb..b1b4465f 100644
--- a/src/data/GameContext.cpp
+++ b/src/data/GameContext.cpp
@@ -723,7 +723,7 @@ void GameContext::AwardMastery() const
 
             auto* pPopup = pOverlayManager.GetMessage(nPopup);
             if (pPopup != nullptr)
-                pPopup->SetIsMastery(true);
+                pPopup->SetPopupType(ra::ui::viewmodels::Popup::Mastery);
 
             if (pConfiguration.IsFeatureEnabled(ra::services::Feature::MasteryNotificationScreenshot))
             {
@@ -749,6 +749,7 @@ void GameContext::AwardAchievement(ra::AchievementID nAchievementId) const
     vmPopup->SetDescription(ra::StringPrintf(L"%s (%u)", pAchievement->Title(), pAchievement->Points()));
     vmPopup->SetDetail(ra::Widen(pAchievement->Description()));
     vmPopup->SetImage(ra::ui::ImageType::Badge, pAchievement->BadgeImageURI());
+    vmPopup->SetPopupType(ra::ui::viewmodels::Popup::AchievementTriggered);
 
     switch (pAchievement->GetCategory())
     {
@@ -811,7 +812,7 @@ void GameContext::AwardAchievement(ra::AchievementID nAchievementId) const
 
     int nPopupId = -1;
 
-    if (pConfiguration.IsFeatureEnabled(ra::services::Feature::AchievementTriggeredNotifications))
+    if (pConfiguration.GetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered) != ra::ui::viewmodels::PopupLocation::None)
     {
         ra::services::ServiceLocator::Get<ra::services::IAudioSystem>().PlayAudioFile(
             bIsError ? L"Overlay\\acherror.wav" : L"Overlay\\unlock.wav");
@@ -866,6 +867,7 @@ void GameContext::AwardAchievement(ra::AchievementID nAchievementId) const
                 vmPopup->SetTitle(L"Achievement NOT Unlocked");
                 vmPopup->SetErrorDetail(response.ErrorMessage.empty() ?
                     L"Error submitting unlock" : ra::Widen(response.ErrorMessage));
+                vmPopup->SetPopupType(ra::ui::viewmodels::Popup::AchievementTriggered);
 
                 const auto& pGameContext = ra::services::ServiceLocator::Get<GameContext>();
                 const auto* pAchievement = pGameContext.FindAchievement(nAchievementId);
@@ -885,7 +887,7 @@ void GameContext::AwardAchievement(ra::AchievementID nAchievementId) const
         }
     });
 
-    if (pConfiguration.IsFeatureEnabled(ra::services::Feature::MasteryNotification))
+    if (pConfiguration.GetPopupLocation(ra::ui::viewmodels::Popup::Mastery) != ra::ui::viewmodels::PopupLocation::None)
     {
         bool bHasCoreAchievement = false;
         bool bActiveCoreAchievement = false;
@@ -1083,7 +1085,7 @@ void GameContext::SubmitLeaderboardEntry(ra::LeaderboardID nLeaderboardId, int n
         else if (pLeaderboard)
         {
             auto& pConfiguration = ra::services::ServiceLocator::Get<ra::services::IConfiguration>();
-            if (pConfiguration.IsFeatureEnabled(ra::services::Feature::LeaderboardScoreboards))
+            if (pConfiguration.GetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard) != ra::ui::viewmodels::PopupLocation::None)
             {
                 ra::ui::viewmodels::ScoreboardViewModel vmScoreboard;
                 vmScoreboard.SetHeaderText(ra::Widen(pLeaderboard->Title()));
diff --git a/src/services/IConfiguration.hh b/src/services/IConfiguration.hh
index 8ee24bba..1236aa1e 100644
--- a/src/services/IConfiguration.hh
+++ b/src/services/IConfiguration.hh
@@ -3,6 +3,7 @@
 #pragma once
 
 #include "ui\Types.hh"
+#include "ui\viewmodels\PopupViewModelBase.hh"
 
 namespace ra {
 namespace services {
@@ -12,18 +13,13 @@ enum class Feature
     None = 0,
     Hardcore,
     Leaderboards,
-    LeaderboardNotifications,
-    LeaderboardCancelNotifications,
-    LeaderboardCounters,
-    LeaderboardScoreboards,
     PreferDecimal,
     NonHardcoreWarning,
     AchievementTriggeredScreenshot,
-    AchievementTriggeredNotifications,
-    MasteryNotification,
     MasteryNotificationScreenshot,
 };
 
+
 class IConfiguration
 {
 public:
@@ -63,6 +59,16 @@ public:
     /// </summary>
     virtual void SetFeatureEnabled(Feature nFeature, bool bEnabled) = 0;
 
+    /// <summary>
+    /// Gets where the specified popup should be displayed.
+    /// </summary>
+    virtual ra::ui::viewmodels::PopupLocation GetPopupLocation(ra::ui::viewmodels::Popup nPopup) const = 0;
+
+    /// <summary>
+    /// Sets where the specified popup should be displayed.
+    /// </summary>
+    virtual void SetPopupLocation(ra::ui::viewmodels::Popup nPopup, ra::ui::viewmodels::PopupLocation nPopupLocation) = 0;
+
     /// <summary>
     /// Gets the number of background threads to spawn.
     /// </summary>
diff --git a/src/services/impl/JsonFileConfiguration.cpp b/src/services/impl/JsonFileConfiguration.cpp
index 3fcb8b96..21e15767 100644
--- a/src/services/impl/JsonFileConfiguration.cpp
+++ b/src/services/impl/JsonFileConfiguration.cpp
@@ -12,6 +12,51 @@ namespace ra {
 namespace services {
 namespace impl {
 
+static void ReadPopupLocation(JsonFileConfiguration& pConfiguration, ra::ui::viewmodels::Popup nPopup,
+    const rapidjson::Document& doc, const char* sFieldName, ra::ui::viewmodels::PopupLocation nDefaultLocation, bool bAllowCenter)
+{
+    ra::ui::viewmodels::PopupLocation nLocation = nDefaultLocation;
+
+    if (doc.HasMember(sFieldName))
+    {
+        const auto& pField = doc[sFieldName];
+        if (pField.IsBool())
+        {
+            if (!pField.GetBool())
+                nLocation = ra::ui::viewmodels::PopupLocation::None;
+        }
+        else if (pField.IsString())
+        {
+            const char* pValue = pField.GetString();
+            Expects(pValue != nullptr);
+            if (*pValue == 'T')
+            {
+                if (strcmp(pValue, "TopLeft") == 0)
+                    nLocation = ra::ui::viewmodels::PopupLocation::TopLeft;
+                else if (strcmp(pValue, "TopRight") == 0)
+                    nLocation = ra::ui::viewmodels::PopupLocation::TopRight;
+                else if (bAllowCenter && strcmp(pValue, "TopMiddle") == 0)
+                    nLocation = ra::ui::viewmodels::PopupLocation::TopMiddle;
+            }
+            else if (*pValue == 'B')
+            {
+                if (strcmp(pValue, "BottomLeft") == 0)
+                    nLocation = ra::ui::viewmodels::PopupLocation::BottomLeft;
+                else if (strcmp(pValue, "BottomRight") == 0)
+                    nLocation = ra::ui::viewmodels::PopupLocation::BottomRight;
+                else if (bAllowCenter && strcmp(pValue, "BottomMiddle") == 0)
+                    nLocation = ra::ui::viewmodels::PopupLocation::BottomMiddle;
+            }
+            else if (strcmp(pValue, "None") == 0)
+            {
+                nLocation = ra::ui::viewmodels::PopupLocation::None;
+            }
+        }
+    }
+
+    pConfiguration.SetPopupLocation(nPopup, nLocation);
+}
+
 bool JsonFileConfiguration::Load(const std::wstring& sFilename)
 {
     m_sFilename = sFilename;
@@ -24,13 +69,14 @@ bool JsonFileConfiguration::Load(const std::wstring& sFilename)
     m_nBackgroundThreads = 8;
     m_vEnabledFeatures =
         (1 << static_cast<int>(Feature::Hardcore)) |
-        (1 << static_cast<int>(Feature::AchievementTriggeredNotifications)) |
-        (1 << static_cast<int>(Feature::MasteryNotification)) |
-        (1 << static_cast<int>(Feature::Leaderboards)) |
-        (1 << static_cast<int>(Feature::LeaderboardNotifications)) |
-        (1 << static_cast<int>(Feature::LeaderboardCancelNotifications)) |
-        (1 << static_cast<int>(Feature::LeaderboardCounters)) |
-        (1 << static_cast<int>(Feature::LeaderboardScoreboards));
+        (1 << static_cast<int>(Feature::Leaderboards));
+    SetPopupLocation(ra::ui::viewmodels::Popup::Message, ra::ui::viewmodels::PopupLocation::BottomLeft);
+    SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, ra::ui::viewmodels::PopupLocation::BottomLeft);
+    SetPopupLocation(ra::ui::viewmodels::Popup::Mastery, ra::ui::viewmodels::PopupLocation::TopMiddle);
+    SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardStarted, ra::ui::viewmodels::PopupLocation::BottomLeft);
+    SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardCanceled, ra::ui::viewmodels::PopupLocation::BottomLeft);
+    SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardTracker, ra::ui::viewmodels::PopupLocation::BottomRight);
+    SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard, ra::ui::viewmodels::PopupLocation::BottomRight);
 
     RA_LOG_INFO("Loading preferences...");
 
@@ -52,12 +98,10 @@ bool JsonFileConfiguration::Load(const std::wstring& sFilename)
     if (doc.HasMember("Non Hardcore Warning"))
         SetFeatureEnabled(Feature::NonHardcoreWarning, doc["Non Hardcore Warning"].GetBool());
 
-    if (doc.HasMember("Achievement Triggered Notification Display"))
-        SetFeatureEnabled(Feature::AchievementTriggeredNotifications, doc["Achievement Triggered Notification Display"].GetBool());
+    ReadPopupLocation(*this, ra::ui::viewmodels::Popup::AchievementTriggered, doc, "Achievement Triggered Notification Display", ra::ui::viewmodels::PopupLocation::BottomLeft, true);
     if (doc.HasMember("Achievement Triggered Screenshot"))
         SetFeatureEnabled(Feature::AchievementTriggeredScreenshot, doc["Achievement Triggered Screenshot"].GetBool());
-    if (doc.HasMember("Mastery Notification Display"))
-        SetFeatureEnabled(Feature::MasteryNotification, doc["Mastery Notification Display"].GetBool());
+    ReadPopupLocation(*this, ra::ui::viewmodels::Popup::Mastery, doc, "Mastery Notification Display", ra::ui::viewmodels::PopupLocation::TopMiddle, true);
     if (doc.HasMember("Mastery Screenshot"))
         SetFeatureEnabled(Feature::MasteryNotificationScreenshot, doc["Mastery Screenshot"].GetBool());
     if (doc.HasMember("Screenshot Directory"))
@@ -65,14 +109,12 @@ bool JsonFileConfiguration::Load(const std::wstring& sFilename)
 
     if (doc.HasMember("Leaderboards Active"))
         SetFeatureEnabled(Feature::Leaderboards, doc["Leaderboards Active"].GetBool());
-    if (doc.HasMember("Leaderboard Notification Display"))
-        SetFeatureEnabled(Feature::LeaderboardNotifications, doc["Leaderboard Notification Display"].GetBool());
-    if (doc.HasMember("Leaderboard Cancel Display"))
-        SetFeatureEnabled(Feature::LeaderboardCancelNotifications, doc["Leaderboard Cancel Display"].GetBool());
-    if (doc.HasMember("Leaderboard Counter Display"))
-        SetFeatureEnabled(Feature::LeaderboardCounters, doc["Leaderboard Counter Display"].GetBool());
-    if (doc.HasMember("Leaderboard Scoreboard Display"))
-        SetFeatureEnabled(Feature::LeaderboardScoreboards, doc["Leaderboard Scoreboard Display"].GetBool());
+    ReadPopupLocation(*this, ra::ui::viewmodels::Popup::LeaderboardStarted, doc, "Leaderboard Notification Display", ra::ui::viewmodels::PopupLocation::BottomLeft, true);
+    ReadPopupLocation(*this, ra::ui::viewmodels::Popup::LeaderboardCanceled, doc, "Leaderboard Cancel Display", ra::ui::viewmodels::PopupLocation::BottomLeft, true);
+    ReadPopupLocation(*this, ra::ui::viewmodels::Popup::LeaderboardTracker, doc, "Leaderboard Counter Display", ra::ui::viewmodels::PopupLocation::BottomRight, true);
+    ReadPopupLocation(*this, ra::ui::viewmodels::Popup::LeaderboardScoreboard, doc, "Leaderboard Scoreboard Display", ra::ui::viewmodels::PopupLocation::BottomRight, false);
+
+    ReadPopupLocation(*this, ra::ui::viewmodels::Popup::Message, doc, "Informational Notification Display", ra::ui::viewmodels::PopupLocation::BottomLeft, true);
 
     if (doc.HasMember("Prefer Decimal"))
         SetFeatureEnabled(Feature::PreferDecimal, doc["Prefer Decimal"].GetBool());
@@ -107,6 +149,35 @@ bool JsonFileConfiguration::Load(const std::wstring& sFilename)
     return true;
 }
 
+static void WritePopupLocation(rapidjson::Document& doc, rapidjson::Document::AllocatorType& a,
+    const char* sFieldName, ra::ui::viewmodels::PopupLocation nPopupLocation)
+{
+    switch (nPopupLocation)
+    {
+        case ra::ui::viewmodels::PopupLocation::None:
+            doc.AddMember(rapidjson::StringRef(sFieldName), rapidjson::StringRef("None"), a);
+            break;
+        case ra::ui::viewmodels::PopupLocation::TopLeft:
+            doc.AddMember(rapidjson::StringRef(sFieldName), rapidjson::StringRef("TopLeft"), a);
+            break;
+        case ra::ui::viewmodels::PopupLocation::TopMiddle:
+            doc.AddMember(rapidjson::StringRef(sFieldName), rapidjson::StringRef("TopMiddle"), a);
+            break;
+        case ra::ui::viewmodels::PopupLocation::TopRight:
+            doc.AddMember(rapidjson::StringRef(sFieldName), rapidjson::StringRef("TopRight"), a);
+            break;
+        case ra::ui::viewmodels::PopupLocation::BottomLeft:
+            doc.AddMember(rapidjson::StringRef(sFieldName), rapidjson::StringRef("BottomLeft"), a);
+            break;
+        case ra::ui::viewmodels::PopupLocation::BottomMiddle:
+            doc.AddMember(rapidjson::StringRef(sFieldName), rapidjson::StringRef("BottomMiddle"), a);
+            break;
+        case ra::ui::viewmodels::PopupLocation::BottomRight:
+            doc.AddMember(rapidjson::StringRef(sFieldName), rapidjson::StringRef("BottomRight"), a);
+            break;
+    }
+}
+
 void JsonFileConfiguration::Save() const
 {
     RA_LOG_INFO("Saving preferences...");
@@ -125,15 +196,16 @@ void JsonFileConfiguration::Save() const
     doc.AddMember("Token", rapidjson::StringRef(m_sApiToken), a);
     doc.AddMember("Hardcore Active", IsFeatureEnabled(Feature::Hardcore), a);
     doc.AddMember("Non Hardcore Warning", IsFeatureEnabled(Feature::NonHardcoreWarning), a);
-    doc.AddMember("Achievement Triggered Notification Display", IsFeatureEnabled(Feature::AchievementTriggeredNotifications), a);
+    WritePopupLocation(doc, a, "Achievement Triggered Notification Display", GetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered));
     doc.AddMember("Achievement Triggered Screenshot", IsFeatureEnabled(Feature::AchievementTriggeredScreenshot), a);
-    doc.AddMember("Mastery Notification Display", IsFeatureEnabled(Feature::MasteryNotification), a);
+    WritePopupLocation(doc, a, "Mastery Notification Display", GetPopupLocation(ra::ui::viewmodels::Popup::Mastery));
     doc.AddMember("Mastery Screenshot", IsFeatureEnabled(Feature::MasteryNotificationScreenshot), a);
     doc.AddMember("Leaderboards Active", IsFeatureEnabled(Feature::Leaderboards), a);
-    doc.AddMember("Leaderboard Notification Display", IsFeatureEnabled(Feature::LeaderboardNotifications), a);
-    doc.AddMember("Leaderboard Cancel Display", IsFeatureEnabled(Feature::LeaderboardCancelNotifications), a);
-    doc.AddMember("Leaderboard Counter Display", IsFeatureEnabled(Feature::LeaderboardCounters), a);
-    doc.AddMember("Leaderboard Scoreboard Display", IsFeatureEnabled(Feature::LeaderboardScoreboards), a);
+    WritePopupLocation(doc, a, "Leaderboard Notification Display", GetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardStarted));
+    WritePopupLocation(doc, a, "Leaderboard Cancel Display", GetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardCanceled));
+    WritePopupLocation(doc, a, "Leaderboard Counter Display", GetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardTracker));
+    WritePopupLocation(doc, a, "Leaderboard Scoreboard Display", GetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard));
+    WritePopupLocation(doc, a, "Informational Notification Display", GetPopupLocation(ra::ui::viewmodels::Popup::Message));
     doc.AddMember("Prefer Decimal", IsFeatureEnabled(Feature::PreferDecimal), a);
     doc.AddMember("Num Background Threads", m_nBackgroundThreads, a);
 
@@ -184,6 +256,16 @@ void JsonFileConfiguration::SetFeatureEnabled(Feature nFeature, bool bEnabled) n
         m_vEnabledFeatures &= ~bit;
 }
 
+ra::ui::viewmodels::PopupLocation JsonFileConfiguration::GetPopupLocation(ra::ui::viewmodels::Popup nPopup) const
+{
+    return m_vPopupLocations.at(ra::etoi(nPopup));
+}
+
+void JsonFileConfiguration::SetPopupLocation(ra::ui::viewmodels::Popup nPopup, ra::ui::viewmodels::PopupLocation nPopupLocation)
+{
+    m_vPopupLocations.at(ra::etoi(nPopup)) = nPopupLocation;
+}
+
 ra::ui::Position JsonFileConfiguration::GetWindowPosition(const std::string& sPositionKey) const
 {
     const WindowPositionMap::const_iterator iter = m_mWindowPositions.find(sPositionKey);
diff --git a/src/services/impl/JsonFileConfiguration.hh b/src/services/impl/JsonFileConfiguration.hh
index 4de0572b..ff375972 100644
--- a/src/services/impl/JsonFileConfiguration.hh
+++ b/src/services/impl/JsonFileConfiguration.hh
@@ -21,6 +21,9 @@ public:
     bool IsFeatureEnabled(Feature nFeature) const noexcept override;
     void SetFeatureEnabled(Feature nFeature, bool bEnabled) noexcept override;
 
+    ra::ui::viewmodels::PopupLocation GetPopupLocation(ra::ui::viewmodels::Popup nPopup) const override;
+    void SetPopupLocation(ra::ui::viewmodels::Popup nPopup, ra::ui::viewmodels::PopupLocation nPopupLocation) override;
+
     unsigned int GetNumBackgroundThreads() const noexcept override { return m_nBackgroundThreads; }
 
     const std::wstring& GetRomDirectory() const noexcept override { return m_sRomDirectory; }
@@ -50,6 +53,8 @@ private:
 
     int m_vEnabledFeatures = 0;
 
+    std::array<ra::ui::viewmodels::PopupLocation, ra::etoi(ra::ui::viewmodels::Popup::NumPopups)> m_vPopupLocations = {};
+
     unsigned int m_nBackgroundThreads = 8;
     std::wstring m_sRomDirectory;
     std::wstring m_sScreenshotDirectory;
diff --git a/src/ui/viewmodels/OverlayManager.cpp b/src/ui/viewmodels/OverlayManager.cpp
index 312289d7..82886d69 100644
--- a/src/ui/viewmodels/OverlayManager.cpp
+++ b/src/ui/viewmodels/OverlayManager.cpp
@@ -16,31 +16,13 @@ namespace ra {
 namespace ui {
 namespace viewmodels {
 
-static int GetAbsolutePosition(int nValue, RelativePosition nRelativePosition, unsigned int nFarValue) noexcept
-{
-    switch (nRelativePosition)
-    {
-        case RelativePosition::Before:
-            return -nValue;
-        default:
-        case RelativePosition::Near:
-            return nValue;
-        case RelativePosition::Center:
-            return ra::to_signed((nFarValue - nValue) / 2);
-        case RelativePosition::Far:
-            return ra::to_signed(nFarValue - nValue);
-        case RelativePosition::After:
-            return ra::to_signed(nFarValue + nValue);
-    }
-}
-
 static void RenderPopup(ra::ui::drawing::ISurface& pSurface, const PopupViewModelBase& vmPopup)
 {
     if (!vmPopup.IsAnimationStarted())
         return;
 
-    const int nX = GetAbsolutePosition(vmPopup.GetRenderLocationX(), vmPopup.GetRenderLocationXRelativePosition(), pSurface.GetWidth());
-    const int nY = GetAbsolutePosition(vmPopup.GetRenderLocationY(), vmPopup.GetRenderLocationYRelativePosition(), pSurface.GetHeight());
+    const auto nX = vmPopup.GetRenderLocationX();
+    const auto nY = vmPopup.GetRenderLocationY();
     pSurface.DrawSurface(nX, nY, vmPopup.GetRenderImage());
 }
 
@@ -49,8 +31,8 @@ static void RenderPopupClippedX(ra::ui::drawing::ISurface& pSurface, const Popup
     if (!vmPopup.IsAnimationStarted())
         return;
 
-    const int nX = GetAbsolutePosition(vmPopup.GetRenderLocationX(), vmPopup.GetRenderLocationXRelativePosition(), pSurface.GetWidth());
-    const int nY = GetAbsolutePosition(vmPopup.GetRenderLocationY(), vmPopup.GetRenderLocationYRelativePosition(), pSurface.GetHeight());
+    const auto nX = vmPopup.GetRenderLocationX();
+    const auto nY = vmPopup.GetRenderLocationY();
 
     const auto& pImage = vmPopup.GetRenderImage();
     const int nVisibleWidth = nX + ra::to_signed(pImage.GetWidth()) - nClipX;
@@ -158,31 +140,36 @@ void OverlayManager::Render(ra::ui::drawing::ISurface& pSurface, bool bRedrawAll
 
     if (m_vmOverlay.CurrentState() == OverlayViewModel::State::Hidden)
     {
-        // update the visible popups - if any are updated, m_bRedrawAll will be set
-        if (!m_vScoreboards.empty())
+        PopupLocations pPopupLocations;
+        memset(&pPopupLocations, 0, sizeof(pPopupLocations));
+
+        // layout pass - stack items in desired order
+
+        if (!m_vPopupMessages.empty())
         {
-            UpdateActiveScoreboard(pSurface, fElapsed);
+            UpdateActiveMessage(pSurface, pPopupLocations, fElapsed);
 
-            // a visible scoreboard may not set m_bRedrawAll if it's in the pause portion of its animation loop
+            // a visible popup may not set m_bRedrawAll if it's in the pause portion of its animation loop
             // make sure we keep the render cycle going so it'll eventually handle when the pause ends
             bRequestRender = true;
         }
 
-        if (!m_vScoreTrackers.empty())
-            UpdateScoreTrackers(pSurface, fElapsed);
-
-        if (!m_vPopupMessages.empty())
+        if (!m_vScoreboards.empty())
         {
-            UpdateActiveMessage(pSurface, fElapsed);
+            UpdateActiveScoreboard(pSurface, pPopupLocations, fElapsed);
 
-            // a visible popup may not set m_bRedrawAll if it's in the pause portion of its animation loop
+            // a visible scoreboard may not set m_bRedrawAll if it's in the pause portion of its animation loop
             // make sure we keep the render cycle going so it'll eventually handle when the pause ends
             bRequestRender = true;
         }
 
+        if (!m_vScoreTrackers.empty())
+            UpdateScoreTrackers(pSurface, pPopupLocations, fElapsed);
+
         // if anything changed, or caller requested a repaint, do so now
         if (m_bRedrawAll)
         {
+            // render pass - items that should appear over other items should be drawn last
             if (!m_vScoreboards.empty())
                 RenderPopup(pSurface, m_vScoreboards.front());
             for (const auto& pScoreTracker : m_vScoreTrackers)
@@ -291,62 +278,171 @@ void OverlayManager::ClearPopups()
     RequestRender();
 }
 
-void OverlayManager::UpdatePopup(ra::ui::drawing::ISurface& pSurface, double fElapsed, ra::ui::viewmodels::PopupViewModelBase& vmPopup)
+ra::ui::Position OverlayManager::GetRenderLocation(const ra::ui::viewmodels::PopupViewModelBase& vmPopup,
+    int nX, int nY, const ra::ui::drawing::ISurface& pSurface, const PopupLocations& pPopupLocations)
+{
+    const auto& pConfiguration = ra::services::ServiceLocator::Get<ra::services::IConfiguration>();
+    const auto nPopupLocation = pConfiguration.GetPopupLocation(vmPopup.GetPopupType());
+
+    Position nPos;
+    nPos.X = nX;
+    nPos.Y = nY;
+
+    // horizontal alignment
+    switch (nPopupLocation)
+    {
+        default:
+            break;
+        case PopupLocation::TopMiddle:
+        case PopupLocation::BottomMiddle:
+            nPos.X = (pSurface.GetWidth() - vmPopup.GetRenderImage().GetWidth()) / 2;
+            break;
+        case PopupLocation::TopRight:
+        case PopupLocation::BottomRight:
+            nPos.X = pSurface.GetWidth() - vmPopup.GetRenderImage().GetWidth() - nPos.X;
+            break;
+    }
+
+    // vertical alignment
+    switch (nPopupLocation)
+    {
+        default:
+            break;
+        case PopupLocation::TopLeft:
+            nPos.Y += pPopupLocations.nTopLeftY;
+            break;
+        case PopupLocation::TopMiddle:
+            nPos.Y += pPopupLocations.nTopMiddleY;
+            break;
+        case PopupLocation::TopRight:
+            nPos.Y += pPopupLocations.nTopRightY;
+            break;
+        case PopupLocation::BottomLeft:
+            nPos.Y = pSurface.GetHeight() - vmPopup.GetRenderImage().GetHeight() - nPos.Y - pPopupLocations.nBottomLeftY;
+            break;
+        case PopupLocation::BottomMiddle:
+            nPos.Y = pSurface.GetHeight() - vmPopup.GetRenderImage().GetHeight() - nPos.Y - pPopupLocations.nBottomMiddleY;
+            break;
+        case PopupLocation::BottomRight:
+            nPos.Y = pSurface.GetHeight() - vmPopup.GetRenderImage().GetHeight() - nPos.Y - pPopupLocations.nBottomRightY;
+            break;
+    }
+
+    return nPos;
+}
+
+void OverlayManager::UpdatePopup(ra::ui::drawing::ISurface& pSurface, const PopupLocations& pPopupLocations, double fElapsed, ra::ui::viewmodels::PopupViewModelBase& vmPopup)
 {
+    constexpr int nFudge = 4;
+    unsigned nOldWidth = 0, nOldHeight = 0;
+
     if (!vmPopup.IsAnimationStarted())
     {
         vmPopup.BeginAnimation();
-        vmPopup.UpdateRenderImage(0.0);
         fElapsed = 0.0;
+        nOldWidth = nOldHeight = 0;
+    }
+    else
+    {
+        const auto& pImage = vmPopup.GetRenderImage();
+        nOldWidth = pImage.GetWidth();
+        nOldHeight = pImage.GetHeight();
     }
 
-    const int nOldX = GetAbsolutePosition(vmPopup.GetRenderLocationX(), vmPopup.GetRenderLocationXRelativePosition(), pSurface.GetWidth());
-    const int nOldY = GetAbsolutePosition(vmPopup.GetRenderLocationY(), vmPopup.GetRenderLocationYRelativePosition(), pSurface.GetHeight());
+    const auto nOldX = vmPopup.GetRenderLocationX();
+    const auto nOldY = vmPopup.GetRenderLocationY();
 
     if (vmPopup.IsDestroyPending())
     {
-        const auto& pImage = vmPopup.GetRenderImage();
-        pSurface.FillRectangle(nOldX, nOldY, pImage.GetWidth(), pImage.GetHeight(), ra::ui::Color::Transparent);
+        pSurface.FillRectangle(nOldX, nOldY, nOldWidth, nOldHeight, ra::ui::Color::Transparent);
         m_bRedrawAll = true;
         return;
     }
 
-    const bool bUpdated = vmPopup.UpdateRenderImage(fElapsed);
+    m_bRedrawAll |= vmPopup.UpdateRenderImage(fElapsed);
 
-    const int nNewX = GetAbsolutePosition(vmPopup.GetRenderLocationX(), vmPopup.GetRenderLocationXRelativePosition(), pSurface.GetWidth());
-    const int nNewY = GetAbsolutePosition(vmPopup.GetRenderLocationY(), vmPopup.GetRenderLocationYRelativePosition(), pSurface.GetHeight());
+    const auto& pNewImage = vmPopup.GetRenderImage();
 
-    if (bUpdated)
+    const auto nNewPos = GetRenderLocation(vmPopup, vmPopup.GetHorizontalOffset(),
+        vmPopup.GetVerticalOffset(), pSurface, pPopupLocations);
+
+    // eliminate any excess to the left or right of the popup
+    if (nOldX != nNewPos.X || nOldWidth != pNewImage.GetWidth())
     {
-        // if the image moved, we have to blank out the area it previously covered
-        if (nOldY != nNewY || nOldX != nNewX)
+        vmPopup.SetRenderLocationX(nNewPos.X);
+
+        if (nOldWidth != 0)
         {
-            const auto& pImage = vmPopup.GetRenderImage();
-            const int nWidth = pImage.GetWidth() + std::abs(nNewX - nOldX);
-            const int nHeight = pImage.GetHeight() + std::abs(nNewY - nOldY);
-
-            if (nNewX > nOldX)
-                pSurface.FillRectangle(nOldX, nOldY, nNewX - nOldX, nHeight, ra::ui::Color::Transparent);
-            else if (nNewX < nOldX)
-                pSurface.FillRectangle(nNewX + pImage.GetWidth() - 1, nOldY, nOldX - nNewX + 1, nHeight, ra::ui::Color::Transparent);
-
-            if (nNewY > nOldY)
-                pSurface.FillRectangle(nOldX, nOldY, nWidth, nNewY - nOldY, ra::ui::Color::Transparent);
-            else if (nNewY < nOldY)
-                pSurface.FillRectangle(nOldX, nNewY + pImage.GetHeight() - 1, nWidth, nOldY - nNewY + 1, ra::ui::Color::Transparent);
+            const int nHeight = pNewImage.GetHeight() + std::abs(nNewPos.Y - nOldY);
+            if (nNewPos.X > nOldX)
+                pSurface.FillRectangle(nOldX - nFudge, nOldY, nNewPos.X - nOldX + nFudge, nHeight, ra::ui::Color::Transparent);
+
+            const int nOldRightBound = nOldX + nOldWidth;
+            const int nNewRightBound = nNewPos.X + pNewImage.GetWidth();
+            if (nNewRightBound < nOldRightBound)
+                pSurface.FillRectangle(nNewRightBound, nOldY, nOldRightBound - nNewRightBound + nFudge, nHeight, ra::ui::Color::Transparent);
         }
 
         m_bRedrawAll = true;
     }
+
+    // eliminate any excess above or below the popup
+    if (nOldY != nNewPos.Y || nOldHeight != pNewImage.GetHeight())
+    {
+        vmPopup.SetRenderLocationY(nNewPos.Y);
+
+        if (nOldHeight != 0)
+        {
+            if (nNewPos.Y > nOldY)
+                pSurface.FillRectangle(nNewPos.X, nOldY - nFudge, pNewImage.GetWidth(), nNewPos.Y - nOldY + nFudge, ra::ui::Color::Transparent);
+
+            const int nOldLowerBound = nOldY + nOldHeight;
+            const int nNewLowerBound = nNewPos.Y + pNewImage.GetHeight();
+            if (nNewLowerBound < nOldLowerBound)
+                pSurface.FillRectangle(nNewPos.X, nNewLowerBound, pNewImage.GetWidth(), nOldLowerBound - nNewLowerBound + nFudge, ra::ui::Color::Transparent);
+        }
+
+        m_bRedrawAll = true;
+    }
+}
+
+void OverlayManager::AdjustLocationForPopup(PopupLocations& pPopupLocations, const ra::ui::viewmodels::PopupViewModelBase& vmPopup)
+{
+    const auto& pConfiguration = ra::services::ServiceLocator::Get<ra::services::IConfiguration>();
+    const auto nPopupLocation = pConfiguration.GetPopupLocation(vmPopup.GetPopupType());
+    const auto nHeight = vmPopup.GetRenderImage().GetHeight();
+    const auto nOffset = vmPopup.GetVerticalOffset() + nHeight;
+
+    switch (nPopupLocation)
+    {
+        case PopupLocation::TopLeft:
+            pPopupLocations.nTopLeftY += nOffset;
+            break;
+        case PopupLocation::TopMiddle:
+            pPopupLocations.nTopMiddleY += nOffset;
+            break;
+        case PopupLocation::TopRight:
+            pPopupLocations.nTopRightY += nOffset;
+            break;
+        case PopupLocation::BottomLeft:
+            pPopupLocations.nBottomLeftY += nOffset;
+            break;
+        case PopupLocation::BottomMiddle:
+            pPopupLocations.nBottomMiddleY += nOffset;
+            break;
+        case PopupLocation::BottomRight:
+            pPopupLocations.nBottomRightY += nOffset;
+            break;
+    }
 }
 
-void OverlayManager::UpdateActiveMessage(ra::ui::drawing::ISurface& pSurface, double fElapsed)
+void OverlayManager::UpdateActiveMessage(ra::ui::drawing::ISurface& pSurface, PopupLocations& pPopupLocations, double fElapsed)
 {
     assert(!m_vPopupMessages.empty());
 
     auto* pActiveMessage = m_vPopupMessages.front().get();
     Expects(pActiveMessage != nullptr);
-    UpdatePopup(pSurface, fElapsed, *pActiveMessage);
+    UpdatePopup(pSurface, pPopupLocations, fElapsed, *pActiveMessage);
 
     while (pActiveMessage->IsAnimationComplete() || pActiveMessage->IsDestroyPending())
     {
@@ -356,17 +452,19 @@ void OverlayManager::UpdateActiveMessage(ra::ui::drawing::ISurface& pSurface, do
 
         pActiveMessage = m_vPopupMessages.front().get();
         Expects(pActiveMessage != nullptr);
-        pActiveMessage->BeginAnimation();
-        pActiveMessage->UpdateRenderImage(0.0);
+        UpdatePopup(pSurface, pPopupLocations, fElapsed, *pActiveMessage);
     }
+
+    if (!m_vPopupMessages.empty())
+        AdjustLocationForPopup(pPopupLocations, *m_vPopupMessages.front());
 }
 
-void OverlayManager::UpdateActiveScoreboard(ra::ui::drawing::ISurface& pSurface, double fElapsed)
+void OverlayManager::UpdateActiveScoreboard(ra::ui::drawing::ISurface& pSurface, PopupLocations& pPopupLocations, double fElapsed)
 {
     assert(!m_vScoreboards.empty());
 
     auto* pActiveScoreboard = &m_vScoreboards.front();
-    UpdatePopup(pSurface, fElapsed, *pActiveScoreboard);
+    UpdatePopup(pSurface, pPopupLocations, fElapsed, *pActiveScoreboard);
 
     while (pActiveScoreboard->IsAnimationComplete() || pActiveScoreboard->IsDestroyPending())
     {
@@ -378,14 +476,17 @@ void OverlayManager::UpdateActiveScoreboard(ra::ui::drawing::ISurface& pSurface,
         pActiveScoreboard->BeginAnimation();
         pActiveScoreboard->UpdateRenderImage(0.0);
     }
+
+    if (!m_vScoreboards.empty())
+        AdjustLocationForPopup(pPopupLocations, m_vScoreboards.front());
 }
 
-void OverlayManager::UpdateScoreTrackers(ra::ui::drawing::ISurface& pSurface, double fElapsed)
+void OverlayManager::UpdateScoreTrackers(ra::ui::drawing::ISurface& pSurface, PopupLocations& pPopupLocations, double fElapsed)
 {
     assert(!m_vScoreTrackers.empty());
 
     auto& pConfiguration = ra::services::ServiceLocator::Get<ra::services::IConfiguration>();
-    if (!pConfiguration.IsFeatureEnabled(ra::services::Feature::LeaderboardCounters))
+    if (pConfiguration.GetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardTracker) == ra::ui::viewmodels::PopupLocation::None)
     {
         auto pIter = m_vScoreTrackers.begin();
         while (pIter != m_vScoreTrackers.end())
@@ -399,43 +500,29 @@ void OverlayManager::UpdateScoreTrackers(ra::ui::drawing::ISurface& pSurface, do
         return;
     }
 
-    int nOffset = 10;
-    if (!m_vScoreboards.empty())
-        nOffset += m_vScoreboards.front().GetRenderImage().GetHeight() + 10;
-
-    bool bPopupMoved = false;
     auto pIter = m_vScoreTrackers.begin();
     while (pIter != m_vScoreTrackers.end())
     {
         auto& vmTracker = **pIter;
+        UpdatePopup(pSurface, pPopupLocations, fElapsed, vmTracker);
+
         if (vmTracker.IsDestroyPending())
         {
-            bPopupMoved = true;
-            UpdatePopup(pSurface, fElapsed, vmTracker);
             pIter = m_vScoreTrackers.erase(pIter);
         }
         else
         {
-            bPopupMoved |= vmTracker.SetOffset(nOffset);
-            UpdatePopup(pSurface, fElapsed, vmTracker);
-            nOffset += vmTracker.GetRenderImage().GetHeight() + 10;
+            AdjustLocationForPopup(pPopupLocations, vmTracker);
             ++pIter;
         }
     }
-
-    // if one or more of the popups moved, we will have drawn a transparent square where the popup used to be. redraw all the popups to make sure we didn't erase one
-    if (bPopupMoved)
-    {
-        for (auto& vmTracker : m_vScoreTrackers)
-            RenderPopup(pSurface, *vmTracker);
-
-        if (!m_vScoreboards.empty())
-            RenderPopup(pSurface, m_vScoreboards.front());
-    }
 }
 
 void OverlayManager::UpdateOverlay(ra::ui::drawing::ISurface& pSurface, double fElapsed)
 {
+    PopupLocations pPopupLocations;
+    memset(&pPopupLocations, 0, sizeof(pPopupLocations));
+
     if (pSurface.GetWidth() != m_vmOverlay.GetRenderImage().GetWidth() ||
         pSurface.GetHeight() != m_vmOverlay.GetRenderImage().GetHeight())
     {
@@ -445,8 +532,7 @@ void OverlayManager::UpdateOverlay(ra::ui::drawing::ISurface& pSurface, double f
     if (m_vmOverlay.CurrentState() == OverlayViewModel::State::FadeOut)
     {
         // when fading out, have to redraw the popups that were obscured by the overlay
-        const int nOverlayRight = GetAbsolutePosition(m_vmOverlay.GetRenderLocationX(),
-            m_vmOverlay.GetRenderLocationXRelativePosition(), pSurface.GetWidth()) +
+        const int nOverlayRight = m_vmOverlay.GetHorizontalOffset() +
             m_vmOverlay.GetRenderImage().GetWidth();
 
         for (auto& pTracker : m_vScoreTrackers)
@@ -461,7 +547,7 @@ void OverlayManager::UpdateOverlay(ra::ui::drawing::ISurface& pSurface, double f
         m_bRedrawAll = true;
     }
 
-    UpdatePopup(pSurface, fElapsed, m_vmOverlay);
+    UpdatePopup(pSurface, pPopupLocations, fElapsed, m_vmOverlay);
     if (m_bRedrawAll)
         RenderPopup(pSurface, m_vmOverlay);
 
@@ -595,8 +681,9 @@ std::unique_ptr<ra::ui::drawing::ISurface> OverlayManager::RenderScreenshot(cons
 
     pSurface->DrawSurface(0, 0, pClientSurface);
 
-    const int nX = GetAbsolutePosition(vmPopup.GetRenderLocationX(), vmPopup.GetRenderLocationXRelativePosition(), pSurface->GetWidth());
-    const int nY = GetAbsolutePosition(vmPopup.GetRenderTargetY(), vmPopup.GetRenderLocationYRelativePosition(), pSurface->GetHeight());
+    PopupLocations pPopupLocations;
+    memset(&pPopupLocations, 0, sizeof(pPopupLocations));
+    const auto nPos = GetRenderLocation(vmPopup, vmPopup.GetHorizontalOffset(), vmPopup.GetTargetVerticalOffset(), *pSurface, pPopupLocations);
     const auto& pPopupImage = vmPopup.GetRenderImage();
 
     if (ra::services::ServiceLocator::Get<ra::ui::OverlayTheme>().Transparent())
@@ -604,11 +691,11 @@ std::unique_ptr<ra::ui::drawing::ISurface> OverlayManager::RenderScreenshot(cons
         auto pTransparentPopup = pSurfaceFactory.CreateTransparentSurface(pPopupImage.GetWidth(), pPopupImage.GetHeight());
         pTransparentPopup->DrawSurface(0, 0, pPopupImage);
         pTransparentPopup->SetOpacity(0.90);
-        pSurface->DrawSurface(nX, nY, *pTransparentPopup);
+        pSurface->DrawSurface(nPos.X, nPos.Y, *pTransparentPopup);
     }
     else
     {
-        pSurface->DrawSurface(nX, nY, pPopupImage);
+        pSurface->DrawSurface(nPos.X, nPos.Y, pPopupImage);
     }
 
     return pSurface;
diff --git a/src/ui/viewmodels/OverlayManager.hh b/src/ui/viewmodels/OverlayManager.hh
index 5ca5938d..cb81dbef 100644
--- a/src/ui/viewmodels/OverlayManager.hh
+++ b/src/ui/viewmodels/OverlayManager.hh
@@ -101,6 +101,18 @@ public:
         return QueueMessage(vmMessage);
     }
 
+    /// <summary>
+    /// Queues a popup message.
+    /// </summary>
+    int QueueMessage(ra::ui::viewmodels::Popup nPopup, const std::wstring& sTitle, const std::wstring& sDescription)
+    {
+        std::unique_ptr<PopupMessageViewModel> vmMessage(new PopupMessageViewModel);
+        vmMessage->SetTitle(sTitle);
+        vmMessage->SetDescription(sDescription);
+        vmMessage->SetPopupType(nPopup);
+        return QueueMessage(vmMessage);
+    }
+
     /// <summary>
     /// Queues a popup message.
     /// </summary>
@@ -113,6 +125,19 @@ public:
         return QueueMessage(vmMessage);
     }
 
+    /// <summary>
+    /// Queues a popup message.
+    /// </summary>
+    int QueueMessage(ra::ui::viewmodels::Popup nPopup, const std::wstring& sTitle, const std::wstring& sDescription, const std::wstring& sDetail)
+    {
+        std::unique_ptr<PopupMessageViewModel> vmMessage(new PopupMessageViewModel);
+        vmMessage->SetTitle(sTitle);
+        vmMessage->SetDescription(sDescription);
+        vmMessage->SetDetail(sDetail);
+        vmMessage->SetPopupType(nPopup);
+        return QueueMessage(vmMessage);
+    }
+
     /// <summary>
     /// Queues a popup message with an image.
     /// </summary>
@@ -269,10 +294,24 @@ protected:
     }
 
 private:
-    void UpdateActiveMessage(ra::ui::drawing::ISurface& pSurface, double fElapsed);
-    void UpdateActiveScoreboard(ra::ui::drawing::ISurface& pSurface, double fElapsed);
-    void UpdateScoreTrackers(ra::ui::drawing::ISurface& pSurface, double fElapsed);
-    void UpdatePopup(ra::ui::drawing::ISurface& pSurface, double fElapsed, ra::ui::viewmodels::PopupViewModelBase& vmPopup);
+    struct PopupLocations
+    {
+        int nTopLeftY;
+        int nTopMiddleY;
+        int nTopRightY;
+        int nBottomLeftY;
+        int nBottomMiddleY;
+        int nBottomRightY;
+    };
+
+    static ra::ui::Position GetRenderLocation(const ra::ui::viewmodels::PopupViewModelBase& vmPopup,
+        int nX, int nY, const ra::ui::drawing::ISurface& pSurface, const PopupLocations& pPopupLocations);
+    static void AdjustLocationForPopup(PopupLocations& pPopupLocations, const ra::ui::viewmodels::PopupViewModelBase& vmPopup);
+
+    void UpdateActiveMessage(ra::ui::drawing::ISurface& pSurface, PopupLocations& pPopupLocations, double fElapsed);
+    void UpdateActiveScoreboard(ra::ui::drawing::ISurface& pSurface, PopupLocations& pPopupLocations, double fElapsed);
+    void UpdateScoreTrackers(ra::ui::drawing::ISurface& pSurface, PopupLocations& pPopupLocations, double fElapsed);
+    void UpdatePopup(ra::ui::drawing::ISurface& pSurface, const PopupLocations& pPopupLocations, double fElapsed, ra::ui::viewmodels::PopupViewModelBase& vmPopup);
 
     void UpdateOverlay(ra::ui::drawing::ISurface& pSurface, double fElapsed);
 
diff --git a/src/ui/viewmodels/OverlaySettingsViewModel.cpp b/src/ui/viewmodels/OverlaySettingsViewModel.cpp
index 24ea1e15..76f931ef 100644
--- a/src/ui/viewmodels/OverlaySettingsViewModel.cpp
+++ b/src/ui/viewmodels/OverlaySettingsViewModel.cpp
@@ -9,32 +9,48 @@ namespace ra {
 namespace ui {
 namespace viewmodels {
 
-const BoolModelProperty OverlaySettingsViewModel::DisplayAchievementTriggerProperty("OverlaySettingsViewModel", "DisplayAchievementTrigger", true);
+const IntModelProperty OverlaySettingsViewModel::MessageLocationProperty("OverlaySettingsViewModel", "MessageLocation", ra::etoi(PopupLocation::BottomLeft));
+const IntModelProperty OverlaySettingsViewModel::AchievementTriggerLocationProperty("OverlaySettingsViewModel", "AchievementTriggerLocation", ra::etoi(PopupLocation::BottomLeft));
 const BoolModelProperty OverlaySettingsViewModel::ScreenshotAchievementTriggerProperty("OverlaySettingsViewModel", "ScreenshotAchievementTrigger", false);
-const BoolModelProperty OverlaySettingsViewModel::DisplayMasteryProperty("OverlaySettingsViewModel", "DisplayMastery", true);
+const IntModelProperty OverlaySettingsViewModel::MasteryLocationProperty("OverlaySettingsViewModel", "MasteryLocation", ra::etoi(PopupLocation::TopMiddle));
 const BoolModelProperty OverlaySettingsViewModel::ScreenshotMasteryProperty("OverlaySettingsViewModel", "ScreenshotMastery", false);
-const BoolModelProperty OverlaySettingsViewModel::DisplayLeaderboardStartedProperty("OverlaySettingsViewModel", "DisplayLeaderboardStarted", true);
-const BoolModelProperty OverlaySettingsViewModel::DisplayLeaderboardCanceledProperty("OverlaySettingsViewModel", "DisplayLeaderboardCanceled", true);
-const BoolModelProperty OverlaySettingsViewModel::DisplayLeaderboardValueProperty("OverlaySettingsViewModel", "DisplayLeaderboardValue", true);
-const BoolModelProperty OverlaySettingsViewModel::DisplayLeaderboardScoreboardProperty("OverlaySettingsViewModel", "DisplayLeaderboardScoreboard", true);
+const IntModelProperty OverlaySettingsViewModel::LeaderboardStartedLocationProperty("OverlaySettingsViewModel", "LeaderboardStartedLocation", ra::etoi(PopupLocation::BottomLeft));
+const IntModelProperty OverlaySettingsViewModel::LeaderboardCanceledLocationProperty("OverlaySettingsViewModel", "LeaderboardCanceledLocation", ra::etoi(PopupLocation::BottomLeft));
+const IntModelProperty OverlaySettingsViewModel::LeaderboardTrackerLocationProperty("OverlaySettingsViewModel", "LeaderboardTrackerLocation", ra::etoi(PopupLocation::BottomRight));
+const IntModelProperty OverlaySettingsViewModel::LeaderboardScoreboardLocationProperty("OverlaySettingsViewModel", "LeaderboardScoreboardLocation", ra::etoi(PopupLocation::BottomRight));
 const StringModelProperty OverlaySettingsViewModel::ScreenshotLocationProperty("OverlaySettingsViewModel", "ScreenshotLocation", L"");
 
 OverlaySettingsViewModel::OverlaySettingsViewModel() noexcept
 {
     SetWindowTitle(L"Overlay Settings");
+
+    m_vPopupLocations.Add(ra::etoi(PopupLocation::None), L"None");
+    m_vPopupLocations.Add(ra::etoi(PopupLocation::TopLeft), L"Top Left");
+    m_vPopupLocations.Add(ra::etoi(PopupLocation::TopMiddle), L"Top Middle");
+    m_vPopupLocations.Add(ra::etoi(PopupLocation::TopRight), L"Top Right");
+    m_vPopupLocations.Add(ra::etoi(PopupLocation::BottomLeft), L"Bottom Left");
+    m_vPopupLocations.Add(ra::etoi(PopupLocation::BottomMiddle), L"Bottom Middle");
+    m_vPopupLocations.Add(ra::etoi(PopupLocation::BottomRight), L"Bottom Right");
+
+    m_vPopupLocationsNoMiddle.Add(ra::etoi(PopupLocation::None), L"None");
+    m_vPopupLocationsNoMiddle.Add(ra::etoi(PopupLocation::TopLeft), L"Top Left");
+    m_vPopupLocationsNoMiddle.Add(ra::etoi(PopupLocation::TopRight), L"Top Right");
+    m_vPopupLocationsNoMiddle.Add(ra::etoi(PopupLocation::BottomLeft), L"Bottom Left");
+    m_vPopupLocationsNoMiddle.Add(ra::etoi(PopupLocation::BottomRight), L"Bottom Right");
 }
 
 void OverlaySettingsViewModel::Initialize()
 {
     const auto& pConfiguration = ra::services::ServiceLocator::Get<ra::services::IConfiguration>();
-    SetDisplayAchievementTrigger(pConfiguration.IsFeatureEnabled(ra::services::Feature::AchievementTriggeredNotifications));
+    SetMessageLocation(pConfiguration.GetPopupLocation(Popup::Message));
+    SetAchievementTriggerLocation(pConfiguration.GetPopupLocation(Popup::AchievementTriggered));
     SetScreenshotAchievementTrigger(pConfiguration.IsFeatureEnabled(ra::services::Feature::AchievementTriggeredScreenshot));
-    SetDisplayMastery(pConfiguration.IsFeatureEnabled(ra::services::Feature::MasteryNotification));
+    SetMasteryLocation(pConfiguration.GetPopupLocation(Popup::Mastery));
     SetScreenshotMastery(pConfiguration.IsFeatureEnabled(ra::services::Feature::MasteryNotificationScreenshot));
-    SetDisplayLeaderboardStarted(pConfiguration.IsFeatureEnabled(ra::services::Feature::LeaderboardNotifications));
-    SetDisplayLeaderboardCanceled(pConfiguration.IsFeatureEnabled(ra::services::Feature::LeaderboardCancelNotifications));
-    SetDisplayLeaderboardValue(pConfiguration.IsFeatureEnabled(ra::services::Feature::LeaderboardCounters));
-    SetDisplayLeaderboardScoreboard(pConfiguration.IsFeatureEnabled(ra::services::Feature::LeaderboardScoreboards));
+    SetLeaderboardStartedLocation(pConfiguration.GetPopupLocation(Popup::LeaderboardStarted));
+    SetLeaderboardCanceledLocation(pConfiguration.GetPopupLocation(Popup::LeaderboardCanceled));
+    SetLeaderboardTrackerLocation(pConfiguration.GetPopupLocation(Popup::LeaderboardTracker));
+    SetLeaderboardScoreboardLocation(pConfiguration.GetPopupLocation(Popup::LeaderboardScoreboard));
 
     SetScreenshotLocation(pConfiguration.GetScreenshotDirectory());
 }
@@ -43,14 +59,15 @@ void OverlaySettingsViewModel::Commit()
 {
     auto& pConfiguration = ra::services::ServiceLocator::GetMutable<ra::services::IConfiguration>();
 
-    pConfiguration.SetFeatureEnabled(ra::services::Feature::AchievementTriggeredNotifications, DisplayAchievementTrigger());
+    pConfiguration.SetPopupLocation(Popup::Message, GetMessageLocation());
+    pConfiguration.SetPopupLocation(Popup::AchievementTriggered, GetAchievementTriggerLocation());
     pConfiguration.SetFeatureEnabled(ra::services::Feature::AchievementTriggeredScreenshot, ScreenshotAchievementTrigger());
-    pConfiguration.SetFeatureEnabled(ra::services::Feature::MasteryNotification, DisplayMastery());
+    pConfiguration.SetPopupLocation(Popup::Mastery, GetMasteryLocation());
     pConfiguration.SetFeatureEnabled(ra::services::Feature::MasteryNotificationScreenshot, ScreenshotMastery());
-    pConfiguration.SetFeatureEnabled(ra::services::Feature::LeaderboardNotifications, DisplayLeaderboardStarted());
-    pConfiguration.SetFeatureEnabled(ra::services::Feature::LeaderboardCancelNotifications, DisplayLeaderboardCanceled());
-    pConfiguration.SetFeatureEnabled(ra::services::Feature::LeaderboardCounters, DisplayLeaderboardValue());
-    pConfiguration.SetFeatureEnabled(ra::services::Feature::LeaderboardScoreboards, DisplayLeaderboardScoreboard());
+    pConfiguration.SetPopupLocation(Popup::LeaderboardStarted, GetLeaderboardStartedLocation());
+    pConfiguration.SetPopupLocation(Popup::LeaderboardCanceled, GetLeaderboardCanceledLocation());
+    pConfiguration.SetPopupLocation(Popup::LeaderboardTracker, GetLeaderboardTrackerLocation());
+    pConfiguration.SetPopupLocation(Popup::LeaderboardScoreboard, GetLeaderboardScoreboardLocation());
 
     std::wstring sLocation = ScreenshotLocation();
     if (!sLocation.empty() && sLocation.back() != '\\')
@@ -63,14 +80,26 @@ void OverlaySettingsViewModel::Commit()
 
 void OverlaySettingsViewModel::OnValueChanged(const BoolModelProperty::ChangeArgs& args)
 {
-    if (args.Property == DisplayAchievementTriggerProperty && !args.tNewValue)
+    if (args.Property == ScreenshotAchievementTriggerProperty && args.tNewValue)
+    {
+        if (GetAchievementTriggerLocation() == PopupLocation::None)
+            SetAchievementTriggerLocation(ra::itoe<PopupLocation>(AchievementTriggerLocationProperty.GetDefaultValue()));
+    }
+    else if (args.Property == ScreenshotMasteryProperty && args.tNewValue)
+    {
+        if (GetMasteryLocation() == PopupLocation::None)
+            SetMasteryLocation(ra::itoe<PopupLocation>(MasteryLocationProperty.GetDefaultValue()));
+    }
+
+    WindowViewModelBase::OnValueChanged(args);
+}
+
+void OverlaySettingsViewModel::OnValueChanged(const IntModelProperty::ChangeArgs& args)
+{
+    if (args.Property == AchievementTriggerLocationProperty && ra::itoe<PopupLocation>(args.tNewValue) == PopupLocation::None)
         SetScreenshotAchievementTrigger(false);
-    else if (args.Property == ScreenshotAchievementTriggerProperty && args.tNewValue)
-        SetDisplayAchievementTrigger(true);
-    else if (args.Property == DisplayMasteryProperty && !args.tNewValue)
+    else if (args.Property == MasteryLocationProperty && ra::itoe<PopupLocation>(args.tNewValue) == PopupLocation::None)
         SetScreenshotMastery(false);
-    else if (args.Property == ScreenshotMasteryProperty && args.tNewValue)
-        SetDisplayMastery(true);
 
     WindowViewModelBase::OnValueChanged(args);
 }
diff --git a/src/ui/viewmodels/OverlaySettingsViewModel.hh b/src/ui/viewmodels/OverlaySettingsViewModel.hh
index ebe9e9a9..5d58dfeb 100644
--- a/src/ui/viewmodels/OverlaySettingsViewModel.hh
+++ b/src/ui/viewmodels/OverlaySettingsViewModel.hh
@@ -3,6 +3,8 @@
 #pragma once
 
 #include "ui/WindowViewModelBase.hh"
+#include "ui/viewmodels/LookupItemViewModel.hh"
+#include "ui/viewmodels/PopupViewModelBase.hh"
 
 namespace ra {
 namespace ui {
@@ -24,19 +26,34 @@ public:
     void Commit();
 
     /// <summary>
-    /// The <see cref="ModelProperty" /> for whether or not the achievement triggered notification should be displayed.
+    /// The <see cref="ModelProperty" /> for where the message notification should be displayed.
     /// </summary>
-    static const BoolModelProperty DisplayAchievementTriggerProperty;
+    static const IntModelProperty MessageLocationProperty;
 
     /// <summary>
-    /// Gets whether or not the achievement triggered notification should be displayed.
+    /// Gets the location where message notification popups should be displayed.
     /// </summary>
-    bool DisplayAchievementTrigger() const { return GetValue(DisplayAchievementTriggerProperty); }
+    PopupLocation GetMessageLocation() const { return ra::itoe<PopupLocation>(GetValue(MessageLocationProperty)); }
 
     /// <summary>
-    /// Sets whether or not the achievement triggered notification should be displayed.
+    /// Sets the location where message notification popups should be displayed.
     /// </summary>
-    void SetDisplayAchievementTrigger(bool bValue) { SetValue(DisplayAchievementTriggerProperty, bValue); }
+    void SetMessageLocation(PopupLocation nValue) { SetValue(MessageLocationProperty, ra::etoi(nValue)); }
+
+    /// <summary>
+    /// The <see cref="ModelProperty" /> for where the achievement triggered notification should be displayed.
+    /// </summary>
+    static const IntModelProperty AchievementTriggerLocationProperty;
+
+    /// <summary>
+    /// Gets the location where achievement triggered notification popups should be displayed.
+    /// </summary>
+    PopupLocation GetAchievementTriggerLocation() const { return ra::itoe<PopupLocation>(GetValue(AchievementTriggerLocationProperty)); }
+
+    /// <summary>
+    /// Sets the location where achievement triggered notification popups should be displayed.
+    /// </summary>
+    void SetAchievementTriggerLocation(PopupLocation nValue) { SetValue(AchievementTriggerLocationProperty, ra::etoi(nValue)); }
 
     /// <summary>
     /// The <see cref="ModelProperty" /> for whether or not the achievement triggered notification should be screenshoted.
@@ -54,19 +71,19 @@ public:
     void SetScreenshotAchievementTrigger(bool bValue) { SetValue(ScreenshotAchievementTriggerProperty, bValue); }
 
     /// <summary>
-    /// The <see cref="ModelProperty" /> for whether or not the game mastery notification should be displayed.
+    /// The <see cref="ModelProperty" /> for where the mastery notification should be displayed.
     /// </summary>
-    static const BoolModelProperty DisplayMasteryProperty;
+    static const IntModelProperty MasteryLocationProperty;
 
     /// <summary>
-    /// Gets whether or not the game mastery notification should be displayed.
+    /// Gets the location where mastery notification popups should be displayed.
     /// </summary>
-    bool DisplayMastery() const { return GetValue(DisplayMasteryProperty); }
+    PopupLocation GetMasteryLocation() const { return ra::itoe<PopupLocation>(GetValue(MasteryLocationProperty)); }
 
     /// <summary>
-    /// Sets whether or not the game mastery  notification should be displayed.
+    /// Sets the location where mastery notification popups should be displayed.
     /// </summary>
-    void SetDisplayMastery(bool bValue) { SetValue(DisplayMasteryProperty, bValue); }
+    void SetMasteryLocation(PopupLocation nValue) { SetValue(MasteryLocationProperty, ra::etoi(nValue)); }
 
     /// <summary>
     /// The <see cref="ModelProperty" /> for whether or not the game mastery notification should be screenshoted.
@@ -84,64 +101,64 @@ public:
     void SetScreenshotMastery(bool bValue) { SetValue(ScreenshotMasteryProperty, bValue); }
 
     /// <summary>
-    /// The <see cref="ModelProperty" /> for whether or not the leaderboard started notification should be displayed.
+    /// The <see cref="ModelProperty" /> for where the leaderboard started notification should be displayed.
     /// </summary>
-    static const BoolModelProperty DisplayLeaderboardStartedProperty;
+    static const IntModelProperty LeaderboardStartedLocationProperty;
 
     /// <summary>
-    /// Gets whether or not the leaderboard started notification should be displayed.
+    /// Gets the location where leaderboard started notification popups should be displayed.
     /// </summary>
-    bool DisplayLeaderboardStarted() const { return GetValue(DisplayLeaderboardStartedProperty); }
+    PopupLocation GetLeaderboardStartedLocation() const { return ra::itoe<PopupLocation>(GetValue(LeaderboardStartedLocationProperty)); }
 
     /// <summary>
-    /// Sets whether or not the leaderboard started notification should be displayed.
+    /// Sets the location where leaderboard started notification popups should be displayed.
     /// </summary>
-    void SetDisplayLeaderboardStarted(bool bValue) { SetValue(DisplayLeaderboardStartedProperty, bValue); }
+    void SetLeaderboardStartedLocation(PopupLocation nValue) { SetValue(LeaderboardStartedLocationProperty, ra::etoi(nValue)); }
 
     /// <summary>
-    /// The <see cref="ModelProperty" /> for whether or not the leaderboard canceled notification should be displayed.
+    /// The <see cref="ModelProperty" /> for where the leaderboard canceled notification should be displayed.
     /// </summary>
-    static const BoolModelProperty DisplayLeaderboardCanceledProperty;
+    static const IntModelProperty LeaderboardCanceledLocationProperty;
 
     /// <summary>
-    /// Gets whether or not the leaderboard canceled notification should be displayed.
+    /// Gets the location where leaderboard canceled notification popups should be displayed.
     /// </summary>
-    bool DisplayLeaderboardCanceled() const { return GetValue(DisplayLeaderboardCanceledProperty); }
+    PopupLocation GetLeaderboardCanceledLocation() const { return ra::itoe<PopupLocation>(GetValue(LeaderboardCanceledLocationProperty)); }
 
     /// <summary>
-    /// Sets whether or not the leaderboard canceled notification should be displayed.
+    /// Sets the location where leaderboard canceled notification popups should be displayed.
     /// </summary>
-    void SetDisplayLeaderboardCanceled(bool bValue) { SetValue(DisplayLeaderboardCanceledProperty, bValue); }
+    void SetLeaderboardCanceledLocation(PopupLocation nValue) { SetValue(LeaderboardCanceledLocationProperty, ra::etoi(nValue)); }
 
     /// <summary>
-    /// The <see cref="ModelProperty" /> for whether or not the value for an active leaderboard should be displayed.
+    /// The <see cref="ModelProperty" /> for where the leaderboard tracker should be displayed.
     /// </summary>
-    static const BoolModelProperty DisplayLeaderboardValueProperty;
+    static const IntModelProperty LeaderboardTrackerLocationProperty;
 
     /// <summary>
-    /// Gets whether or not the value for an active leaderboard should be displayed.
+    /// Gets the location where leaderboard tracker popups should be displayed.
     /// </summary>
-    bool DisplayLeaderboardValue() const { return GetValue(DisplayLeaderboardValueProperty); }
+    PopupLocation GetLeaderboardTrackerLocation() const { return ra::itoe<PopupLocation>(GetValue(LeaderboardTrackerLocationProperty)); }
 
     /// <summary>
-    /// Sets whether or not the value for an active leaderboard should be displayed.
+    /// Sets the location where leaderboard tracker popups should be displayed.
     /// </summary>
-    void SetDisplayLeaderboardValue(bool bValue) { SetValue(DisplayLeaderboardValueProperty, bValue); }
+    void SetLeaderboardTrackerLocation(PopupLocation nValue) { SetValue(LeaderboardTrackerLocationProperty, ra::etoi(nValue)); }
 
     /// <summary>
-    /// The <see cref="ModelProperty" /> for whether or not the leaderboard scoreboard should be displayed after submitting a leaderboard entry.
+    /// The <see cref="ModelProperty" /> for where the leaderboard scoreboard should be displayed.
     /// </summary>
-    static const BoolModelProperty DisplayLeaderboardScoreboardProperty;
+    static const IntModelProperty LeaderboardScoreboardLocationProperty;
 
     /// <summary>
-    /// Gets whether or not the leaderboard scoreboard should be displayed after submitting a leaderboard entry.
+    /// Gets the location where leaderboard scoreboard popups should be displayed.
     /// </summary>
-    bool DisplayLeaderboardScoreboard() const { return GetValue(DisplayLeaderboardScoreboardProperty); }
+    PopupLocation GetLeaderboardScoreboardLocation() const { return ra::itoe<PopupLocation>(GetValue(LeaderboardScoreboardLocationProperty)); }
 
     /// <summary>
-    /// Sets whether or not the leaderboard scoreboard should be displayed after submitting a leaderboard entry.
+    /// Sets the location where leaderboard scoreboard popups should be displayed.
     /// </summary>
-    void SetDisplayLeaderboardScoreboard(bool bValue) { SetValue(DisplayLeaderboardScoreboardProperty, bValue); }
+    void SetLeaderboardScoreboardLocation(PopupLocation nValue) { SetValue(LeaderboardScoreboardLocationProperty, ra::etoi(nValue)); }
 
     /// <summary>
     /// The <see cref="ModelProperty" /> for the location to write screenshots.
@@ -163,8 +180,29 @@ public:
     /// </summary>
     void BrowseLocation();
 
+    /// <summary>
+    /// Gets the list of popup locations.
+    /// </summary>
+    const LookupItemViewModelCollection& PopupLocations() const noexcept
+    {
+        return m_vPopupLocations;
+    }
+
+    /// <summary>
+    /// Gets the list of popup locations without middle options.
+    /// </summary>
+    const LookupItemViewModelCollection& PopupLocationsNoMiddle() const noexcept
+    {
+        return m_vPopupLocationsNoMiddle;
+    }
+
 protected:
     void OnValueChanged(const BoolModelProperty::ChangeArgs& args) override;
+    void OnValueChanged(const IntModelProperty::ChangeArgs& args) override;
+
+private:
+    LookupItemViewModelCollection m_vPopupLocations;
+    LookupItemViewModelCollection m_vPopupLocationsNoMiddle;
 };
 
 } // namespace viewmodels
diff --git a/src/ui/viewmodels/OverlayViewModel.cpp b/src/ui/viewmodels/OverlayViewModel.cpp
index 4e919230..829dd7ea 100644
--- a/src/ui/viewmodels/OverlayViewModel.cpp
+++ b/src/ui/viewmodels/OverlayViewModel.cpp
@@ -33,8 +33,8 @@ void OverlayViewModel::BeginAnimation()
 
     Resize(szEmulator.Width, szEmulator.Height);
 
-    SetRenderLocationX(-szEmulator.Width);
-    SetRenderLocationY(0);
+    SetHorizontalOffset(-szEmulator.Width);
+    SetVerticalOffset(0);
 }
 
 void OverlayViewModel::Resize(int nWidth, int nHeight)
@@ -56,7 +56,7 @@ bool OverlayViewModel::UpdateRenderImage(double fElapsed)
     if (m_nState != State::Visible)
     {
         const int nWidth = m_pSurface->GetWidth();
-        const int nOldX = GetRenderLocationX();
+        const int nOldX = GetHorizontalOffset();
 
         m_fAnimationProgress += fElapsed;
 
@@ -72,7 +72,7 @@ bool OverlayViewModel::UpdateRenderImage(double fElapsed)
             const int nNewX = ftol(-nOffset);
             if (nNewX != nOldX)
             {
-                SetRenderLocationX(nNewX);
+                SetHorizontalOffset(nNewX);
                 bUpdated = true;
 
                 if (nNewX == 0)
@@ -91,7 +91,7 @@ bool OverlayViewModel::UpdateRenderImage(double fElapsed)
             const int nNewX = ftol(-nOffset);
             if (nNewX != nOldX)
             {
-                SetRenderLocationX(nNewX);
+                SetHorizontalOffset(nNewX);
                 bUpdated = true;
 
                 if (nNewX == -nWidth)
@@ -352,7 +352,7 @@ void OverlayViewModel::Deactivate()
         case State::FadeIn:
             m_nState = State::FadeOut;
             m_fAnimationProgress = INOUT_TIME - m_fAnimationProgress;
-            SetRenderLocationX(GetRenderImage().GetWidth() - GetRenderLocationX());
+            SetHorizontalOffset(GetRenderImage().GetWidth() - GetHorizontalOffset());
             break;
     }
 }
diff --git a/src/ui/viewmodels/PopupMessageViewModel.cpp b/src/ui/viewmodels/PopupMessageViewModel.cpp
index 7f389600..5023c1b9 100644
--- a/src/ui/viewmodels/PopupMessageViewModel.cpp
+++ b/src/ui/viewmodels/PopupMessageViewModel.cpp
@@ -10,25 +10,33 @@ const StringModelProperty PopupMessageViewModel::TitleProperty("PopupMessageView
 const StringModelProperty PopupMessageViewModel::DescriptionProperty("PopupMessageViewModel", "Description", L"");
 const StringModelProperty PopupMessageViewModel::DetailProperty("PopupMessageViewModel", "Detail", L"");
 const BoolModelProperty PopupMessageViewModel::IsDetailErrorProperty("PopupMessageViewModel", "IsDetailError", false);
-const BoolModelProperty PopupMessageViewModel::IsMasteryProperty("PopupMessageViewModel", "IsMastery", false);
+
+PopupMessageViewModel::PopupMessageViewModel() noexcept
+{
+    GSL_SUPPRESS_F6 SetPopupType(Popup::Message);
+}
 
 void PopupMessageViewModel::BeginAnimation()
 {
     m_fAnimationProgress = 0.0;
 
     // left margin 10px
-    SetRenderLocationX(10);
+    SetHorizontalOffset(10);
+
+    // animate to bottom margin 10px.
+    m_nTargetY = 10;
 
-    // animate to bottom margin 10px. assume height = 64+6
-    m_nInitialY = 0;
-    m_nTargetY = 10 + 64 + 6;
-    SetRenderLocationY(m_nInitialY);
-    SetRenderLocationYRelativePosition(RelativePosition::Far);
+    const auto& pOverlayTheme = ra::services::ServiceLocator::Get<ra::ui::OverlayTheme>();
+    const auto nShadowOffset = pOverlayTheme.ShadowOffset();
+    constexpr int nImageSize = 64;
+    const int nHeight = 4 + nImageSize + 4 + nShadowOffset;
+    m_nInitialY = -nHeight;
+    SetVerticalOffset(m_nInitialY);
 }
 
 bool PopupMessageViewModel::UpdateRenderImage(double fElapsed)
 {
-    const int nOldY = GetRenderLocationY();
+    const int nOldY = GetVerticalOffset();
 
     m_fAnimationProgress += fElapsed;
     const int nNewY = GetFadeOffset(m_fAnimationProgress, TOTAL_ANIMATION_TIME, INOUT_TIME, m_nInitialY, m_nTargetY);
@@ -36,7 +44,7 @@ bool PopupMessageViewModel::UpdateRenderImage(double fElapsed)
     bool bUpdated = false;
     if (nNewY != nOldY)
     {
-        SetRenderLocationY(nNewY);
+        SetVerticalOffset(nNewY);
         bUpdated = true;
     }
 
@@ -103,7 +111,7 @@ void PopupMessageViewModel::CreateRenderImage()
     pSurface->FillRectangle(nShadowOffset, nShadowOffset, nWidth - nShadowOffset, nHeight - nShadowOffset, pOverlayTheme.ColorShadow());
 
     // frame
-    const auto nColorBackground = IsMastery() ? pOverlayTheme.ColorMasteryBackground() : pOverlayTheme.ColorBackground();
+    const auto nColorBackground = (GetPopupType() == Popup::Mastery) ? pOverlayTheme.ColorMasteryBackground() : pOverlayTheme.ColorBackground();
     pSurface->FillRectangle(0, 0, nWidth - nShadowOffset, nHeight - nShadowOffset, nColorBackground);
     pSurface->FillRectangle(1, 1, nWidth - nShadowOffset - 2, nHeight - nShadowOffset - 2, pOverlayTheme.ColorBorder());
     pSurface->FillRectangle(2, 2, nWidth - nShadowOffset - 4, nHeight - nShadowOffset - 4, nColorBackground);
diff --git a/src/ui/viewmodels/PopupMessageViewModel.hh b/src/ui/viewmodels/PopupMessageViewModel.hh
index efcdf093..71d75477 100644
--- a/src/ui/viewmodels/PopupMessageViewModel.hh
+++ b/src/ui/viewmodels/PopupMessageViewModel.hh
@@ -11,6 +11,8 @@ namespace viewmodels {
 class PopupMessageViewModel : public PopupViewModelBase
 {
 public:
+    PopupMessageViewModel() noexcept;
+
     /// <summary>
     /// The <see cref="ModelProperty" /> for the title message.
     /// </summary>
@@ -83,21 +85,6 @@ public:
         m_hImage.ChangeReference(nImageType, sImageName);
     }
 
-    /// <summary>
-    /// The <see cref="ModelProperty" /> for whether or not the popup should use the Mastery style.
-    /// </summary>
-    static const BoolModelProperty IsMasteryProperty;
-
-    /// <summary>
-    /// Gets whether the popup should use the Mastery style.
-    /// </summary>
-    bool IsMastery() const { return GetValue(IsMasteryProperty); }
-
-    /// <summary>
-    /// Sets whether  the popup should use the Mastery style.
-    /// </summary>
-    void SetIsMastery(bool bValue) { SetValue(IsMasteryProperty, bValue); }
-
     /// <summary>
     /// Gets the image to display.
     /// </summary>
@@ -137,7 +124,7 @@ public:
     /// <summary>
     /// Gets the final Y location to render the popup at.
     /// </summary>
-    int GetRenderTargetY() const noexcept { return m_nTargetY; }
+    int GetTargetVerticalOffset() const noexcept { return m_nTargetY; }
 
     class RenderImageLock
     {
diff --git a/src/ui/viewmodels/PopupViewModelBase.cpp b/src/ui/viewmodels/PopupViewModelBase.cpp
index 56671d1d..f32b7005 100644
--- a/src/ui/viewmodels/PopupViewModelBase.cpp
+++ b/src/ui/viewmodels/PopupViewModelBase.cpp
@@ -2,14 +2,17 @@
 
 #include "ra_math.h"
 
+#include "services\IConfiguration.hh"
+
 namespace ra {
 namespace ui {
 namespace viewmodels {
 
+const IntModelProperty PopupViewModelBase::PopupTypeProperty("PopupViewModelBase", "PopupType", ra::etoi(Popup::None));
+const IntModelProperty PopupViewModelBase::HorizontalOffsetProperty("PopupViewModelBase", "HorizontalOffset", 0);
+const IntModelProperty PopupViewModelBase::VerticalOffsetProperty("PopupViewModelBase", "VerticalOffset", 0);
 const IntModelProperty PopupViewModelBase::RenderLocationXProperty("PopupViewModelBase", "RenderLocationX", 0);
 const IntModelProperty PopupViewModelBase::RenderLocationYProperty("PopupViewModelBase", "RenderLocationY", 0);
-const IntModelProperty PopupViewModelBase::RenderLocationXRelativePositionProperty("PopupViewModelBase", "RenderLocationXRelativePosition", ra::etoi(RelativePosition::Near));
-const IntModelProperty PopupViewModelBase::RenderLocationYRelativePositionProperty("PopupViewModelBase", "RenderLocationYRelativePosition", ra::etoi(RelativePosition::Near));
 
 int PopupViewModelBase::GetFadeOffset(double fAnimationProgress, double fTotalAnimationTime, double fInOutTime, int nInitialOffset, int nTargetOffset)
 {
diff --git a/src/ui/viewmodels/PopupViewModelBase.hh b/src/ui/viewmodels/PopupViewModelBase.hh
index d3a96a2b..dcd46773 100644
--- a/src/ui/viewmodels/PopupViewModelBase.hh
+++ b/src/ui/viewmodels/PopupViewModelBase.hh
@@ -9,80 +9,107 @@ namespace ra {
 namespace ui {
 namespace viewmodels {
 
+enum class Popup
+{
+    None = 0,
+    Message,
+    AchievementTriggered,
+    Mastery,
+    LeaderboardStarted,
+    LeaderboardCanceled,
+    LeaderboardTracker,
+    LeaderboardScoreboard,
+    NumPopups
+};
+
+enum class PopupLocation
+{
+    None,
+    TopLeft,
+    TopMiddle,
+    TopRight,
+    BottomLeft,
+    BottomMiddle,
+    BottomRight
+};
+
 class PopupViewModelBase : public ViewModelBase
 {
 public:
     /// <summary>
-    /// The <see cref="ModelProperty" /> for the X location to render the popup at.
+    /// The <see cref="ModelProperty" /> for the classification of the popup.
     /// </summary>
-    static const IntModelProperty RenderLocationXProperty;
+    static const IntModelProperty PopupTypeProperty;
 
     /// <summary>
-    /// Gets the X location to render the popup at.
+    /// Gets the classification of the popup.
     /// </summary>
-    int GetRenderLocationX() const { return GetValue(RenderLocationXProperty); }
+    Popup GetPopupType() const { return ra::itoe<Popup>(GetValue(PopupTypeProperty)); }
 
     /// <summary>
-    /// Sets the X location to render the popup at.
+    /// Sets the classification of the popup.
     /// </summary>
-    void SetRenderLocationX(int nValue) { SetValue(RenderLocationXProperty, nValue); }
+    void SetPopupType(Popup nType) { SetValue(PopupTypeProperty, ra::etoi(nType)); }
 
     /// <summary>
-    /// The <see cref="ModelProperty" /> for the parent relative X location to render the popup at.
+    /// The <see cref="ModelProperty" /> for the X location to render the popup at.
     /// </summary>
-    static const IntModelProperty RenderLocationXRelativePositionProperty;
+    static const IntModelProperty HorizontalOffsetProperty;
 
     /// <summary>
-    /// Gets the parent relative X location to render the popup at.
+    /// Gets the X location to render the popup at.
     /// </summary>
-    RelativePosition GetRenderLocationXRelativePosition() const
-    {
-        return ra::itoe<RelativePosition>(GetValue(RenderLocationXRelativePositionProperty));
-    }
+    int GetHorizontalOffset() const { return GetValue(HorizontalOffsetProperty); }
 
     /// <summary>
-    /// Sets the parent relative X location to render the popup at.
+    /// Sets the X location to render the popup at.
     /// </summary>
-    void SetRenderLocationXRelativePosition(RelativePosition nValue)
-    {
-        SetValue(RenderLocationXRelativePositionProperty, ra::etoi(nValue));
-    }
+    void SetHorizontalOffset(int nValue) { SetValue(HorizontalOffsetProperty, nValue); }
 
     /// <summary>
     /// The <see cref="ModelProperty" /> for the Y location to render the popup at.
     /// </summary>
-    static const IntModelProperty RenderLocationYProperty;
+    static const IntModelProperty VerticalOffsetProperty;
 
     /// <summary>
     /// Gets the Y location to render the popup at.
     /// </summary>
-    int GetRenderLocationY() const { return GetValue(RenderLocationYProperty); }
+    int GetVerticalOffset() const { return GetValue(VerticalOffsetProperty); }
 
     /// <summary>
     /// Sets the Y location to render the popup at.
     /// </summary>
-    void SetRenderLocationY(int nValue) { SetValue(RenderLocationYProperty, nValue); }
+    void SetVerticalOffset(int nValue) { SetValue(VerticalOffsetProperty, nValue); }
 
     /// <summary>
-    /// The <see cref="ModelProperty" /> for the parent relative Y location to render the popup at.
+    /// The <see cref="ModelProperty" /> for the X location to render the popup at.
     /// </summary>
-    static const IntModelProperty RenderLocationYRelativePositionProperty;
+    static const IntModelProperty RenderLocationXProperty;
 
     /// <summary>
-    /// Gets the parent relative Y location to render the popup at.
+    /// Gets the X location to render the popup at.
     /// </summary>
-    RelativePosition GetRenderLocationYRelativePosition() const
-    {
-        return ra::itoe<RelativePosition>(GetValue(RenderLocationYRelativePositionProperty));
-    }
+    int GetRenderLocationX() const { return GetValue(RenderLocationXProperty); }
 
     /// <summary>
-    /// Sets the parent relative Y location to render the popup at.
+    /// Sets the X location to render the popup at.
     /// </summary>
-    void SetRenderLocationYRelativePosition(RelativePosition nValue)
-    {
-        SetValue(RenderLocationYRelativePositionProperty, ra::etoi(nValue));
-    }
+    void SetRenderLocationX(int nValue) { SetValue(RenderLocationXProperty, nValue); }
+
+    /// <summary>
+    /// The <see cref="ModelProperty" /> for the X location to render the popup at.
+    /// </summary>
+    static const IntModelProperty RenderLocationYProperty;
+
+    /// <summary>
+    /// Gets the X location to render the popup at.
+    /// </summary>
+    int GetRenderLocationY() const { return GetValue(RenderLocationYProperty); }
+
+    /// <summary>
+    /// Sets the Y location to render the popup at.
+    /// </summary>
+    void SetRenderLocationY(int nValue) { SetValue(RenderLocationYProperty, nValue); }
 
     /// <summary>
     /// Begins the animation cycle.
diff --git a/src/ui/viewmodels/ScoreTrackerViewModel.cpp b/src/ui/viewmodels/ScoreTrackerViewModel.cpp
index 70d527f6..4b1cfd29 100644
--- a/src/ui/viewmodels/ScoreTrackerViewModel.cpp
+++ b/src/ui/viewmodels/ScoreTrackerViewModel.cpp
@@ -11,6 +11,14 @@ namespace viewmodels {
 
 const StringModelProperty ScoreTrackerViewModel::DisplayTextProperty("ScoreTrackerViewModel", "DisplayText", L"0");
 
+ScoreTrackerViewModel::ScoreTrackerViewModel() noexcept
+{
+    GSL_SUPPRESS_F6 SetPopupType(ra::ui::viewmodels::Popup::LeaderboardTracker);
+
+    GSL_SUPPRESS_F6 SetHorizontalOffset(10);
+    GSL_SUPPRESS_F6 SetVerticalOffset(10);
+}
+
 void ScoreTrackerViewModel::SetDisplayText(const std::wstring& sValue)
 {
     if (sValue != GetDisplayText())
@@ -25,16 +33,7 @@ void ScoreTrackerViewModel::SetDisplayText(const std::wstring& sValue)
 bool ScoreTrackerViewModel::UpdateRenderImage(_UNUSED double fElapsed)
 {
     if (m_pSurface && !m_bSurfaceStale)
-    {
-        const auto nOffset = GetRenderLocationY() + ra::to_signed(GetRenderImage().GetHeight());
-        if (m_nOffset != nOffset)
-        {
-            SetRenderLocationY(m_nOffset + GetRenderImage().GetHeight());
-            return true;
-        }
-
         return false;
-    }
 
     const auto& pTheme = ra::services::ServiceLocator::Get<ra::ui::OverlayTheme>();
     const auto nShadowOffset = pTheme.ShadowOffset();
@@ -62,13 +61,6 @@ bool ScoreTrackerViewModel::UpdateRenderImage(_UNUSED double fElapsed)
     // text
     m_pSurface->WriteText(4, 0, nFontText, pTheme.ColorLeaderboardEntry(), sScoreSoFar);
 
-    // set location
-    SetRenderLocationX(10 + m_pSurface->GetWidth());
-    SetRenderLocationXRelativePosition(RelativePosition::Far);
-
-    SetRenderLocationY(m_nOffset + m_pSurface->GetHeight());
-    SetRenderLocationYRelativePosition(RelativePosition::Far);
-
     m_bSurfaceStale = false;
     return true;
 }
diff --git a/src/ui/viewmodels/ScoreTrackerViewModel.hh b/src/ui/viewmodels/ScoreTrackerViewModel.hh
index 1ba0d896..a1e8e521 100644
--- a/src/ui/viewmodels/ScoreTrackerViewModel.hh
+++ b/src/ui/viewmodels/ScoreTrackerViewModel.hh
@@ -11,6 +11,8 @@ namespace viewmodels {
 class ScoreTrackerViewModel : public PopupViewModelBase
 {
 public:
+    ScoreTrackerViewModel() noexcept;
+
     /// <summary>
     /// The <see cref="ModelProperty" /> for the title message.
     /// </summary>
@@ -35,19 +37,8 @@ public:
     bool IsAnimationStarted() const noexcept override { return true; }
     bool IsAnimationComplete() const noexcept override { return false; }
 
-    bool SetOffset(int nValue) noexcept
-    {
-        if (m_nOffset == nValue)
-            return false;
-
-        m_nOffset = nValue;
-        return true;
-    }
-
 private:
     bool m_bSurfaceStale = false;
-
-    int m_nOffset = 10;
 };
 
 } // namespace viewmodels
diff --git a/src/ui/viewmodels/ScoreboardViewModel.cpp b/src/ui/viewmodels/ScoreboardViewModel.cpp
index f9db50ea..fcdca59f 100644
--- a/src/ui/viewmodels/ScoreboardViewModel.cpp
+++ b/src/ui/viewmodels/ScoreboardViewModel.cpp
@@ -27,6 +27,11 @@ static int CalculateScoreboardWidth(const ra::ui::OverlayTheme& pTheme) noexcept
     return 4 + std::max(pTheme.FontSizePopupLeaderboardEntry() * 15, pTheme.FontSizePopupLeaderboardTitle() * 10) + 4;
 }
 
+ScoreboardViewModel::ScoreboardViewModel() noexcept
+{
+    GSL_SUPPRESS_F6 SetPopupType(ra::ui::viewmodels::Popup::LeaderboardScoreboard);
+}
+
 void ScoreboardViewModel::BeginAnimation()
 {
     m_fAnimationProgress = 0.0;
@@ -36,19 +41,17 @@ void ScoreboardViewModel::BeginAnimation()
     const auto nWidth = CalculateScoreboardWidth(pTheme);
 
     // bottom margin 10px
-    SetRenderLocationY(10 + nHeight);
-    SetRenderLocationYRelativePosition(RelativePosition::Far);
+    SetVerticalOffset(10);
 
     // animate to right margin 10px.
-    m_nInitialX = 0;
-    m_nTargetX = 10 + nWidth;
-    SetRenderLocationX(m_nInitialX);
-    SetRenderLocationXRelativePosition(RelativePosition::Far);
+    m_nInitialX = -nWidth;
+    m_nTargetX = 10;
+    SetHorizontalOffset(m_nInitialX);
 }
 
 bool ScoreboardViewModel::UpdateRenderImage(double fElapsed)
 {
-    const int nOldX = GetRenderLocationX();
+    const int nOldX = GetHorizontalOffset();
 
     m_fAnimationProgress += fElapsed;
     const int nNewX = GetFadeOffset(m_fAnimationProgress, TOTAL_ANIMATION_TIME, INOUT_TIME, m_nInitialX, m_nTargetX);
@@ -56,7 +59,7 @@ bool ScoreboardViewModel::UpdateRenderImage(double fElapsed)
     bool bUpdated = false;
     if (nNewX != nOldX)
     {
-        SetRenderLocationX(nNewX);
+        SetHorizontalOffset(nNewX);
         bUpdated = true;
     }
 
diff --git a/src/ui/viewmodels/ScoreboardViewModel.hh b/src/ui/viewmodels/ScoreboardViewModel.hh
index 5f376bc1..ab75748f 100644
--- a/src/ui/viewmodels/ScoreboardViewModel.hh
+++ b/src/ui/viewmodels/ScoreboardViewModel.hh
@@ -13,6 +13,8 @@ namespace viewmodels {
 class ScoreboardViewModel : public PopupViewModelBase
 {
 public:
+    ScoreboardViewModel() noexcept;
+
     /// <summary>
     /// The <see cref="ModelProperty" /> for the header text.
     /// </summary>
diff --git a/src/ui/win32/OverlaySettingsDialog.cpp b/src/ui/win32/OverlaySettingsDialog.cpp
index e3966c79..74f737bf 100644
--- a/src/ui/win32/OverlaySettingsDialog.cpp
+++ b/src/ui/win32/OverlaySettingsDialog.cpp
@@ -29,7 +29,7 @@ void OverlaySettingsDialog::Presenter::ShowWindow(ra::ui::WindowViewModelBase& o
 
 OverlaySettingsDialog::OverlaySettingsDialog(ra::ui::viewmodels::OverlaySettingsViewModel& vmSettings)
     : DialogBase(vmSettings),
-      m_bindDisplayAchievementTrigger(vmSettings),
+      m_bindAchievementTriggerLocation(vmSettings),
       m_bindScreenshotAchievementTrigger(vmSettings),
       m_bindDisplayMastery(vmSettings),
       m_bindScreenshotMastery(vmSettings),
@@ -37,25 +37,38 @@ OverlaySettingsDialog::OverlaySettingsDialog(ra::ui::viewmodels::OverlaySettings
       m_bindDisplayLeaderboardCanceled(vmSettings),
       m_bindDisplayLeaderboardValue(vmSettings),
       m_bindDisplayLeaderboardScoreboards(vmSettings),
-      m_bindScreenshotLocation(vmSettings)
+      m_bindScreenshotLocation(vmSettings),
+      m_bindInformationLocation(vmSettings)
 {
     m_bindWindow.SetInitialPosition(RelativePosition::Center, RelativePosition::Center);
 
-    m_bindDisplayAchievementTrigger.BindCheck(ra::ui::viewmodels::OverlaySettingsViewModel::DisplayAchievementTriggerProperty);
+    m_bindAchievementTriggerLocation.BindItems(vmSettings.PopupLocations());
+    m_bindAchievementTriggerLocation.BindSelectedItem(ra::ui::viewmodels::OverlaySettingsViewModel::AchievementTriggerLocationProperty);
+
     m_bindScreenshotAchievementTrigger.BindCheck(ra::ui::viewmodels::OverlaySettingsViewModel::ScreenshotAchievementTriggerProperty);
-    m_bindDisplayMastery.BindCheck(ra::ui::viewmodels::OverlaySettingsViewModel::DisplayMasteryProperty);
+
+    m_bindDisplayMastery.BindItems(vmSettings.PopupLocations());
+    m_bindDisplayMastery.BindSelectedItem(ra::ui::viewmodels::OverlaySettingsViewModel::MasteryLocationProperty);
     m_bindScreenshotMastery.BindCheck(ra::ui::viewmodels::OverlaySettingsViewModel::ScreenshotMasteryProperty);
-    m_bindDisplayLeaderboardStarted.BindCheck(ra::ui::viewmodels::OverlaySettingsViewModel::DisplayLeaderboardStartedProperty);
-    m_bindDisplayLeaderboardCanceled.BindCheck(ra::ui::viewmodels::OverlaySettingsViewModel::DisplayLeaderboardCanceledProperty);
-    m_bindDisplayLeaderboardValue.BindCheck(ra::ui::viewmodels::OverlaySettingsViewModel::DisplayLeaderboardValueProperty);
-    m_bindDisplayLeaderboardScoreboards.BindCheck(ra::ui::viewmodels::OverlaySettingsViewModel::DisplayLeaderboardScoreboardProperty);
+
+    m_bindDisplayLeaderboardStarted.BindItems(vmSettings.PopupLocations());
+    m_bindDisplayLeaderboardStarted.BindSelectedItem(ra::ui::viewmodels::OverlaySettingsViewModel::LeaderboardStartedLocationProperty);
+    m_bindDisplayLeaderboardCanceled.BindItems(vmSettings.PopupLocations());
+    m_bindDisplayLeaderboardCanceled.BindSelectedItem(ra::ui::viewmodels::OverlaySettingsViewModel::LeaderboardCanceledLocationProperty);
+    m_bindDisplayLeaderboardValue.BindItems(vmSettings.PopupLocations());
+    m_bindDisplayLeaderboardValue.BindSelectedItem(ra::ui::viewmodels::OverlaySettingsViewModel::LeaderboardTrackerLocationProperty);
+    m_bindDisplayLeaderboardScoreboards.BindItems(vmSettings.PopupLocationsNoMiddle());
+    m_bindDisplayLeaderboardScoreboards.BindSelectedItem(ra::ui::viewmodels::OverlaySettingsViewModel::LeaderboardScoreboardLocationProperty);
+
+    m_bindInformationLocation.BindItems(vmSettings.PopupLocations());
+    m_bindInformationLocation.BindSelectedItem(ra::ui::viewmodels::OverlaySettingsViewModel::MessageLocationProperty);
 
     m_bindScreenshotLocation.BindText(ra::ui::viewmodels::OverlaySettingsViewModel::ScreenshotLocationProperty);
 }
 
 BOOL OverlaySettingsDialog::OnInitDialog()
 {
-    m_bindDisplayAchievementTrigger.SetControl(*this, IDC_RA_DISPLAY_TRIGGER);
+    m_bindAchievementTriggerLocation.SetControl(*this, IDC_RA_DISPLAY_TRIGGER);
     m_bindScreenshotAchievementTrigger.SetControl(*this, IDC_RA_SCREENSHOT_TRIGGER);
     m_bindDisplayMastery.SetControl(*this, IDC_RA_DISPLAY_MASTERY);
     m_bindScreenshotMastery.SetControl(*this, IDC_RA_SCREENSHOT_MASTERY);
@@ -63,6 +76,7 @@ BOOL OverlaySettingsDialog::OnInitDialog()
     m_bindDisplayLeaderboardCanceled.SetControl(*this, IDC_RA_DISPLAY_LBOARD_CANCEL);
     m_bindDisplayLeaderboardValue.SetControl(*this, IDC_RA_DISPLAY_LBOARD_VALUE);
     m_bindDisplayLeaderboardScoreboards.SetControl(*this, IDC_RA_DISPLAY_LBOARD_SCOREBOARD);
+    m_bindInformationLocation.SetControl(*this, IDC_RA_DISPLAY_INFORMATION);
 
     m_bindScreenshotLocation.SetControl(*this, IDC_RA_SCREENSHOT_LOCATION);
 
diff --git a/src/ui/win32/OverlaySettingsDialog.hh b/src/ui/win32/OverlaySettingsDialog.hh
index 8bf75cc5..f4f16272 100644
--- a/src/ui/win32/OverlaySettingsDialog.hh
+++ b/src/ui/win32/OverlaySettingsDialog.hh
@@ -6,6 +6,7 @@
 #include "ui/win32/DialogBase.hh"
 #include "ui/win32/IDialogPresenter.hh"
 #include "ui/win32/bindings/CheckBoxBinding.hh"
+#include "ui/win32/bindings/ComboBoxBinding.hh"
 #include "ui/win32/bindings/TextBoxBinding.hh"
 
 namespace ra {
@@ -35,14 +36,15 @@ protected:
     BOOL OnCommand(WORD nCommand) override;
 
 private:
-    ra::ui::win32::bindings::CheckBoxBinding m_bindDisplayAchievementTrigger;
+    ra::ui::win32::bindings::ComboBoxBinding m_bindAchievementTriggerLocation;
     ra::ui::win32::bindings::CheckBoxBinding m_bindScreenshotAchievementTrigger;
-    ra::ui::win32::bindings::CheckBoxBinding m_bindDisplayMastery;
+    ra::ui::win32::bindings::ComboBoxBinding m_bindDisplayMastery;
     ra::ui::win32::bindings::CheckBoxBinding m_bindScreenshotMastery;
-    ra::ui::win32::bindings::CheckBoxBinding m_bindDisplayLeaderboardStarted;
-    ra::ui::win32::bindings::CheckBoxBinding m_bindDisplayLeaderboardCanceled;
-    ra::ui::win32::bindings::CheckBoxBinding m_bindDisplayLeaderboardValue;
-    ra::ui::win32::bindings::CheckBoxBinding m_bindDisplayLeaderboardScoreboards;
+    ra::ui::win32::bindings::ComboBoxBinding m_bindDisplayLeaderboardStarted;
+    ra::ui::win32::bindings::ComboBoxBinding m_bindDisplayLeaderboardCanceled;
+    ra::ui::win32::bindings::ComboBoxBinding m_bindDisplayLeaderboardValue;
+    ra::ui::win32::bindings::ComboBoxBinding m_bindDisplayLeaderboardScoreboards;
+    ra::ui::win32::bindings::ComboBoxBinding m_bindInformationLocation;
     ra::ui::win32::bindings::TextBoxBinding m_bindScreenshotLocation;
 };
 


Test patch:
diff --git a/tests/Exports_Tests.cpp b/tests/Exports_Tests.cpp
index a881ad08..60f8ecb1 100644
--- a/tests/Exports_Tests.cpp
+++ b/tests/Exports_Tests.cpp
@@ -537,7 +537,7 @@ public:
     TEST_METHOD(TestDoAchievementsFrameLeaderboardStart)
     {
         DoAchievementsFrameHarness harness;
-        harness.mockConfiguration.SetFeatureEnabled(ra::services::Feature::LeaderboardNotifications, true);
+        harness.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardStarted, ra::ui::viewmodels::PopupLocation::BottomLeft);
         auto& pLeaderboard = harness.mockGameContext.NewLeaderboard(1U);
         pLeaderboard.SetTitle("Title");
         pLeaderboard.SetDescription("Description");
@@ -562,7 +562,7 @@ public:
     TEST_METHOD(TestDoAchievementsFrameLeaderboardStartPopupDisabled)
     {
         DoAchievementsFrameHarness harness;
-        harness.mockConfiguration.SetFeatureEnabled(ra::services::Feature::LeaderboardNotifications, false);
+        harness.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardStarted, ra::ui::viewmodels::PopupLocation::None);
         harness.mockGameContext.NewLeaderboard(1U);
 
         harness.mockRuntime.QueueChange(ra::services::AchievementRuntime::ChangeType::LeaderboardStarted, 1U, 1234U);
@@ -580,7 +580,7 @@ public:
     TEST_METHOD(TestDoAchievementsFrameLeaderboardCanceled)
     {
         DoAchievementsFrameHarness harness;
-        harness.mockConfiguration.SetFeatureEnabled(ra::services::Feature::LeaderboardCancelNotifications, true);
+        harness.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardCanceled, ra::ui::viewmodels::PopupLocation::BottomLeft);
         auto& pLeaderboard = harness.mockGameContext.NewLeaderboard(1U);
         pLeaderboard.SetTitle("Title");
         pLeaderboard.SetDescription("Description");
@@ -602,7 +602,7 @@ public:
     TEST_METHOD(TestDoAchievementsFrameLeaderboardCaneledPopupDisabled)
     {
         DoAchievementsFrameHarness harness;
-        harness.mockConfiguration.SetFeatureEnabled(ra::services::Feature::LeaderboardCancelNotifications, false);
+        harness.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardCanceled, ra::ui::viewmodels::PopupLocation::None);
         auto& pLeaderboard = harness.mockGameContext.NewLeaderboard(1U);
         pLeaderboard.SetTitle("Title");
         pLeaderboard.SetDescription("Description");
diff --git a/tests/data/GameContext_Tests.cpp b/tests/data/GameContext_Tests.cpp
index d4f20c05..3576519c 100644
--- a/tests/data/GameContext_Tests.cpp
+++ b/tests/data/GameContext_Tests.cpp
@@ -1253,7 +1253,7 @@ public:
     TEST_METHOD(TestAwardAchievementNonExistant)
     {
         GameContextHarness game;
-        game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::AchievementTriggeredNotifications, true);
+        game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, ra::ui::viewmodels::PopupLocation::BottomLeft);
         game.mockServer.ExpectUncalled<ra::api::AwardAchievement>();
 
         game.AwardAchievement(1U);
@@ -1269,7 +1269,7 @@ public:
     TEST_METHOD(TestAwardAchievement)
     {
         GameContextHarness game;
-        game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::AchievementTriggeredNotifications, true);
+        game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, ra::ui::viewmodels::PopupLocation::BottomLeft);
         game.SetGameHash("hash");
         game.mockServer.HandleRequest<ra::api::AwardAchievement>([](const ra::api::AwardAchievement::Request& request, ra::api::AwardAchievement::Response& response)
         {
@@ -1302,7 +1302,7 @@ public:
     {
         GameContextHarness game;
         game.SetGameHash("hash");
-        game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::AchievementTriggeredNotifications, true);
+        game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, ra::ui::viewmodels::PopupLocation::BottomLeft);
         game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true);
         game.mockServer.HandleRequest<ra::api::AwardAchievement>([](const ra::api::AwardAchievement::Request& request, ra::api::AwardAchievement::Response& response)
         {
@@ -1334,7 +1334,7 @@ public:
     TEST_METHOD(TestAwardAchievementLocal)
     {
         GameContextHarness game;
-        game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::AchievementTriggeredNotifications, true);
+        game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, ra::ui::viewmodels::PopupLocation::BottomLeft);
         game.mockServer.ExpectUncalled<ra::api::AwardAchievement>();
 
         game.MockAchievement().SetCategory(Achievement::Category::Local);
@@ -1356,7 +1356,7 @@ public:
     TEST_METHOD(TestAwardAchievementUnofficial)
     {
         GameContextHarness game;
-        game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::AchievementTriggeredNotifications, true);
+        game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, ra::ui::viewmodels::PopupLocation::BottomLeft);
         game.mockServer.ExpectUncalled<ra::api::AwardAchievement>();
 
         game.MockAchievement().SetCategory(Achievement::Category::Unofficial);
@@ -1378,7 +1378,7 @@ public:
     TEST_METHOD(TestAwardAchievementModified)
     {
         GameContextHarness game;
-        game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::AchievementTriggeredNotifications, true);
+        game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, ra::ui::viewmodels::PopupLocation::BottomLeft);
         game.mockServer.ExpectUncalled<ra::api::AwardAchievement>();
 
         game.MockAchievement().SetModified(true);
@@ -1400,7 +1400,7 @@ public:
     TEST_METHOD(TestAwardAchievementModifiedLocal)
     {
         GameContextHarness game;
-        game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::AchievementTriggeredNotifications, true);
+        game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, ra::ui::viewmodels::PopupLocation::BottomLeft);
         game.mockServer.ExpectUncalled<ra::api::AwardAchievement>();
 
         auto& pAch = game.MockAchievement();
@@ -1424,7 +1424,7 @@ public:
     TEST_METHOD(TestAwardAchievementModifiedUnofficial)
     {
         GameContextHarness game;
-        game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::AchievementTriggeredNotifications, true);
+        game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, ra::ui::viewmodels::PopupLocation::BottomLeft);
         game.mockServer.ExpectUncalled<ra::api::AwardAchievement>();
 
         auto& pAch = game.MockAchievement();
@@ -1448,7 +1448,7 @@ public:
     TEST_METHOD(TestAwardAchievementMemoryModified)
     {
         GameContextHarness game;
-        game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::AchievementTriggeredNotifications, true);
+        game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, ra::ui::viewmodels::PopupLocation::BottomLeft);
         game.mockServer.ExpectUncalled<ra::api::AwardAchievement>();
 
         game.MockAchievement();
@@ -1472,7 +1472,7 @@ public:
     TEST_METHOD(TestAwardAchievementMemoryInsecureHardcore)
     {
         GameContextHarness game;
-        game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::AchievementTriggeredNotifications, true);
+        game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, ra::ui::viewmodels::PopupLocation::BottomLeft);
         game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true);
         game.mockServer.ExpectUncalled<ra::api::AwardAchievement>();
 
@@ -1498,7 +1498,7 @@ public:
     {
         GameContextHarness game;
         game.SetGameHash("hash");
-        game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::AchievementTriggeredNotifications, true);
+        game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, ra::ui::viewmodels::PopupLocation::BottomLeft);
         game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, false);
         game.mockServer.HandleRequest<ra::api::AwardAchievement>([](const ra::api::AwardAchievement::Request& request, ra::api::AwardAchievement::Response& response)
         {
@@ -1531,7 +1531,7 @@ public:
     TEST_METHOD(TestAwardAchievementDuplicate)
     {
         GameContextHarness game;
-        game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::AchievementTriggeredNotifications, true);
+        game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, ra::ui::viewmodels::PopupLocation::BottomLeft);
         game.mockServer.HandleRequest<ra::api::AwardAchievement>([](const ra::api::AwardAchievement::Request&, ra::api::AwardAchievement::Response& response)
         {
             response.ErrorMessage = "User already has this achievement awarded.";
@@ -1562,7 +1562,7 @@ public:
     TEST_METHOD(TestAwardAchievementDuplicateHardcore)
     {
         GameContextHarness game;
-        game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::AchievementTriggeredNotifications, true);
+        game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, ra::ui::viewmodels::PopupLocation::BottomLeft);
         game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true);
         game.mockServer.HandleRequest<ra::api::AwardAchievement>([](const ra::api::AwardAchievement::Request&, ra::api::AwardAchievement::Response& response)
         {
@@ -1594,7 +1594,7 @@ public:
     TEST_METHOD(TestAwardAchievementError)
     {
         GameContextHarness game;
-        game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::AchievementTriggeredNotifications, true);
+        game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, ra::ui::viewmodels::PopupLocation::BottomLeft);
         game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true);
         game.mockServer.HandleRequest<ra::api::AwardAchievement>([](const ra::api::AwardAchievement::Request&, ra::api::AwardAchievement::Response& response)
         {
@@ -1628,7 +1628,7 @@ public:
     TEST_METHOD(TestAwardAchievementErrorAfterPopupDismissed)
     {
         GameContextHarness game;
-        game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::AchievementTriggeredNotifications, true);
+        game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, ra::ui::viewmodels::PopupLocation::BottomLeft);
         game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true);
         game.mockServer.HandleRequest<ra::api::AwardAchievement>([](const ra::api::AwardAchievement::Request&, ra::api::AwardAchievement::Response& response)
         {
@@ -1667,7 +1667,7 @@ public:
     TEST_METHOD(TestAwardAchievementCompatibilityMode)
     {
         GameContextHarness game;
-        game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::AchievementTriggeredNotifications, true);
+        game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, ra::ui::viewmodels::PopupLocation::BottomLeft);
         game.mockServer.ExpectUncalled<ra::api::AwardAchievement>();
         game.SetMode(ra::data::GameContext::Mode::CompatibilityTest);
 
@@ -1690,8 +1690,8 @@ public:
     TEST_METHOD(TestAwardAchievementMasteryHardcore)
     {
         GameContextHarness game;
-        game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::AchievementTriggeredNotifications, true);
-        game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::MasteryNotification, true);
+        game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, ra::ui::viewmodels::PopupLocation::BottomLeft);
+        game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::Mastery, ra::ui::viewmodels::PopupLocation::TopMiddle);
         game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true);
         game.SetGameId(1U);
         game.SetGameHash("hash");
@@ -1756,8 +1756,8 @@ public:
     TEST_METHOD(TestAwardAchievementMasteryNonHardcore)
     {
         GameContextHarness game;
-        game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::AchievementTriggeredNotifications, true);
-        game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::MasteryNotification, true);
+        game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, ra::ui::viewmodels::PopupLocation::BottomLeft);
+        game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::Mastery, ra::ui::viewmodels::PopupLocation::TopMiddle);
         game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, false);
         game.SetGameId(1U);
         game.SetGameHash("hash");
@@ -1822,8 +1822,8 @@ public:
     TEST_METHOD(TestAwardAchievementMasteryIncomplete)
     {
         GameContextHarness game;
-        game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::AchievementTriggeredNotifications, true);
-        game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::MasteryNotification, true);
+        game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, ra::ui::viewmodels::PopupLocation::BottomLeft);
+        game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::Mastery, ra::ui::viewmodels::PopupLocation::TopMiddle);
         game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true);
         game.SetGameHash("hash");
         game.SetGameTitle(L"GameName");
@@ -1893,7 +1893,7 @@ public:
     {
         GameContextHarness game;
         game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true);
-        game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::LeaderboardScoreboards, true);
+        game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard, ra::ui::viewmodels::PopupLocation::BottomRight);
         game.mockUser.Initialize("Player", "ApiToken");
         game.SetGameHash("hash");
 
@@ -1967,7 +1967,7 @@ public:
     {
         GameContextHarness game;
         game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true);
-        game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::LeaderboardScoreboards, false);
+        game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard, ra::ui::viewmodels::PopupLocation::None);
         game.mockUser.Initialize("Player", "ApiToken");
         game.SetGameHash("hash");
 
@@ -2052,7 +2052,7 @@ public:
     {
         GameContextHarness game;
         game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true);
-        game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::LeaderboardScoreboards, true);
+        game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard, ra::ui::viewmodels::PopupLocation::BottomRight);
         game.mockUser.Initialize("Player", "ApiToken");
         game.SetGameHash("hash");
 
@@ -2115,7 +2115,7 @@ public:
     {
         GameContextHarness game;
         game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true);
-        game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::LeaderboardScoreboards, true);
+        game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard, ra::ui::viewmodels::PopupLocation::BottomRight);
         game.mockUser.Initialize("Player", "ApiToken");
         game.SetGameHash("hash");
 
diff --git a/tests/mocks/MockConfiguration.hh b/tests/mocks/MockConfiguration.hh
index 7d457fb0..02db0ea2 100644
--- a/tests/mocks/MockConfiguration.hh
+++ b/tests/mocks/MockConfiguration.hh
@@ -36,6 +36,16 @@ public:
             m_vEnabledFeatures.erase(nFeature);
     }
 
+    ra::ui::viewmodels::PopupLocation GetPopupLocation(ra::ui::viewmodels::Popup nPopup) const override
+    {
+        return m_vPopupLocations.at(ra::etoi(nPopup));
+    }
+
+    void SetPopupLocation(ra::ui::viewmodels::Popup nPopup, ra::ui::viewmodels::PopupLocation nPopupLocation) override
+    {
+        m_vPopupLocations.at(ra::etoi(nPopup)) = nPopupLocation;
+    }
+
     unsigned int GetNumBackgroundThreads() const noexcept override { return m_nBackgroundThreads; }
 
     const std::wstring& GetRomDirectory() const noexcept override { return m_sRomDirectory; }
@@ -101,6 +111,7 @@ private:
     unsigned int m_nBackgroundThreads = 0;
 
     std::set<Feature> m_vEnabledFeatures;
+    std::array<ra::ui::viewmodels::PopupLocation, ra::etoi(ra::ui::viewmodels::Popup::NumPopups)> m_vPopupLocations = {};
 };
 
 } // namespace mocks
diff --git a/tests/services/JsonFileConfiguration_Tests.cpp b/tests/services/JsonFileConfiguration_Tests.cpp
index 91cd7a30..0a9823d4 100644
--- a/tests/services/JsonFileConfiguration_Tests.cpp
+++ b/tests/services/JsonFileConfiguration_Tests.cpp
@@ -7,6 +7,39 @@ using namespace Microsoft::VisualStudio::CppUnitTestFramework;
 
 using ra::services::mocks::MockFileSystem;
 
+namespace Microsoft {
+namespace VisualStudio {
+namespace CppUnitTestFramework {
+
+template<>
+std::wstring ToString<ra::ui::viewmodels::PopupLocation>(
+    const ra::ui::viewmodels::PopupLocation& nPopupLocation)
+{
+    switch (nPopupLocation)
+    {
+        case ra::ui::viewmodels::PopupLocation::None:
+            return L"None";
+        case ra::ui::viewmodels::PopupLocation::TopLeft:
+            return L"TopLeft";
+        case ra::ui::viewmodels::PopupLocation::TopMiddle:
+            return L"TopMiddle";
+        case ra::ui::viewmodels::PopupLocation::TopRight:
+            return L"TopRight";
+        case ra::ui::viewmodels::PopupLocation::BottomLeft:
+            return L"BottomLeft";
+        case ra::ui::viewmodels::PopupLocation::BottomMiddle:
+            return L"BottomMiddle";
+        case ra::ui::viewmodels::PopupLocation::BottomRight:
+            return L"BottomRight";
+        default:
+            return std::to_wstring(static_cast<int>(nPopupLocation));
+    }
+}
+
+} // namespace CppUnitTestFramework
+} // namespace VisualStudio
+} // namespace Microsoft
+
 namespace ra {
 namespace services {
 namespace impl {
@@ -118,29 +151,87 @@ public:
         TestFeature(ra::services::Feature::Hardcore, "Hardcore Active", true);
     }
 
+    TEST_METHOD(TestNonHardcoreWarning)
+    {
+        TestFeature(ra::services::Feature::NonHardcoreWarning, "Non Hardcore Warning", false);
+    }
+
+    TEST_METHOD(TestAchievementTriggeredScreenshot)
+    {
+        TestFeature(ra::services::Feature::AchievementTriggeredScreenshot, "Achievement Triggered Screenshot", false);
+    }
+
+    TEST_METHOD(TestMasteryScreenshot)
+    {
+        TestFeature(ra::services::Feature::MasteryNotificationScreenshot, "Mastery Screenshot", false);
+    }
+
     TEST_METHOD(TestLeaderboardsActive)
     {
         TestFeature(ra::services::Feature::Leaderboards, "Leaderboards Active", true);
     }
 
+    TEST_METHOD(TestPreferDecimal)
+    {
+        TestFeature(ra::services::Feature::PreferDecimal, "Prefer Decimal", false);
+    }
+
+    void TestPopupLocation(ra::ui::viewmodels::Popup nPopup, const std::string& sJsonKey, ra::ui::viewmodels::PopupLocation nDefault)
+    {
+        MockFileSystem fileSystem;
+
+        // uninitialized
+        JsonFileConfiguration config;
+        Assert::AreEqual(ra::ui::viewmodels::PopupLocation::None, config.GetPopupLocation(nPopup), L"default");
+
+        // no file
+        Assert::IsFalse(config.Load(sFilename));
+        Assert::AreEqual(nDefault, config.GetPopupLocation(nPopup), L"no file");
+
+        // no value provided
+        fileSystem.MockFile(sFilename, "{}");
+        Assert::IsTrue(config.Load(sFilename));
+        Assert::AreEqual(nDefault, config.GetPopupLocation(nPopup), L"empty file");
+
+        // value provided
+        std::string sJson = "{\"" + sJsonKey + "\":\"TopRight\"}";
+        fileSystem.MockFile(sFilename, sJson);
+        Assert::IsTrue(config.Load(sFilename));
+        Assert::AreEqual(ra::ui::viewmodels::PopupLocation::TopRight, config.GetPopupLocation(nPopup), L"value provided");
+
+        // provide value
+        config.SetPopupLocation(nPopup, ra::ui::viewmodels::PopupLocation::TopLeft);
+        Assert::AreEqual(ra::ui::viewmodels::PopupLocation::TopLeft, config.GetPopupLocation(nPopup), L"value set");
+
+        // persist value
+        config.Save();
+        sJson = "\"" + sJsonKey + "\":\"TopLeft\"";
+        AssertContains(fileSystem.GetFileContents(sFilename), sJson);
+    }
+
     TEST_METHOD(TestLeaderboardNotificationDisplay)
     {
-        TestFeature(ra::services::Feature::LeaderboardNotifications, "Leaderboard Notification Display", true);
+        TestPopupLocation(ra::ui::viewmodels::Popup::LeaderboardStarted, "Leaderboard Notification Display", ra::ui::viewmodels::PopupLocation::BottomLeft);
+    }
+
+    TEST_METHOD(TestLeaderboardCanceledDisplay)
+    {
+        TestPopupLocation(ra::ui::viewmodels::Popup::LeaderboardCanceled, "Leaderboard Cancel Display", ra::ui::viewmodels::PopupLocation::BottomLeft);
     }
 
     TEST_METHOD(TestLeaderboardCounterDisplay)
     {
-        TestFeature(ra::services::Feature::LeaderboardCounters, "Leaderboard Counter Display", true);
+        TestPopupLocation(ra::ui::viewmodels::Popup::LeaderboardTracker, "Leaderboard Counter Display", ra::ui::viewmodels::PopupLocation::BottomRight);
     }
 
     TEST_METHOD(TestLeaderboardsScoreboardDisplay)
     {
-        TestFeature(ra::services::Feature::LeaderboardScoreboards, "Leaderboard Scoreboard Display", true);
+        TestPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard, "Leaderboard Scoreboard Display", ra::ui::viewmodels::PopupLocation::BottomRight);
     }
 
-    TEST_METHOD(TestPreferDecimal)
+    TEST_METHOD(TestMessageDisplay)
     {
-        TestFeature(ra::services::Feature::PreferDecimal, "Prefer Decimal", false);
+        TestPopupLocation(ra::ui::viewmodels::Popup::Message, "Informational Notification Display", ra::ui::viewmodels::PopupLocation::BottomLeft);
     }
 
     TEST_METHOD(TestHostNameNoFile)
diff --git a/tests/ui/viewmodels/OverlayManager_Tests.cpp b/tests/ui/viewmodels/OverlayManager_Tests.cpp
index 608c0344..4b75a1e3 100644
--- a/tests/ui/viewmodels/OverlayManager_Tests.cpp
+++ b/tests/ui/viewmodels/OverlayManager_Tests.cpp
@@ -75,7 +75,7 @@ private:
             m_bHideRequested = false;
         }
 
-        int GetOverlayRenderX() const { return m_vmOverlay.GetRenderLocationX(); }
+        int GetOverlayRenderX() const { return m_vmOverlay.GetHorizontalOffset(); }
 
     private:
         bool m_bRenderRequested = false;
@@ -162,7 +162,7 @@ public:
 
         ra::ui::drawing::mocks::MockSurface mockSurface(800, 600);
         overlay.Render(mockSurface, false);
-        Assert::AreEqual(pPopup->GetRenderLocationY(), 0);
+        Assert::AreEqual(pPopup->GetVerticalOffset(), -74);
         // a queued popup still requests a render even if it's not visible. this ensures the Render
         // callback will be called to animate the popup
         Assert::IsTrue(overlay.WasRenderRequested());
@@ -173,8 +173,8 @@ public:
         overlay.mockClock.AdvanceTime(std::chrono::milliseconds(500));
         overlay.ResetRenderRequested();
         overlay.Render(mockSurface, false);
-        Assert::IsTrue(pPopup->GetRenderLocationY() > 0);
-        Assert::IsTrue(pPopup->GetRenderLocationY() < 80);
+        Assert::IsTrue(pPopup->GetVerticalOffset() > -74);
+        Assert::IsTrue(pPopup->GetVerticalOffset() < 10);
         Assert::IsTrue(overlay.WasRenderRequested());
         Assert::IsFalse(overlay.WasShowRequested());
         Assert::IsFalse(overlay.WasHideRequested());
@@ -183,7 +183,7 @@ public:
         overlay.mockClock.AdvanceTime(std::chrono::milliseconds(500));
         overlay.ResetRenderRequested();
         overlay.Render(mockSurface, false);
-        Assert::AreEqual(pPopup->GetRenderLocationY(), 80);
+        Assert::AreEqual(pPopup->GetVerticalOffset(), 10);
         Assert::IsTrue(overlay.WasRenderRequested());
         Assert::IsFalse(overlay.WasShowRequested());
         Assert::IsFalse(overlay.WasHideRequested());
@@ -192,7 +192,7 @@ public:
         overlay.mockClock.AdvanceTime(std::chrono::seconds(1));
         overlay.ResetRenderRequested();
         overlay.Render(mockSurface, false);
-        Assert::AreEqual(pPopup->GetRenderLocationY(), 80);
+        Assert::AreEqual(pPopup->GetVerticalOffset(), 10);
         Assert::IsTrue(overlay.WasRenderRequested());
         Assert::IsFalse(overlay.WasShowRequested());
         Assert::IsFalse(overlay.WasHideRequested());
@@ -201,7 +201,7 @@ public:
         overlay.mockClock.AdvanceTime(std::chrono::seconds(1));
         overlay.ResetRenderRequested();
         overlay.Render(mockSurface, false);
-        Assert::AreEqual(pPopup->GetRenderLocationY(), 80);
+        Assert::AreEqual(pPopup->GetVerticalOffset(), 10);
         Assert::IsTrue(overlay.WasRenderRequested());
         Assert::IsFalse(overlay.WasShowRequested());
         Assert::IsFalse(overlay.WasHideRequested());
@@ -210,7 +210,7 @@ public:
         overlay.mockClock.AdvanceTime(std::chrono::seconds(1));
         overlay.ResetRenderRequested();
         overlay.Render(mockSurface, false);
-        Assert::AreEqual(pPopup->GetRenderLocationY(), 80);
+        Assert::AreEqual(pPopup->GetVerticalOffset(), 10);
         Assert::IsTrue(overlay.WasRenderRequested());
         Assert::IsFalse(overlay.WasShowRequested());
         Assert::IsFalse(overlay.WasHideRequested());
@@ -219,8 +219,8 @@ public:
         overlay.mockClock.AdvanceTime(std::chrono::milliseconds(500));
         overlay.ResetRenderRequested();
         overlay.Render(mockSurface, false);
-        Assert::IsTrue(pPopup->GetRenderLocationY() > 0);
-        Assert::IsTrue(pPopup->GetRenderLocationY() < 80);
+        Assert::IsTrue(pPopup->GetVerticalOffset() > -74);
+        Assert::IsTrue(pPopup->GetVerticalOffset() < 10);
         Assert::IsTrue(overlay.WasRenderRequested());
         Assert::IsFalse(overlay.WasShowRequested());
         Assert::IsFalse(overlay.WasHideRequested());
@@ -240,20 +240,20 @@ public:
     TEST_METHOD(TestAddRemoveScoreTrackerLeaderboardEnabled)
     {
         OverlayManagerHarness overlay;
-        overlay.mockConfiguration.SetFeatureEnabled(ra::services::Feature::LeaderboardCounters, true);
+        overlay.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardTracker, ra::ui::viewmodels::PopupLocation::BottomRight);
 
         const auto& vmScoreTracker = overlay.AddScoreTracker(3);
         Assert::AreEqual(3, vmScoreTracker.GetPopupId());
         Assert::AreEqual(std::wstring(L"0"), vmScoreTracker.GetDisplayText());
         Assert::IsTrue(overlay.WasRenderRequested());
         Assert::IsFalse(vmScoreTracker.IsDestroyPending());
-        Assert::AreEqual(13, vmScoreTracker.GetRenderLocationY());
+        Assert::AreEqual(10, vmScoreTracker.GetVerticalOffset());
 
         Assert::IsTrue(&vmScoreTracker == overlay.GetScoreTracker(3));
 
         ra::ui::drawing::mocks::MockSurface mockSurface(800, 600);
         overlay.Render(mockSurface, false);
-        Assert::AreEqual(13, vmScoreTracker.GetRenderLocationY());
+        Assert::AreEqual(10, vmScoreTracker.GetVerticalOffset());
 
         overlay.ResetRenderRequested();
         overlay.RemoveScoreTracker(3);
@@ -268,20 +268,20 @@ public:
     TEST_METHOD(TestAddRemoveScoreTrackerLeaderboardDisabled)
     {
         OverlayManagerHarness overlay;
-        overlay.mockConfiguration.SetFeatureEnabled(ra::services::Feature::LeaderboardCounters, false);
+        overlay.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardTracker, ra::ui::viewmodels::PopupLocation::None);
 
         const auto& vmScoreTracker = overlay.AddScoreTracker(3);
         Assert::AreEqual(3, vmScoreTracker.GetPopupId());
         Assert::AreEqual(std::wstring(L"0"), vmScoreTracker.GetDisplayText());
         Assert::IsTrue(overlay.WasRenderRequested());
         Assert::IsFalse(vmScoreTracker.IsDestroyPending());
-        Assert::AreEqual(13, vmScoreTracker.GetRenderLocationY());
+        Assert::AreEqual(10, vmScoreTracker.GetVerticalOffset());
 
         Assert::IsTrue(&vmScoreTracker == overlay.GetScoreTracker(3));
 
         ra::ui::drawing::mocks::MockSurface mockSurface(800, 600);
         overlay.Render(mockSurface, false);
-        Assert::AreEqual(13, vmScoreTracker.GetRenderLocationY());
+        Assert::AreEqual(10, vmScoreTracker.GetVerticalOffset());
 
         overlay.ResetRenderRequested();
         overlay.RemoveScoreTracker(3);
@@ -320,11 +320,11 @@ public:
         const auto* pScoreboard = overlay.GetScoreboard(3);
         Expects(pScoreboard != nullptr);
 
-        constexpr auto nExpectedX = 288;
+        constexpr auto nScoreboardWidth = 288;
 
         ra::ui::drawing::mocks::MockSurface mockSurface(800, 600);
         overlay.Render(mockSurface, false);
-        Assert::AreEqual(pScoreboard->GetRenderLocationX(), 0);
+        Assert::AreEqual(pScoreboard->GetHorizontalOffset(), 10 - nScoreboardWidth);
         // no time has elapsed since the popup was queued, we're still waiting for the first render
         Assert::IsTrue(overlay.WasRenderRequested());
 
@@ -332,58 +332,58 @@ public:
         overlay.mockClock.AdvanceTime(std::chrono::milliseconds(500));
         overlay.ResetRenderRequested();
         overlay.Render(mockSurface, false);
-        Assert::IsTrue(pScoreboard->GetRenderLocationX() > 0);
-        Assert::IsTrue(pScoreboard->GetRenderLocationX() < nExpectedX);
+        Assert::IsTrue(pScoreboard->GetHorizontalOffset() > 10 - nScoreboardWidth);
+        Assert::IsTrue(pScoreboard->GetHorizontalOffset() < 10);
         Assert::IsTrue(overlay.WasRenderRequested());
 
         // after 1 second, it should be fully on screen
         overlay.mockClock.AdvanceTime(std::chrono::milliseconds(500));
         overlay.ResetRenderRequested();
         overlay.Render(mockSurface, false);
-        Assert::AreEqual(nExpectedX, pScoreboard->GetRenderLocationX());
+        Assert::AreEqual(10, pScoreboard->GetHorizontalOffset());
         Assert::IsTrue(overlay.WasRenderRequested());
 
         // after 2 seconds, it should still be fully on screen
         overlay.mockClock.AdvanceTime(std::chrono::seconds(1));
         overlay.ResetRenderRequested();
         overlay.Render(mockSurface, false);
-        Assert::AreEqual(nExpectedX, pScoreboard->GetRenderLocationX());
+        Assert::AreEqual(10, pScoreboard->GetHorizontalOffset());
         Assert::IsTrue(overlay.WasRenderRequested());
 
         // after 3 seconds, it should still be fully on screen
         overlay.mockClock.AdvanceTime(std::chrono::seconds(1));
         overlay.ResetRenderRequested();
         overlay.Render(mockSurface, false);
-        Assert::AreEqual(nExpectedX, pScoreboard->GetRenderLocationX());
+        Assert::AreEqual(10, pScoreboard->GetHorizontalOffset());
         Assert::IsTrue(overlay.WasRenderRequested());
 
         // after 4 seconds, it should still be fully on screen
         overlay.mockClock.AdvanceTime(std::chrono::seconds(1));
         overlay.ResetRenderRequested();
         overlay.Render(mockSurface, false);
-        Assert::AreEqual(nExpectedX, pScoreboard->GetRenderLocationX());
+        Assert::AreEqual(10, pScoreboard->GetHorizontalOffset());
         Assert::IsTrue(overlay.WasRenderRequested());
 
         // after 5 seconds, it should still be fully on screen
         overlay.mockClock.AdvanceTime(std::chrono::seconds(1));
         overlay.ResetRenderRequested();
         overlay.Render(mockSurface, false);
-        Assert::AreEqual(nExpectedX, pScoreboard->GetRenderLocationX());
+        Assert::AreEqual(10, pScoreboard->GetHorizontalOffset());
         Assert::IsTrue(overlay.WasRenderRequested());
 
         // after 6 seconds, it should still be fully on screen
         overlay.mockClock.AdvanceTime(std::chrono::seconds(1));
         overlay.ResetRenderRequested();
         overlay.Render(mockSurface, false);
-        Assert::AreEqual(nExpectedX, pScoreboard->GetRenderLocationX());
+        Assert::AreEqual(10, pScoreboard->GetHorizontalOffset());
         Assert::IsTrue(overlay.WasRenderRequested());
 
         // after 6.5 seconds, it should be starting to scroll offscreen
         overlay.mockClock.AdvanceTime(std::chrono::milliseconds(500));
         overlay.ResetRenderRequested();
         overlay.Render(mockSurface, false);
-        Assert::IsTrue(pScoreboard->GetRenderLocationX() > 0);
-        Assert::IsTrue(pScoreboard->GetRenderLocationX() < nExpectedX);
+        Assert::IsTrue(pScoreboard->GetHorizontalOffset() > 10 - nScoreboardWidth);
+        Assert::IsTrue(pScoreboard->GetHorizontalOffset() < 10);
         Assert::IsTrue(overlay.WasRenderRequested());
         Assert::IsNotNull(overlay.GetScoreboard(3));
 
diff --git a/tests/ui/viewmodels/OverlaySettingsViewModel_Tests.cpp b/tests/ui/viewmodels/OverlaySettingsViewModel_Tests.cpp
index 79e3d6f8..8af0b579 100644
--- a/tests/ui/viewmodels/OverlaySettingsViewModel_Tests.cpp
+++ b/tests/ui/viewmodels/OverlaySettingsViewModel_Tests.cpp
@@ -10,6 +10,39 @@
 
 using namespace Microsoft::VisualStudio::CppUnitTestFramework;
 
+namespace Microsoft {
+namespace VisualStudio {
+namespace CppUnitTestFramework {
+
+template<>
+std::wstring ToString<ra::ui::viewmodels::PopupLocation>(
+    const ra::ui::viewmodels::PopupLocation& nPopupLocation)
+{
+    switch (nPopupLocation)
+    {
+        case ra::ui::viewmodels::PopupLocation::None:
+            return L"None";
+        case ra::ui::viewmodels::PopupLocation::TopLeft:
+            return L"TopLeft";
+        case ra::ui::viewmodels::PopupLocation::TopMiddle:
+            return L"TopMiddle";
+        case ra::ui::viewmodels::PopupLocation::TopRight:
+            return L"TopRight";
+        case ra::ui::viewmodels::PopupLocation::BottomLeft:
+            return L"BottomLeft";
+        case ra::ui::viewmodels::PopupLocation::BottomMiddle:
+            return L"BottomMiddle";
+        case ra::ui::viewmodels::PopupLocation::BottomRight:
+            return L"BottomRight";
+        default:
+            return std::to_wstring(static_cast<int>(nPopupLocation));
+    }
+}
+
+} // namespace CppUnitTestFramework
+} // namespace VisualStudio
+} // namespace Microsoft
+
 namespace ra {
 namespace ui {
 namespace viewmodels {
@@ -25,6 +58,70 @@ private:
         ra::ui::mocks::MockDesktop mockDesktop;
     };
 
+    void ValidatePopupLocationInitialize(ra::ui::viewmodels::Popup nPopup, std::function<ra::ui::viewmodels::PopupLocation(OverlaySettingsViewModel&)> fGetValue)
+    {
+        OverlaySettingsViewModelHarness vmSettings;
+        vmSettings.mockConfiguration.SetPopupLocation(nPopup, ra::ui::viewmodels::PopupLocation::None);
+        vmSettings.Initialize();
+        Assert::AreEqual(ra::ui::viewmodels::PopupLocation::None, fGetValue(vmSettings));
+
+        vmSettings.mockConfiguration.SetPopupLocation(nPopup, ra::ui::viewmodels::PopupLocation::TopLeft);
+        vmSettings.Initialize();
+        Assert::AreEqual(ra::ui::viewmodels::PopupLocation::TopLeft, fGetValue(vmSettings));
+
+        vmSettings.mockConfiguration.SetPopupLocation(nPopup, ra::ui::viewmodels::PopupLocation::TopMiddle);
+        vmSettings.Initialize();
+        Assert::AreEqual(ra::ui::viewmodels::PopupLocation::TopMiddle, fGetValue(vmSettings));
+
+        vmSettings.mockConfiguration.SetPopupLocation(nPopup, ra::ui::viewmodels::PopupLocation::TopRight);
+        vmSettings.Initialize();
+        Assert::AreEqual(ra::ui::viewmodels::PopupLocation::TopRight, fGetValue(vmSettings));
+
+        vmSettings.mockConfiguration.SetPopupLocation(nPopup, ra::ui::viewmodels::PopupLocation::BottomLeft);
+        vmSettings.Initialize();
+        Assert::AreEqual(ra::ui::viewmodels::PopupLocation::BottomLeft, fGetValue(vmSettings));
+
+        vmSettings.mockConfiguration.SetPopupLocation(nPopup, ra::ui::viewmodels::PopupLocation::BottomMiddle);
+        vmSettings.Initialize();
+        Assert::AreEqual(ra::ui::viewmodels::PopupLocation::BottomMiddle, fGetValue(vmSettings));
+
+        vmSettings.mockConfiguration.SetPopupLocation(nPopup, ra::ui::viewmodels::PopupLocation::BottomRight);
+        vmSettings.Initialize();
+        Assert::AreEqual(ra::ui::viewmodels::PopupLocation::BottomRight, fGetValue(vmSettings));
+    }
+
+    void ValidatePopupLocationCommit(ra::ui::viewmodels::Popup nPopup, std::function<void(OverlaySettingsViewModel&, ra::ui::viewmodels::PopupLocation)> fSetValue)
+    {
+        OverlaySettingsViewModelHarness vmSettings;
+        fSetValue(vmSettings, ra::ui::viewmodels::PopupLocation::None);
+        vmSettings.Commit();
+        Assert::AreEqual(ra::ui::viewmodels::PopupLocation::None, vmSettings.mockConfiguration.GetPopupLocation(nPopup));
+
+        fSetValue(vmSettings, ra::ui::viewmodels::PopupLocation::TopLeft);
+        vmSettings.Commit();
+        Assert::AreEqual(ra::ui::viewmodels::PopupLocation::TopLeft, vmSettings.mockConfiguration.GetPopupLocation(nPopup));
+
+        fSetValue(vmSettings, ra::ui::viewmodels::PopupLocation::TopMiddle);
+        vmSettings.Commit();
+        Assert::AreEqual(ra::ui::viewmodels::PopupLocation::TopMiddle, vmSettings.mockConfiguration.GetPopupLocation(nPopup));
+
+        fSetValue(vmSettings, ra::ui::viewmodels::PopupLocation::TopRight);
+        vmSettings.Commit();
+        Assert::AreEqual(ra::ui::viewmodels::PopupLocation::TopRight, vmSettings.mockConfiguration.GetPopupLocation(nPopup));
+
+        fSetValue(vmSettings, ra::ui::viewmodels::PopupLocation::BottomLeft);
+        vmSettings.Commit();
+        Assert::AreEqual(ra::ui::viewmodels::PopupLocation::BottomLeft, vmSettings.mockConfiguration.GetPopupLocation(nPopup));
+
+        fSetValue(vmSettings, ra::ui::viewmodels::PopupLocation::BottomMiddle);
+        vmSettings.Commit();
+        Assert::AreEqual(ra::ui::viewmodels::PopupLocation::BottomMiddle, vmSettings.mockConfiguration.GetPopupLocation(nPopup));
+
+        fSetValue(vmSettings, ra::ui::viewmodels::PopupLocation::BottomRight);
+        vmSettings.Commit();
+        Assert::AreEqual(ra::ui::viewmodels::PopupLocation::BottomRight, vmSettings.mockConfiguration.GetPopupLocation(nPopup));
+    }
+
     void ValidateFeatureInitialize(ra::services::Feature feature, std::function<bool(OverlaySettingsViewModel&)> fGetValue)
     {
         OverlaySettingsViewModelHarness vmSettings;
@@ -52,14 +149,14 @@ private:
 public:
     TEST_METHOD(TestInitialize)
     {
-        ValidateFeatureInitialize(ra::services::Feature::AchievementTriggeredNotifications, [](OverlaySettingsViewModel& vm) { return vm.DisplayAchievementTrigger(); });
+        ValidatePopupLocationInitialize(ra::ui::viewmodels::Popup::AchievementTriggered, [](OverlaySettingsViewModel& vm) { return vm.GetAchievementTriggerLocation(); });
         ValidateFeatureInitialize(ra::services::Feature::AchievementTriggeredScreenshot, [](OverlaySettingsViewModel& vm) { return vm.ScreenshotAchievementTrigger(); });
-        ValidateFeatureInitialize(ra::services::Feature::MasteryNotification, [](OverlaySettingsViewModel& vm) { return vm.DisplayMastery(); });
+        ValidatePopupLocationInitialize(ra::ui::viewmodels::Popup::Mastery, [](OverlaySettingsViewModel& vm) { return vm.GetMasteryLocation(); });
         ValidateFeatureInitialize(ra::services::Feature::MasteryNotificationScreenshot, [](OverlaySettingsViewModel& vm) { return vm.ScreenshotMastery(); });
-        ValidateFeatureInitialize(ra::services::Feature::LeaderboardNotifications, [](OverlaySettingsViewModel& vm) { return vm.DisplayLeaderboardStarted(); });
-        ValidateFeatureInitialize(ra::services::Feature::LeaderboardCancelNotifications, [](OverlaySettingsViewModel& vm) { return vm.DisplayLeaderboardCanceled(); });
-        ValidateFeatureInitialize(ra::services::Feature::LeaderboardCounters, [](OverlaySettingsViewModel& vm) { return vm.DisplayLeaderboardValue(); });
-        ValidateFeatureInitialize(ra::services::Feature::LeaderboardScoreboards, [](OverlaySettingsViewModel& vm) { return vm.DisplayLeaderboardScoreboard(); });
+        ValidatePopupLocationInitialize(ra::ui::viewmodels::Popup::LeaderboardStarted, [](OverlaySettingsViewModel& vm) { return vm.GetLeaderboardStartedLocation(); });
+        ValidatePopupLocationInitialize(ra::ui::viewmodels::Popup::LeaderboardCanceled, [](OverlaySettingsViewModel& vm) { return vm.GetLeaderboardCanceledLocation(); });
+        ValidatePopupLocationInitialize(ra::ui::viewmodels::Popup::LeaderboardTracker, [](OverlaySettingsViewModel& vm) { return vm.GetLeaderboardTrackerLocation(); });
+        ValidatePopupLocationInitialize(ra::ui::viewmodels::Popup::LeaderboardScoreboard, [](OverlaySettingsViewModel& vm) { return vm.GetLeaderboardScoreboardLocation(); });
 
         OverlaySettingsViewModelHarness vmSettings;
         vmSettings.mockConfiguration.SetScreenshotDirectory(L"C:\\Screenshots\\");
@@ -69,14 +166,14 @@ public:
 
     TEST_METHOD(TestCommit)
     {
-        ValidateFeatureCommit(ra::services::Feature::AchievementTriggeredNotifications, [](OverlaySettingsViewModel& vm, bool bValue) { return vm.SetDisplayAchievementTrigger(bValue); });
+        ValidatePopupLocationCommit(ra::ui::viewmodels::Popup::AchievementTriggered, [](OverlaySettingsViewModel& vm, ra::ui::viewmodels::PopupLocation nValue) { return vm.SetAchievementTriggerLocation(nValue); });
         ValidateFeatureCommit(ra::services::Feature::AchievementTriggeredScreenshot, [](OverlaySettingsViewModel& vm, bool bValue) { return vm.SetScreenshotAchievementTrigger(bValue); });
-        ValidateFeatureCommit(ra::services::Feature::MasteryNotification, [](OverlaySettingsViewModel& vm, bool bValue) { return vm.SetDisplayMastery(bValue); });
+        ValidatePopupLocationCommit(ra::ui::viewmodels::Popup::Mastery, [](OverlaySettingsViewModel& vm, ra::ui::viewmodels::PopupLocation nValue) { return vm.SetMasteryLocation(nValue); });
         ValidateFeatureCommit(ra::services::Feature::MasteryNotificationScreenshot, [](OverlaySettingsViewModel& vm, bool bValue) { return vm.SetScreenshotMastery(bValue); });
-        ValidateFeatureCommit(ra::services::Feature::LeaderboardNotifications, [](OverlaySettingsViewModel& vm, bool bValue) { return vm.SetDisplayLeaderboardStarted(bValue); });
-        ValidateFeatureCommit(ra::services::Feature::LeaderboardCancelNotifications, [](OverlaySettingsViewModel& vm, bool bValue) { return vm.SetDisplayLeaderboardCanceled(bValue); });
-        ValidateFeatureCommit(ra::services::Feature::LeaderboardCounters, [](OverlaySettingsViewModel& vm, bool bValue) { return vm.SetDisplayLeaderboardValue(bValue); });
-        ValidateFeatureCommit(ra::services::Feature::LeaderboardScoreboards, [](OverlaySettingsViewModel& vm, bool bValue) { return vm.SetDisplayLeaderboardScoreboard(bValue); });
+        ValidatePopupLocationCommit(ra::ui::viewmodels::Popup::LeaderboardStarted, [](OverlaySettingsViewModel& vm, ra::ui::viewmodels::PopupLocation nValue) { return vm.SetLeaderboardStartedLocation(nValue); });
+        ValidatePopupLocationCommit(ra::ui::viewmodels::Popup::LeaderboardCanceled, [](OverlaySettingsViewModel& vm, ra::ui::viewmodels::PopupLocation nValue) { return vm.SetLeaderboardCanceledLocation(nValue); });
+        ValidatePopupLocationCommit(ra::ui::viewmodels::Popup::LeaderboardTracker, [](OverlaySettingsViewModel& vm, ra::ui::viewmodels::PopupLocation nValue) { return vm.SetLeaderboardTrackerLocation(nValue); });
+        ValidatePopupLocationCommit(ra::ui::viewmodels::Popup::LeaderboardScoreboard, [](OverlaySettingsViewModel& vm, ra::ui::viewmodels::PopupLocation nValue) { return vm.SetLeaderboardScoreboardLocation(nValue); });
 
         OverlaySettingsViewModelHarness vmSettings;
         vmSettings.SetScreenshotLocation(L"C:\\Screenshots\\");
@@ -92,66 +189,66 @@ public:
     TEST_METHOD(TestAchievementTriggerDependencies)
     {
         OverlaySettingsViewModelHarness vmSettings;
-        vmSettings.SetDisplayAchievementTrigger(false);
+        vmSettings.SetAchievementTriggerLocation(ra::ui::viewmodels::PopupLocation::None);
         vmSettings.SetScreenshotAchievementTrigger(false);
-        Assert::IsFalse(vmSettings.DisplayAchievementTrigger());
+        Assert::AreEqual(ra::ui::viewmodels::PopupLocation::None, vmSettings.GetAchievementTriggerLocation());
         Assert::IsFalse(vmSettings.ScreenshotAchievementTrigger());
 
-        vmSettings.SetDisplayAchievementTrigger(true);
-        Assert::IsTrue(vmSettings.DisplayAchievementTrigger());
+        vmSettings.SetAchievementTriggerLocation(ra::ui::viewmodels::PopupLocation::BottomLeft);
+        Assert::AreEqual(ra::ui::viewmodels::PopupLocation::BottomLeft, vmSettings.GetAchievementTriggerLocation());
         Assert::IsFalse(vmSettings.ScreenshotAchievementTrigger());
 
         vmSettings.SetScreenshotAchievementTrigger(true);
-        Assert::IsTrue(vmSettings.DisplayAchievementTrigger());
+        Assert::AreEqual(ra::ui::viewmodels::PopupLocation::BottomLeft, vmSettings.GetAchievementTriggerLocation());
         Assert::IsTrue(vmSettings.ScreenshotAchievementTrigger());
 
-        vmSettings.SetDisplayAchievementTrigger(false);
-        Assert::IsFalse(vmSettings.DisplayAchievementTrigger());
+        vmSettings.SetAchievementTriggerLocation(ra::ui::viewmodels::PopupLocation::None);
+        Assert::AreEqual(ra::ui::viewmodels::PopupLocation::None, vmSettings.GetAchievementTriggerLocation());
         Assert::IsFalse(vmSettings.ScreenshotAchievementTrigger());
 
         vmSettings.SetScreenshotAchievementTrigger(true);
-        Assert::IsTrue(vmSettings.DisplayAchievementTrigger());
+        Assert::AreEqual(ra::ui::viewmodels::PopupLocation::BottomLeft, vmSettings.GetAchievementTriggerLocation());
         Assert::IsTrue(vmSettings.ScreenshotAchievementTrigger());
 
         vmSettings.SetScreenshotAchievementTrigger(false);
-        Assert::IsTrue(vmSettings.DisplayAchievementTrigger());
+        Assert::AreEqual(ra::ui::viewmodels::PopupLocation::BottomLeft, vmSettings.GetAchievementTriggerLocation());
         Assert::IsFalse(vmSettings.ScreenshotAchievementTrigger());
 
-        vmSettings.SetDisplayAchievementTrigger(false);
-        Assert::IsFalse(vmSettings.DisplayAchievementTrigger());
+        vmSettings.SetAchievementTriggerLocation(ra::ui::viewmodels::PopupLocation::None);
+        Assert::AreEqual(ra::ui::viewmodels::PopupLocation::None, vmSettings.GetAchievementTriggerLocation());
         Assert::IsFalse(vmSettings.ScreenshotAchievementTrigger());
     }
 
     TEST_METHOD(TestMasteryTriggerDependencies)
     {
         OverlaySettingsViewModelHarness vmSettings;
-        vmSettings.SetDisplayMastery(false);
+        vmSettings.SetMasteryLocation(ra::ui::viewmodels::PopupLocation::None);
         vmSettings.SetScreenshotMastery(false);
-        Assert::IsFalse(vmSettings.DisplayMastery());
+        Assert::AreEqual(ra::ui::viewmodels::PopupLocation::None, vmSettings.GetMasteryLocation());
         Assert::IsFalse(vmSettings.ScreenshotMastery());
 
-        vmSettings.SetDisplayMastery(true);
-        Assert::IsTrue(vmSettings.DisplayMastery());
+        vmSettings.SetMasteryLocation(ra::ui::viewmodels::PopupLocation::BottomLeft);
+        Assert::AreEqual(ra::ui::viewmodels::PopupLocation::BottomLeft, vmSettings.GetMasteryLocation());
         Assert::IsFalse(vmSettings.ScreenshotMastery());
 
         vmSettings.SetScreenshotMastery(true);
-        Assert::IsTrue(vmSettings.DisplayMastery());
+        Assert::AreEqual(ra::ui::viewmodels::PopupLocation::BottomLeft, vmSettings.GetMasteryLocation());
         Assert::IsTrue(vmSettings.ScreenshotMastery());
 
-        vmSettings.SetDisplayMastery(false);
-        Assert::IsFalse(vmSettings.DisplayMastery());
+        vmSettings.SetMasteryLocation(ra::ui::viewmodels::PopupLocation::None);
+        Assert::AreEqual(ra::ui::viewmodels::PopupLocation::None, vmSettings.GetMasteryLocation());
         Assert::IsFalse(vmSettings.ScreenshotMastery());
 
         vmSettings.SetScreenshotMastery(true);
-        Assert::IsTrue(vmSettings.DisplayMastery());
+        Assert::AreEqual(ra::ui::viewmodels::PopupLocation::TopMiddle, vmSettings.GetMasteryLocation());
         Assert::IsTrue(vmSettings.ScreenshotMastery());
 
         vmSettings.SetScreenshotMastery(false);
-        Assert::IsTrue(vmSettings.DisplayMastery());
+        Assert::AreEqual(ra::ui::viewmodels::PopupLocation::TopMiddle, vmSettings.GetMasteryLocation());
         Assert::IsFalse(vmSettings.ScreenshotMastery());
 
-        vmSettings.SetDisplayMastery(false);
-        Assert::IsFalse(vmSettings.DisplayMastery());
+        vmSettings.SetMasteryLocation(ra::ui::viewmodels::PopupLocation::None);
+        Assert::AreEqual(ra::ui::viewmodels::PopupLocation::None, vmSettings.GetMasteryLocation());
         Assert::IsFalse(vmSettings.ScreenshotMastery());
     }