diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 364af8f..54044ac 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -46,6 +46,8 @@ add_executable(MassBuilderSaveTool WIN32
Mass/Mass.cpp
Maps/LastMissionId.h
Maps/StoryProgress.h
+ ToastQueue/ToastQueue.h
+ ToastQueue/ToastQueue.cpp
FontAwesome/IconsFontAwesome5.h
FontAwesome/IconsFontAwesome5Brands.h
resource.rc
diff --git a/src/ToastQueue/ToastQueue.cpp b/src/ToastQueue/ToastQueue.cpp
new file mode 100644
index 0000000..60e7351
--- /dev/null
+++ b/src/ToastQueue/ToastQueue.cpp
@@ -0,0 +1,160 @@
+// MassBuilderSaveTool
+// Copyright (C) 2021 Guillaume Jacquemin
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+#include
+
+#include
+
+#include
+
+#include "../FontAwesome/IconsFontAwesome5.h"
+
+#include "ToastQueue.h"
+
+using namespace Corrade;
+
+constexpr UnsignedInt success_colour = 0xff67d23bu;
+constexpr UnsignedInt info_colour = 0xffcc832fu;
+constexpr UnsignedInt warning_colour = 0xff2fcfc7u;
+constexpr UnsignedInt error_colour = 0xff3134cdu;
+
+constexpr UnsignedInt fade_time = 150;
+constexpr Float base_opacity = 1.0f;
+constexpr Vector2 padding{20.0f, 20.0f};
+constexpr Float toast_spacing = 10.0f;
+
+Toast::Toast(Type type, const std::string& message, std::chrono::milliseconds timeout):
+ _type{type}, _message{message}, _timeout{timeout}, _creationTime{std::chrono::steady_clock::now()}
+{
+ _phaseTrack = Animation::Track{{
+ {0, Phase::FadeIn},
+ {fade_time, Phase::Wait},
+ {fade_time + timeout.count(), Phase::FadeOut},
+ {(fade_time * 2) + timeout.count(), Phase::TimedOut}
+ }, Math::select, Animation::Extrapolation::Constant};
+}
+
+auto Toast::type() -> Type {
+ return _type;
+}
+
+auto Toast::message() -> const std::string& {
+ return _message;
+}
+
+auto Toast::timeout() -> std::chrono::milliseconds {
+ return _timeout;
+}
+
+auto Toast::creationTime() -> std::chrono::steady_clock::time_point {
+ return _creationTime;
+}
+
+auto Toast::elapsedTime() -> std::chrono::milliseconds {
+ return std::chrono::duration_cast(std::chrono::steady_clock::now() - _creationTime);
+}
+
+auto Toast::phase() -> Phase {
+ return _phaseTrack.at(elapsedTime().count());
+}
+
+auto Toast::opacity() -> Float {
+ Phase phase = this->phase();
+ Long elapsed_time = elapsedTime().count();
+
+ if(phase == Phase::FadeIn) {
+ return Float(elapsed_time) / Float(fade_time);
+ }
+ else if(phase == Phase::FadeOut) {
+ return 1.0f - ((Float(elapsed_time) - Float(fade_time) - Float(_timeout.count())) / Float(fade_time));
+ }
+
+ return 1.0f;
+}
+
+void ToastQueue::addToast(Toast&& toast) {
+ _toasts.push_back(std::move(toast));
+}
+
+void ToastQueue::addToast(Toast::Type type, const std::string& message, std::chrono::milliseconds timeout) {
+ _toasts.emplace_back(type, message, timeout);
+}
+
+void ToastQueue::draw(Vector2i viewport_size) {
+ Float height = 0.0f;
+
+ for(UnsignedInt i = 0; i < _toasts.size(); i++) {
+ Toast* current = &_toasts[i];
+
+ if(current->phase() == Toast::Phase::TimedOut) {
+ removeToast(i);
+ continue;
+ }
+
+ std::string win_id = Utility::formatString("##Toast{}", i);
+
+ Float opacity = base_opacity * current->opacity();
+
+ ImGui::PushStyleVar(ImGuiStyleVar_Alpha, opacity);
+
+ ImGui::SetNextWindowPos({viewport_size.x() - padding.x(), viewport_size.y() - padding.y() - height}, ImGuiCond_Always, {1.0f, 1.0f});
+ if(ImGui::Begin(win_id.c_str(), nullptr,
+ ImGuiWindowFlags_AlwaysAutoResize|ImGuiWindowFlags_NoDecoration|
+ ImGuiWindowFlags_NoInputs|ImGuiWindowFlags_NoNav|ImGuiWindowFlags_NoFocusOnAppearing))
+ {
+ ImColor colour = 0xffffffff;
+
+ switch(current->type()) {
+ case Toast::Type::Default:
+ break;
+ case Toast::Type::Success:
+ colour = success_colour;
+ ImGui::TextColored(colour, ICON_FA_CHECK_CIRCLE);
+ break;
+ case Toast::Type::Info:
+ colour = info_colour;
+ ImGui::TextColored(colour, ICON_FA_INFO_CIRCLE);
+ break;
+ case Toast::Type::Warning:
+ colour = warning_colour;
+ ImGui::TextColored(colour, ICON_FA_EXCLAMATION_TRIANGLE);
+ break;
+ case Toast::Type::Error:
+ colour = error_colour;
+ ImGui::TextColored(colour, ICON_FA_TIMES_CIRCLE);
+ break;
+ }
+
+ ImGui::SameLine();
+
+ if(current->message().length() > 127) {
+ ImGui::TextColored(colour, "%.*s...", 127, current->message().c_str());
+ }
+ else {
+ ImGui::TextColored(colour, current->message().c_str());
+ }
+
+ height += ImGui::GetWindowHeight() + toast_spacing;
+ }
+ ImGui::End();
+
+ ImGui::PopStyleVar();
+ }
+}
+
+void ToastQueue::removeToast(Long index) {
+ _toasts.erase(_toasts.begin() + index);
+}
diff --git a/src/ToastQueue/ToastQueue.h b/src/ToastQueue/ToastQueue.h
new file mode 100644
index 0000000..3c2d4ce
--- /dev/null
+++ b/src/ToastQueue/ToastQueue.h
@@ -0,0 +1,82 @@
+#pragma once
+
+// MassBuilderSaveTool
+// Copyright (C) 2021 Guillaume Jacquemin
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+#include
+#include
+#include
+
+#include
+#include
+
+using namespace Magnum;
+
+class Toast {
+ public:
+ enum class Type : UnsignedByte {
+ Default, Success, Info, Warning, Error
+ };
+
+ enum class Phase : UnsignedByte {
+ FadeIn, Wait, FadeOut, TimedOut
+ };
+
+ explicit Toast(Type type, const std::string& message,
+ std::chrono::milliseconds timeout = std::chrono::milliseconds{3000});
+
+ Toast(const Toast& other) = delete;
+ Toast& operator=(const Toast& other) = delete;
+
+ Toast(Toast&& other) = default;
+ Toast& operator=(Toast&& other) = default;
+
+ auto type() -> Type;
+
+ auto message() -> std::string const&;
+
+ auto timeout() -> std::chrono::milliseconds;
+
+ auto creationTime() -> std::chrono::steady_clock::time_point;
+
+ auto elapsedTime() -> std::chrono::milliseconds;
+
+ auto phase() -> Phase;
+
+ auto opacity() -> Float;
+
+ private:
+ Type _type{Type::Default};
+ std::string _message;
+ std::chrono::milliseconds _timeout;
+ std::chrono::steady_clock::time_point _creationTime;
+ Animation::Track _phaseTrack;
+};
+
+class ToastQueue {
+ public:
+ void addToast(Toast&& toast);
+
+ void addToast(Toast::Type type, const std::string& message,
+ std::chrono::milliseconds timeout = std::chrono::milliseconds{3000});
+
+ void draw(Vector2i viewport_size);
+
+ private:
+ void removeToast(Long index);
+
+ std::vector _toasts;
+};