yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
toast_manager.h
Go to the documentation of this file.
1#ifndef YAZE_APP_EDITOR_SYSTEM_TOAST_MANAGER_H
2#define YAZE_APP_EDITOR_SYSTEM_TOAST_MANAGER_H
3
4#include <algorithm>
5#include <chrono>
6#include <deque>
7#include <functional>
8#include <string>
9#include <utility>
10
11// Must define before including imgui.h
12#ifndef IMGUI_DEFINE_MATH_OPERATORS
13#define IMGUI_DEFINE_MATH_OPERATORS
14#endif
15
16#include "app/gui/core/style.h"
19#include "imgui/imgui.h"
20
21namespace yaze {
22namespace editor {
23
25
26struct Toast {
27 std::string message;
29 float ttl_seconds = 3.0f;
31 std::string action_label;
32 std::function<void()> on_action;
33 std::chrono::steady_clock::time_point last_tick;
34};
35
40 std::string message;
42 std::chrono::system_clock::time_point timestamp;
43 bool read = false;
44};
45
47 public:
48 using Clock = std::chrono::steady_clock;
49
50 explicit ToastManager(std::function<Clock::time_point()> now = Clock::now)
51 : now_(std::move(now)) {}
52
53 static constexpr size_t kMaxHistorySize = 50;
54 // Suppress duplicate (same message+type) toasts within this window.
55 static constexpr float kDedupCooldownSeconds = 1.0f;
56
57 void Show(const std::string& message, ToastType type = ToastType::kInfo,
58 float ttl_seconds = 3.0f, std::string action_label = {},
59 std::function<void()> on_action = nullptr) {
60 // Dedup: skip if same message+type was shown within the cooldown window.
61 const auto now = now_();
62 if (message == last_shown_message_ && type == last_shown_type_) {
63 auto elapsed = std::chrono::duration<float>(now - last_shown_time_);
64 if (elapsed.count() < kDedupCooldownSeconds) {
66 return;
67 }
68 }
69 last_shown_message_ = message;
70 last_shown_type_ = type;
71 last_shown_time_ = now;
72
73 toasts_.push_back({message, type, ttl_seconds, std::move(action_label),
74 std::move(on_action), now});
75
76 // Also add to notification history
77 NotificationEntry entry;
78 entry.message = message;
79 entry.type = type;
80 entry.timestamp = std::chrono::system_clock::now();
81 entry.read = false;
82
83 notification_history_.push_front(entry);
84
85 // Trim history if too large
86 while (notification_history_.size() > kMaxHistorySize) {
87 notification_history_.pop_back();
88 }
89 }
90
94
96 size_t pending_count() const { return toasts_.size(); }
97 const Toast* GetPendingToast(size_t index) const {
98 if (index >= toasts_.size()) {
99 return nullptr;
100 }
101 return &toasts_[index];
102 }
103
104 void Draw() {
105 if (toasts_.empty())
106 return;
107 ImGuiIO& io = ImGui::GetIO();
108 const auto& theme = gui::ThemeManager::Get().GetCurrentTheme();
109 const auto now = now_();
110
111 // Position toasts from the top-right, below menu bar
112 ImVec2 pos(io.DisplaySize.x - 16.f, 48.f);
113
114 // Iterate copy so we can mutate ttl while drawing ordered from newest.
115 for (auto it = toasts_.begin(); it != toasts_.end();) {
116 Toast& t = *it;
117
118 // Use theme colors for toast backgrounds
119 ImVec4 bg;
120 ImVec4 text_color;
121 switch (t.type) {
122 case ToastType::kInfo:
124 bg.w = 0.95f;
125 text_color = gui::ConvertColorToImVec4(theme.text_primary);
126 break;
128 bg = gui::ConvertColorToImVec4(theme.success);
129 bg.w = 0.95f;
130 text_color = ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
131 break;
133 bg = gui::ConvertColorToImVec4(theme.warning);
134 bg.w = 0.95f;
135 text_color = ImVec4(0.0f, 0.0f, 0.0f, 1.0f);
136 break;
138 bg = gui::ConvertColorToImVec4(theme.error);
139 bg.w = 0.95f;
140 text_color = ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
141 break;
142 }
143
144 ImGui::SetNextWindowBgAlpha(bg.w);
145 ImGui::SetNextWindowPos(pos, ImGuiCond_Always, ImVec2(1.f, 0.f));
146 ImGuiWindowFlags flags =
147 ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize |
148 ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoNav |
149 ImGuiWindowFlags_NoFocusOnAppearing;
150
151 gui::StyleColorGuard color_guard(
152 {{ImGuiCol_WindowBg, bg}, {ImGuiCol_Text, text_color}});
153 gui::StyleVarGuard var_guard(
154 {{ImGuiStyleVar_WindowRounding, 6.0f},
155 {ImGuiStyleVar_WindowPadding, ImVec2(12.0f, 8.0f)}});
156
157 // Use unique window name per toast to allow multiple
158 char window_name[32];
159 snprintf(window_name, sizeof(window_name), "##toast_%p", (void*)&t);
160
161 bool action_clicked = false;
162 bool hovered = false;
163 float toast_height = 0.0f;
164 if (ImGui::Begin(window_name, nullptr, flags)) {
165 // Constrain width so long migrant tips wrap instead of spanning the
166 // display.
167 const float max_width = std::min(360.f, io.DisplaySize.x * 0.4f);
168 ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + max_width);
169 ImGui::TextUnformatted(t.message.c_str());
170 ImGui::PopTextWrapPos();
171 if (!t.action_label.empty()) {
172 ImGui::Spacing();
173 if (ImGui::SmallButton(t.action_label.c_str())) {
174 action_clicked = true;
175 }
176 }
177 hovered = ImGui::IsWindowHovered();
178 toast_height = ImGui::GetWindowSize().y;
179 }
180 ImGui::End();
181
182 if (action_clicked) {
183 if (t.on_action) {
184 t.on_action();
185 }
186 it = toasts_.erase(it);
187 continue;
188 }
189
190 // Frame DeltaTime can include a modal dialog before this toast existed.
191 const auto elapsed = std::chrono::duration<float>(now - t.last_tick);
192 t.last_tick = now;
193 // Decrease TTL (pause while hovered so users can reach the action).
194 if (!hovered) {
195 t.ttl_seconds -= elapsed.count();
196 }
197 if (t.ttl_seconds <= 0.f) {
198 it = toasts_.erase(it);
199 } else {
200 // ItemRect is only the last text/button. Stack by the full toast
201 // window height so actionable toasts cannot overlap the next toast.
202 pos.y += std::max(toast_height, ImGui::GetFrameHeight()) + 8.f;
203 ++it;
204 }
205 }
206 }
207
208 // Notification history methods
209 size_t GetUnreadCount() const {
210 size_t count = 0;
211 for (const auto& entry : notification_history_) {
212 if (!entry.read)
213 ++count;
214 }
215 return count;
216 }
217
218 const std::deque<NotificationEntry>& GetHistory() const {
220 }
221
222 void MarkAllRead() {
223 for (auto& entry : notification_history_) {
224 entry.read = true;
225 }
226 }
227
229
230 private:
231 std::function<Clock::time_point()> now_;
232 std::deque<Toast> toasts_;
233 std::deque<NotificationEntry> notification_history_;
234
235 // Dedup state: suppress rapid-fire identical toasts.
238 std::chrono::steady_clock::time_point last_shown_time_{};
240};
241
242} // namespace editor
243} // namespace yaze
244
245#endif // YAZE_APP_EDITOR_SYSTEM_TOAST_MANAGER_H
std::deque< NotificationEntry > notification_history_
std::chrono::steady_clock Clock
static constexpr float kDedupCooldownSeconds
std::chrono::steady_clock::time_point last_shown_time_
void Show(const std::string &message, ToastType type=ToastType::kInfo, float ttl_seconds=3.0f, std::string action_label={}, std::function< void()> on_action=nullptr)
const Toast * GetPendingToast(size_t index) const
ToastManager(std::function< Clock::time_point()> now=Clock::now)
std::deque< Toast > toasts_
const std::deque< NotificationEntry > & GetHistory() const
size_t pending_count() const
Active on-screen toasts (not notification history). For tests / UI.
std::function< Clock::time_point()> now_
static constexpr size_t kMaxHistorySize
size_t dedup_suppressed_count() const
Number of toasts suppressed by dedup since last reset.
RAII guard for ImGui style colors.
Definition style_guard.h:27
RAII guard for ImGui style vars.
Definition style_guard.h:68
const Theme & GetCurrentTheme() const
static ThemeManager & Get()
ImVec4 ConvertColorToImVec4(const Color &color)
Definition color.h:140
ImVec4 GetSurfaceContainerHighVec4()
Entry in the notification history with timestamp.
std::chrono::system_clock::time_point timestamp
std::chrono::steady_clock::time_point last_tick
std::function< void()> on_action
std::string action_label
Optional button label (empty = message-only toast).