RunicVTT Open Source Virtual Tabletop for TTRPG using P2P
Loading...
Searching...
No Matches
ImGuiToaster.h
Go to the documentation of this file.
1#pragma once
2#include <imgui.h>
3#include <deque>
4#include <mutex>
5#include <string>
6#include <vector>
7#include <chrono>
8#include <algorithm>
9#include <memory>
10#include <atomic>
11
13{
14public:
15 enum class Level
16 {
17 Info,
18 Good,
19 Warning,
20 Error
21 };
22
23 struct Config
24 {
25 // Visuals
26 float bgOpacity = 0.80f; // final window opacity (0..1). We'll apply this to level color.
27 float rounding = 6.0f;
28 ImVec2 windowPadding = ImVec2(10.f, 8.f);
29 float verticalSpacing = 36.f;
30 float edgePadding = 10.f;
31
32 // Sizing
33 bool autoResize = true; // auto-fit to content (clamped by constraints)
34 ImVec2 minSize = ImVec2(360.f, 0.f); // good base width for long phrases
35 ImVec2 maxSize = ImVec2(0.f, 0.f); // 0,0 => will auto-cap to 90% of work area
36 ImVec2 fixedSize = ImVec2(0.f, 0.f); // if >0 for x/y, forces that dimension
37 float maxWidth = 480.f; // text wrap width (0 = use content region avail)
38 bool wrapText = true;
39
40 // Positioning
41 ImVec2 anchorPivot = ImVec2(1.f, 0.f); // top-right
42 enum class Corner
43 {
44 TopLeft,
48 };
50
51 // Behavior
52 size_t maxToasts = 8;
53 bool clickThrough = false; // let clicks pass through
54 bool focusOnAppear = false;
56
57 // Colors per level (used as WINDOW background color; text is always white)
58 ImVec4 colorInfo = ImVec4(0.20f, 0.45f, 0.85f, 1.0f); // blue-ish
59 ImVec4 colorGood = ImVec4(0.16f, 0.65f, 0.22f, 1.0f); // green
60 ImVec4 colorWarning = ImVec4(0.90f, 0.70f, 0.10f, 1.0f); // yellow
61 ImVec4 colorError = ImVec4(0.85f, 0.25f, 0.25f, 1.0f); // red
62 ImVec4 textColor = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); // always white for contrast
63 bool showBorder = false;
64 ImVec4 borderColor = ImVec4(0, 0, 0, 0.35f); // optional subtle border
65 float borderSize = 1.0f;
66 };
67
68 ImGuiToaster() = default;
69 explicit ImGuiToaster(const Config& cfg) :
70 cfg_(cfg) {}
71
72 void Push(Level lvl, const std::string& msg, float durationSec = 4.0f)
73 {
74 using clock = std::chrono::steady_clock;
75 Toast t;
76 t.id = next_id_.fetch_add(1, std::memory_order_relaxed);
77 t.message = msg;
78 t.level = lvl;
79 t.expiresAt = clock::now() + std::chrono::duration_cast<clock::duration>(std::chrono::duration<float>(durationSec));
80 {
81 std::scoped_lock lk(mtx_);
82 toasts_.push_back(std::move(t));
83 if (toasts_.size() > cfg_.maxToasts)
84 toasts_.pop_front();
85 }
86 }
87 void Info(const std::string& msg, float sec = 5.f)
88 {
89 Push(Level::Info, msg, sec);
90 }
91 void Good(const std::string& msg, float sec = 5.f)
92 {
93 Push(Level::Good, msg, sec);
94 }
95 void Warn(const std::string& msg, float sec = 5.f)
96 {
97 Push(Level::Warning, msg, sec);
98 }
99 void Error(const std::string& msg, float sec = 5.f)
100 {
101 Push(Level::Error, msg, sec);
102 }
103
104 void Clear()
105 {
106 std::scoped_lock lk(mtx_);
107 toasts_.clear();
108 }
109
110 // Call once per frame (after ImGui::NewFrame, before Render)
111 void Render()
112 {
113 using clock = std::chrono::steady_clock;
114
115 // Copy & prune under lock
116 std::vector<Toast> local;
117 {
118 std::scoped_lock lk(mtx_);
119 const auto now = clock::now();
120 while (!toasts_.empty() && toasts_.front().expiresAt <= now)
121 toasts_.pop_front();
122 local.assign(toasts_.begin(), toasts_.end());
123 }
124 if (local.empty())
125 return;
126
127 ImGuiViewport* vp = ImGui::GetMainViewport();
128 const float PAD = cfg_.edgePadding;
129
130 ImVec2 basePos;
131 switch (cfg_.corner)
132 {
134 basePos = ImVec2(vp->WorkPos.x + PAD, vp->WorkPos.y + PAD);
135 break;
137 basePos = ImVec2(vp->WorkPos.x + vp->WorkSize.x - PAD, vp->WorkPos.y + PAD);
138 break;
140 basePos = ImVec2(vp->WorkPos.x + PAD, vp->WorkPos.y + vp->WorkSize.y - PAD);
141 break;
143 basePos = ImVec2(vp->WorkPos.x + vp->WorkSize.x - PAD, vp->WorkPos.y + vp->WorkSize.y - PAD);
144 break;
145 }
146
147 ImVec2 anchor = cfg_.anchorPivot;
151
152 ImVec2 pos = basePos;
153 int idx = 0;
154
155 for (const auto& t : local)
156 {
157 const ImVec4 lvlCol = ColorForLevel_(t.level);
158 ImVec4 bg = lvlCol;
159 bg.w = cfg_.bgOpacity; // apply opacity to window bg
160 bool delete_this_toast = false;
161
162 ImGui::SetNextWindowViewport(vp->ID);
163 ImGui::SetNextWindowPos(pos, ImGuiCond_Always, anchor);
164
165 // Sizing setup
166 if (cfg_.fixedSize.x > 0.f || cfg_.fixedSize.y > 0.f)
167 {
168 ImGui::SetNextWindowSize(ImVec2(
169 cfg_.fixedSize.x > 0.f ? cfg_.fixedSize.x : 0.f,
170 cfg_.fixedSize.y > 0.f ? cfg_.fixedSize.y : 0.f),
171 ImGuiCond_Always);
172 }
173 else
174 {
175 ImVec2 minC = cfg_.minSize;
176 ImVec2 maxC = cfg_.maxSize;
177 if (maxC.x <= 0.f || maxC.y <= 0.f)
178 {
179 maxC.x = (maxC.x <= 0.f) ? (vp->WorkSize.x * 0.9f) : maxC.x;
180 maxC.y = (maxC.y <= 0.f) ? (vp->WorkSize.y * 0.9f) : maxC.y;
181 }
182 ImGui::SetNextWindowSizeConstraints(minC, maxC);
183 if (!cfg_.autoResize)
184 {
185 ImGui::SetNextWindowSize(minC, ImGuiCond_Always);
186 }
187 }
188
189 ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration |
190 ImGuiWindowFlags_NoSavedSettings |
191 ImGuiWindowFlags_NoNav;
192 if (!cfg_.focusOnAppear)
193 flags |= ImGuiWindowFlags_NoFocusOnAppearing;
195 flags |= ImGuiWindowFlags_NoInputs;
196 if (cfg_.autoResize && !(cfg_.fixedSize.x > 0.f || cfg_.fixedSize.y > 0.f))
197 flags |= ImGuiWindowFlags_AlwaysAutoResize;
198
199 // Style: background colored by level, text always white, optional border
200 ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, cfg_.rounding);
201 ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, cfg_.windowPadding);
202
203 ImGui::PushStyleColor(ImGuiCol_WindowBg, bg);
204 if (cfg_.showBorder)
205 {
206 ImGui::PushStyleColor(ImGuiCol_Border, cfg_.borderColor);
207 ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, cfg_.borderSize);
208 }
209
210 std::string name = "##toast-" + std::to_string(idx++);
211 if (ImGui::Begin(name.c_str(), nullptr, flags))
212 {
214 ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem) &&
215 ImGui::IsMouseClicked(ImGuiMouseButton_Left))
216 {
217 delete_this_toast = true;
218 }
219
220 // Text wrapping
221 float wrapAt = 0.f;
222 if (cfg_.wrapText)
223 {
224 wrapAt = (cfg_.maxWidth > 0.f) ? cfg_.maxWidth : ImGui::GetContentRegionAvail().x;
225 ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + wrapAt);
226 }
227
228 ImGui::PushStyleColor(ImGuiCol_Text, cfg_.textColor);
229 ImGui::TextUnformatted(t.message.c_str());
230 ImGui::PopStyleColor();
231
232 if (cfg_.wrapText)
233 {
234 ImGui::PopTextWrapPos();
235 }
236
237 /*if (cfg_.killOnClickAnywhere) {
238 // Cover the content region with an invisible button.
239 // It won't change layout because we restore the cursor after.
240 ImVec2 crMin = ImGui::GetWindowContentRegionMin();
241 ImVec2 crMax = ImGui::GetWindowContentRegionMax();
242 ImVec2 old = ImGui::GetCursorPos();
243
244 ImGui::SetCursorPos(crMin);
245 ImVec2 size(crMax.x - crMin.x, crMax.y - crMin.y);
246 ImGui::InvisibleButton("##toast_kill", size);
247 if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) {
248 delete_this_toast = true;
249 }
250 ImGui::SetCursorPos(old);
251 }*/
252 }
253 ImGui::End();
254
255 if (cfg_.showBorder)
256 {
257 ImGui::PopStyleVar(); // WindowBorderSize
258 ImGui::PopStyleColor(); // Border
259 }
260 ImGui::PopStyleColor(); // WindowBg
261 ImGui::PopStyleVar(2); // rounding, padding
262 if (cfg_.killOnClickAnywhere && delete_this_toast)
263 {
264 std::scoped_lock lk(mtx_);
265 auto it_real = std::find_if(toasts_.begin(), toasts_.end(),
266 [&](const Toast& tt)
267 { return tt.id == t.id; });
268 if (it_real != toasts_.end())
269 toasts_.erase(it_real);
270 }
271 pos.y += y_step;
272 }
273 }
274
275 void SetConfig(const Config& cfg)
276 {
277 cfg_ = cfg;
278 }
279 const Config& GetConfig() const
280 {
281 return cfg_;
282 }
283
284private:
285 std::atomic<uint64_t> next_id_{1};
286 struct Toast
287 {
288 uint64_t id = 0;
289 std::string message;
291 std::chrono::steady_clock::time_point expiresAt;
292 };
293
294 ImVec4 ColorForLevel_(Level lvl) const
295 {
296 switch (lvl)
297 {
298 case Level::Good:
299 return cfg_.colorGood;
300 case Level::Warning:
301 return cfg_.colorWarning;
302 case Level::Error:
303 return cfg_.colorError;
304 case Level::Info:
305 default:
306 return cfg_.colorInfo;
307 }
308 }
309
310 mutable std::mutex mtx_;
311 std::deque<Toast> toasts_;
313};
314
315/*
316USAGE
317--------------------------------------------------------
318// Somewhere in your app bootstrap:
319auto g_toaster = std::make_shared<ImGuiToaster>();
320
321// Optional: tweak look/feel
322ImGuiToaster::Config cfg;
323cfg.corner = ImGuiToaster::Config::Corner::TopRight;
324cfg.maxToasts = 8;
325g_toaster->SetConfig(cfg);
326
327// Give to your managers (NetworkManager, GameTableManager, AutosaveManager, etc.)
328networkManager->setToaster(g_toaster);
329gameTableManager->setToaster(g_toaster);
330// autosaveManager->setToaster(g_toaster); // if you want autosave messages here
331
332--------------------------------------------------------
333// In NetworkManager:
334class NetworkManager {
335public:
336 void setToaster(std::shared_ptr<ImGuiToaster> t) { toaster_ = std::move(t); }
337
338 void pushStatusToast(const std::string& msg, ImGuiToaster::Level lvl, float durationSec) {
339 if (toaster_) toaster_->Push(lvl, msg, durationSec);
340 }
341
342private:
343 std::shared_ptr<ImGuiToaster> toaster_;
344};
345----------------------------------------------------------
346// In your main GUI frame (once per frame, after NewFrame, before Render):
347if (g_toaster) g_toaster->Render();
348-----------------------------------------------------------
349bool ok = //your save routine /;
350if (ok) {
351 g_toaster->Good("GameTable Saved!!", 5.0f);
352} else {
353 g_toaster->Error("Save Failed: <reason>", 5.0f);
354}
355--------------------------------------------------------------------
356// NetworkManager.hpp
357#include <memory>
358#include "ImGuiToaster.hpp"
359
360class NetworkManager {
361public:
362 void setToaster(std::shared_ptr<ImGuiToaster> t) { toaster_ = std::move(t); }
363
364 // Unified push (replaces your old pushStatusToast)
365 void pushStatusToast(const std::string& msg, ImGuiToaster::Level lvl, float durationSec = 5.0f) {
366 if (toaster_) toaster_->Push(lvl, msg, durationSec);
367 }
368
369 // Example: call on events
370 void onConnected() { pushStatusToast("Connected", ImGuiToaster::Level::Good, 4.0f); }
371 void onDisconnected(){ pushStatusToast("Disconnected", ImGuiToaster::Level::Warning, 5.0f); }
372 void onError(const std::string& e){ pushStatusToast("Network error: " + e, ImGuiToaster::Level::Error, 6.0f); }
373
374private:
375 std::shared_ptr<ImGuiToaster> toaster_;
376};
377--------------------------------------------------------------------
378void GameTableManager::renderToasts(std::shared_ptr<ImGuiToaster> toaster) {
379 if (toaster) toaster->Render();
380}
381--------------------------------------------------------------------
382// During initialization
383auto toaster = std::make_shared<ImGuiToaster>();
384ImGuiToaster::Config cfg;
385cfg.corner = ImGuiToaster::Config::Corner::TopRight;
386cfg.maxToasts = 8;
387toaster->SetConfig(cfg);
388
389// Pass to your managers
390networkManager->setToaster(toaster);
391gameTableManager->setToaster(toaster); // if you keep a ref there
392// autosaveManager->setToaster(toaster); // optional
393
394// In your frame loop, after ImGui::NewFrame() and before ImGui::Render():
395// ... your UI
396// Render toasts ONCE, globally
397toaster->Render();
398--------------------------------------------------------------------
399// Somewhere in your input handling (per-frame)
400if (ImGui::IsKeyPressed(ImGuiKey_1)) toaster->Info ("Info: Hello!", 5.0f);
401if (ImGui::IsKeyPressed(ImGuiKey_2)) toaster->Good ("Good: Saved", 5.0f);
402if (ImGui::IsKeyPressed(ImGuiKey_3)) toaster->Warn ("Warning: Ping high", 5.0f);
403if (ImGui::IsKeyPressed(ImGuiKey_4)) toaster->Error("Error: Failed op", 5.0f);
404---------------------------------------------------------------------------
405
406*/
void Push(Level lvl, const std::string &msg, float durationSec=4.0f)
void Error(const std::string &msg, float sec=5.f)
void Warn(const std::string &msg, float sec=5.f)
std::atomic< uint64_t > next_id_
std::deque< Toast > toasts_
ImVec4 ColorForLevel_(Level lvl) const
ImGuiToaster(const Config &cfg)
const Config & GetConfig() const
void Info(const std::string &msg, float sec=5.f)
void Good(const std::string &msg, float sec=5.f)
void SetConfig(const Config &cfg)
std::mutex mtx_
ImGuiToaster()=default
Definition Message.h:28
std::chrono::steady_clock::time_point expiresAt