256 lines
8.3 KiB
C++
256 lines
8.3 KiB
C++
#pragma once
|
|
|
|
#include "imgui.h"
|
|
#include "AnimationUtils.h"
|
|
|
|
#include <string>
|
|
#include <vector>
|
|
#include <functional>
|
|
|
|
enum class NotificationLevel {
|
|
Info,
|
|
Success,
|
|
Warning,
|
|
Error
|
|
};
|
|
|
|
struct NotificationTarget {
|
|
std::string path;
|
|
int line = -1; // zero-based
|
|
int col = -1; // zero-based
|
|
};
|
|
|
|
struct Notification {
|
|
int id = 0;
|
|
NotificationLevel level = NotificationLevel::Info;
|
|
std::string message;
|
|
double timestamp = 0.0;
|
|
float durationSeconds = 4.0f;
|
|
bool dismissed = false;
|
|
bool read = false;
|
|
bool hasTarget = false;
|
|
NotificationTarget target;
|
|
};
|
|
|
|
class NotificationSystem {
|
|
public:
|
|
float toastDurationSeconds = 4.0f;
|
|
bool showHistory = false;
|
|
|
|
void notify(NotificationLevel level, const std::string& message) {
|
|
notifyAt(level, message, ImGui::GetTime(), nullptr);
|
|
}
|
|
|
|
void notify(NotificationLevel level, const std::string& message, const NotificationTarget& target) {
|
|
notifyAt(level, message, ImGui::GetTime(), &target);
|
|
}
|
|
|
|
void notifyAt(NotificationLevel level, const std::string& message, double nowSeconds) {
|
|
notifyAt(level, message, nowSeconds, nullptr);
|
|
}
|
|
|
|
void notifyAt(NotificationLevel level,
|
|
const std::string& message,
|
|
double nowSeconds,
|
|
const NotificationTarget* target) {
|
|
Notification n;
|
|
n.id = nextId++;
|
|
n.level = level;
|
|
n.message = message;
|
|
n.timestamp = nowSeconds;
|
|
n.durationSeconds = toastDurationSeconds;
|
|
if (target) {
|
|
n.hasTarget = true;
|
|
n.target = *target;
|
|
}
|
|
history.push_back(std::move(n));
|
|
}
|
|
|
|
void dismiss(int id) {
|
|
for (auto& n : history) {
|
|
if (n.id == id) {
|
|
n.dismissed = true;
|
|
n.read = true;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void markAllRead() {
|
|
for (auto& n : history) {
|
|
n.read = true;
|
|
}
|
|
}
|
|
|
|
int unreadCount() const {
|
|
int count = 0;
|
|
for (const auto& n : history) {
|
|
if (!n.read) ++count;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
const std::vector<Notification>& getHistory() const {
|
|
return history;
|
|
}
|
|
|
|
const Notification* latest() const {
|
|
if (history.empty()) return nullptr;
|
|
return &history.back();
|
|
}
|
|
|
|
void renderToasts(const std::function<void(const Notification&)>& onNavigate = {},
|
|
bool reduceMotion = false) {
|
|
const double now = ImGui::GetTime();
|
|
const ImGuiViewport* viewport = ImGui::GetMainViewport();
|
|
const ImVec2 base(viewport->WorkPos.x + viewport->WorkSize.x,
|
|
viewport->WorkPos.y + viewport->WorkSize.y);
|
|
const float padding = 12.0f;
|
|
const float toastWidth = 360.0f;
|
|
const float fadeIn = 0.18f;
|
|
const float fadeOut = 0.25f;
|
|
const float slidePixels = 28.0f;
|
|
float y = base.y - padding;
|
|
|
|
for (int i = (int)history.size() - 1; i >= 0; --i) {
|
|
auto& n = history[i];
|
|
if (n.dismissed) continue;
|
|
if ((now - n.timestamp) > n.durationSeconds) continue;
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration |
|
|
ImGuiWindowFlags_AlwaysAutoResize |
|
|
ImGuiWindowFlags_NoFocusOnAppearing |
|
|
ImGuiWindowFlags_NoNav |
|
|
ImGuiWindowFlags_NoSavedSettings;
|
|
|
|
float age = (float)(now - n.timestamp);
|
|
float alpha = 1.0f;
|
|
float slide = 0.0f;
|
|
if (!reduceMotion) {
|
|
float inT = AnimationUtils::clamp01(age / fadeIn);
|
|
float outT = AnimationUtils::clamp01((age - (n.durationSeconds - fadeOut)) / fadeOut);
|
|
alpha = AnimationUtils::easeOutCubic(inT);
|
|
if (age > n.durationSeconds - fadeOut) {
|
|
alpha *= (1.0f - AnimationUtils::easeOutCubic(outT));
|
|
}
|
|
slide = (1.0f - AnimationUtils::easeOutCubic(inT)) * slidePixels;
|
|
}
|
|
ImGui::SetNextWindowPos(ImVec2(base.x - padding + slide, y),
|
|
ImGuiCond_Always,
|
|
ImVec2(1.0f, 1.0f));
|
|
ImGui::SetNextWindowSize(ImVec2(toastWidth, 0.0f), ImGuiCond_Always);
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(12, 10));
|
|
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * alpha);
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.08f, 0.08f, 0.08f, 0.92f));
|
|
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.22f, 0.22f, 0.22f, 0.9f));
|
|
ImGui::Begin(("##toast_" + std::to_string(n.id)).c_str(), nullptr, flags);
|
|
|
|
ImVec4 color = levelColor(n.level);
|
|
ImGui::TextColored(color, "%s", levelLabel(n.level));
|
|
ImGui::SameLine(0.0f, 8.0f);
|
|
ImGui::TextWrapped("%s", n.message.c_str());
|
|
if (n.hasTarget) {
|
|
std::string loc = n.target.path;
|
|
if (n.target.line >= 0) {
|
|
loc += ":" + std::to_string(n.target.line + 1);
|
|
if (n.target.col >= 0) loc += ":" + std::to_string(n.target.col + 1);
|
|
}
|
|
ImGui::TextDisabled("%s", loc.c_str());
|
|
}
|
|
|
|
bool clicked = ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup) &&
|
|
ImGui::IsMouseClicked(0);
|
|
ImVec2 size = ImGui::GetWindowSize();
|
|
ImGui::End();
|
|
ImGui::PopStyleColor(2);
|
|
ImGui::PopStyleVar(2);
|
|
|
|
if (clicked) {
|
|
n.read = true;
|
|
n.dismissed = true;
|
|
if (n.hasTarget && onNavigate) {
|
|
onNavigate(n);
|
|
}
|
|
}
|
|
|
|
y -= size.y + 8.0f;
|
|
}
|
|
}
|
|
|
|
void renderHistoryWindow(const std::function<void(const Notification&)>& onNavigate = {}) {
|
|
if (!showHistory) {
|
|
historyWasOpen = false;
|
|
return;
|
|
}
|
|
|
|
if (!historyWasOpen) {
|
|
markAllRead();
|
|
historyWasOpen = true;
|
|
}
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(520, 360), ImGuiCond_FirstUseEver);
|
|
if (!ImGui::Begin("Notifications", &showHistory)) {
|
|
ImGui::End();
|
|
return;
|
|
}
|
|
|
|
if (history.empty()) {
|
|
ImGui::TextDisabled("(no notifications yet)");
|
|
ImGui::End();
|
|
return;
|
|
}
|
|
|
|
ImGui::BeginChild("##notificationHistory", ImVec2(0, 0), false);
|
|
for (const auto& n : history) {
|
|
ImGui::PushID(n.id);
|
|
ImVec4 color = levelColor(n.level);
|
|
ImGui::TextColored(color, "%s", levelLabel(n.level));
|
|
ImGui::SameLine();
|
|
if (ImGui::Selectable(n.message.c_str(), false)) {
|
|
if (n.hasTarget && onNavigate) {
|
|
onNavigate(n);
|
|
}
|
|
}
|
|
if (n.hasTarget) {
|
|
std::string loc = n.target.path;
|
|
if (n.target.line >= 0) {
|
|
loc += ":" + std::to_string(n.target.line + 1);
|
|
if (n.target.col >= 0) loc += ":" + std::to_string(n.target.col + 1);
|
|
}
|
|
ImGui::TextDisabled("%s", loc.c_str());
|
|
}
|
|
ImGui::Separator();
|
|
ImGui::PopID();
|
|
}
|
|
ImGui::EndChild();
|
|
ImGui::End();
|
|
}
|
|
|
|
private:
|
|
int nextId = 1;
|
|
bool historyWasOpen = false;
|
|
std::vector<Notification> history;
|
|
|
|
static const char* levelLabel(NotificationLevel level) {
|
|
switch (level) {
|
|
case NotificationLevel::Success: return "Success";
|
|
case NotificationLevel::Warning: return "Warning";
|
|
case NotificationLevel::Error: return "Error";
|
|
case NotificationLevel::Info:
|
|
default:
|
|
return "Info";
|
|
}
|
|
}
|
|
|
|
static ImVec4 levelColor(NotificationLevel level) {
|
|
switch (level) {
|
|
case NotificationLevel::Success: return ImVec4(0.25f, 0.78f, 0.35f, 1.0f);
|
|
case NotificationLevel::Warning: return ImVec4(0.95f, 0.72f, 0.28f, 1.0f);
|
|
case NotificationLevel::Error: return ImVec4(0.90f, 0.35f, 0.35f, 1.0f);
|
|
case NotificationLevel::Info:
|
|
default:
|
|
return ImVec4(0.40f, 0.70f, 0.95f, 1.0f);
|
|
}
|
|
}
|
|
};
|