/** * @file ImGuiNotify.hpp * @brief A header-only library for creating toast notifications with ImGui. * * Based on imgui-notify by patrickcjk * https://github.com/patrickcjk/imgui-notify * * @version 0.0.3 by TyomaVader * @date 07.07.2024 */ #ifndef IMGUI_NOTIFY #define IMGUI_NOTIFY #pragma once #include // Vector for storing notifications list #include #include // For the notifications timed dissmiss #include // For storing the code, which executest on the button click in the notification #include "imgui.h" #include "imgui_internal.h" #include "extras/IconsFontAwesome6.h" /** * CONFIGURATION SECTION Start */ #define NOTIFY_MAX_MSG_LENGTH 4096 // Max message content length #define NOTIFY_PADDING_X 20.f // Bottom-left X padding #define NOTIFY_PADDING_Y 20.f // Bottom-left Y padding #define NOTIFY_PADDING_MESSAGE_Y 10.f // Padding Y between each message #define NOTIFY_FADE_IN_OUT_TIME 150 // Fade in and out duration #define NOTIFY_DEFAULT_DISMISS 3000 // Auto dismiss after X ms (default, applied only of no data provided in constructors) #define NOTIFY_OPACITY 0.8f // 0-1 Toast opacity #define NOTIFY_USE_SEPARATOR false // If true, a separator will be rendered between the title and the content #define NOTIFY_USE_DISMISS_BUTTON true // If true, a dismiss button will be rendered in the top right corner of the toast #define NOTIFY_RENDER_LIMIT 5 // Max number of toasts rendered at the same time. Set to 0 for unlimited // Warning: Requires ImGui docking with multi-viewport enabled #ifndef NOTIFY_RENDER_OUTSIDE_MAIN_WINDOW #define NOTIFY_RENDER_OUTSIDE_MAIN_WINDOW true // If true, the notifications will be rendered in the corner of the monitor, otherwise in the corner of the main window #endif /** * CONFIGURATION SECTION End */ static const ImGuiWindowFlags NOTIFY_DEFAULT_TOAST_FLAGS = ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoFocusOnAppearing; #define NOTIFY_NULL_OR_EMPTY(str) (!str || !strlen(str)) #define NOTIFY_FORMAT(fn, format, ...) \ if (format) \ { \ va_list args; \ va_start(args, format); \ fn(format, args, ##__VA_ARGS__); \ va_end(args); \ } enum class ImGuiToastType : uint8_t { None, Success, Warning, Error, Info, COUNT }; enum class ImGuiToastPhase : uint8_t { FadeIn, Wait, FadeOut, Expired, COUNT }; enum class ImGuiToastPos : uint8_t { TopLeft, TopCenter, TopRight, BottomLeft, BottomCenter, BottomRight, Center, COUNT }; /** * @brief A class for creating toast notifications with ImGui. */ class ImGuiToast { private: ImGuiWindowFlags flags = NOTIFY_DEFAULT_TOAST_FLAGS; ImGuiToastType type = ImGuiToastType::None; char title[NOTIFY_MAX_MSG_LENGTH]; char content[NOTIFY_MAX_MSG_LENGTH]; int dismissTime = NOTIFY_DEFAULT_DISMISS; std::chrono::system_clock::time_point creationTime = std::chrono::system_clock::now(); std::function onButtonPress = nullptr; // A lambda variable, which will be executed when button in notification is pressed char buttonLabel[NOTIFY_MAX_MSG_LENGTH]; private: // Setters inline void setTitle(const char *format, va_list args) { vsnprintf(this->title, sizeof(this->title), format, args); } inline void setContent(const char *format, va_list args) { vsnprintf(this->content, sizeof(this->content), format, args); } inline void setButtonLabel(const char *format, va_list args) { vsnprintf(this->buttonLabel, sizeof(this->buttonLabel), format, args); } public: /** * @brief Set the title of the toast notification. * * @param format The format string for the title. * @param ... The arguments for the format string. */ inline void setTitle(const char *format, ...) { NOTIFY_FORMAT(this->setTitle, format); } /** * @brief Set the content of the toast notification. * * @param format The format string for the content. * @param ... The arguments for the format string. */ inline void setContent(const char *format, ...) { NOTIFY_FORMAT(this->setContent, format); } /** * @brief Set the type of the toast notification. * * @param type The type of the toast notification. */ inline void setType(const ImGuiToastType &typ) { IM_ASSERT(typ < ImGuiToastType::COUNT); this->type = typ; }; /** * @brief Set the ImGui window flags for the notification. * * @param flags ImGui window flags to set. */ inline void setWindowFlags(const ImGuiWindowFlags &flgs) { this->flags = flgs; } /** * @brief Set the function to run on the button click in the notification. * * @param onButtonPress std::fuction or lambda expression, which contains the code for execution. */ inline void setOnButtonPress(const std::function &onBtnPress) { this->onButtonPress = onBtnPress; } /** * @brief Set the label for the button in the notification. * * @param format The format string for the label. * @param ... The arguments for the format string. */ inline void setButtonLabel(const char *format, ...) { NOTIFY_FORMAT(this->setButtonLabel, format); } public: // Getters /** * @brief Get the title of the toast notification. * * @return const char* The title of the toast notification. */ inline const char *getTitle() { return this->title; }; /** * @brief Get the default title of the toast notification based on its type. * * @return const char* The default title of the toast notification. */ inline const char *getDefaultTitle() { if (!strlen(this->title)) { switch (this->type) { case ImGuiToastType::None: return nullptr; case ImGuiToastType::Success: return "Success"; case ImGuiToastType::Warning: return "Warning"; case ImGuiToastType::Error: return "Error"; case ImGuiToastType::Info: return "Info"; default: return nullptr; } } return this->title; }; /** * @brief Get the type of the toast notification. * * @return ImGuiToastType The type of the toast notification. */ inline ImGuiToastType getType() { return this->type; }; /** * @brief Get the color of the toast notification based on its type. * * @return ImVec4 The color of the toast notification. */ inline ImVec4 getColor() { switch (this->type) { case ImGuiToastType::None: return {255, 255, 255, 255}; // White case ImGuiToastType::Success: return {0, 255, 0, 255}; // Green case ImGuiToastType::Warning: return {255, 255, 0, 255}; // Yellow case ImGuiToastType::Error: return {255, 0, 0, 255}; // Error case ImGuiToastType::Info: return {0, 157, 255, 255}; // Blue default: return {255, 255, 255, 255}; // White } } /** * @brief Get the icon of the toast notification based on its type. * * @return const char* The icon of the toast notification. */ inline const char *getIcon() { switch (this->type) { case ImGuiToastType::None: return nullptr; case ImGuiToastType::Success: return ICON_FA_CIRCLE_CHECK; // Font Awesome 6 case ImGuiToastType::Warning: return ICON_FA_TRIANGLE_EXCLAMATION; // Font Awesome 6 case ImGuiToastType::Error: return ICON_FA_CIRCLE_EXCLAMATION; // Font Awesome 6 case ImGuiToastType::Info: return ICON_FA_CIRCLE_INFO; // Font Awesome 6 default: return nullptr; } } /** * @brief Get the content of the toast notification. * * @return char* The content of the toast notification. */ inline char *getContent() { return this->content; }; /** * @brief Get the elapsed time in milliseconds since the creation of the object. * * @return int64_t The elapsed time in milliseconds. * @throws An exception with the message "Unsupported platform" if the platform is not supported. */ inline std::chrono::nanoseconds getElapsedTime() { return std::chrono::system_clock::now() - this->creationTime; } /** * @brief Get the current phase of the toast notification based on the elapsed time since its creation. * * @return ImGuiToastPhase The current phase of the toast notification. * - ImGuiToastPhase::FadeIn: The notification is fading in. * - ImGuiToastPhase::Wait: The notification is waiting to be dismissed. * - ImGuiToastPhase::FadeOut: The notification is fading out. * - ImGuiToastPhase::Expired: The notification has expired and should be removed. */ inline ImGuiToastPhase getPhase() { const int64_t elapsed = std::chrono::duration_cast(getElapsedTime()).count(); if (elapsed > NOTIFY_FADE_IN_OUT_TIME + this->dismissTime + NOTIFY_FADE_IN_OUT_TIME) { return ImGuiToastPhase::Expired; } else if (elapsed > NOTIFY_FADE_IN_OUT_TIME + this->dismissTime) { return ImGuiToastPhase::FadeOut; } else if (elapsed > NOTIFY_FADE_IN_OUT_TIME) { return ImGuiToastPhase::Wait; } else { return ImGuiToastPhase::FadeIn; } } /** * Returns the percentage of fade for the notification. * @return The percentage of fade for the notification. */ inline float getFadePercent() { const ImGuiToastPhase phase = getPhase(); const int64_t elapsed = std::chrono::duration_cast(getElapsedTime()).count(); if (phase == ImGuiToastPhase::FadeIn) { return ((float)elapsed / (float)NOTIFY_FADE_IN_OUT_TIME) * NOTIFY_OPACITY; } else if (phase == ImGuiToastPhase::FadeOut) { return (1.f - (((float)elapsed - (float)NOTIFY_FADE_IN_OUT_TIME - (float)this->dismissTime) / (float)NOTIFY_FADE_IN_OUT_TIME)) * NOTIFY_OPACITY; } return 1.f * NOTIFY_OPACITY; } /** * @return ImGui window flags for the notification. */ inline ImGuiWindowFlags getWindowFlags() { return this->flags; } /** * @return The function, which is executed on the button click in the notification. */ inline std::function getOnButtonPress() { return this->onButtonPress; } /** * @return The label on the button in notification. */ inline const char *getButtonLabel() { return this->buttonLabel; } public: // Constructors /** * @brief Creates a new ImGuiToast object with the specified type and dismiss time. * * @param type The type of the toast. * @param dismissTime The time in milliseconds after which the toast should be dismissed. Default is NOTIFY_DEFAULT_DISMISS. */ ImGuiToast(ImGuiToastType type, int dismissTime = NOTIFY_DEFAULT_DISMISS) { IM_ASSERT(type < ImGuiToastType::COUNT); this->type = type; this->dismissTime = dismissTime; this->creationTime = std::chrono::system_clock::now(); memset(this->title, 0, sizeof(this->title)); memset(this->content, 0, sizeof(this->content)); } /** * @brief Constructor for creating an ImGuiToast object with a specified type and message format. * * @param type The type of the toast message. * @param format The format string for the message. * @param ... The variable arguments to be formatted according to the format string. */ ImGuiToast(ImGuiToastType type, const char *format, ...) : ImGuiToast(type) { NOTIFY_FORMAT(this->setContent, format); } /** * @brief Constructor for creating a new ImGuiToast object with a specified type, dismiss time, and content format. * * @param type The type of the toast message. * @param dismissTime The time in milliseconds before the toast message is dismissed. * @param format The format string for the content of the toast message. * @param ... The variable arguments to be formatted according to the format string. */ ImGuiToast(ImGuiToastType type, int dismissTime, const char *format, ...) : ImGuiToast(type, dismissTime) { NOTIFY_FORMAT(this->setContent, format); } /** * @brief Constructor for creating a new ImGuiToast object with a specified type, dismiss time, title format, content format and a button. * * @param type The type of the toast message. * @param dismissTime The time in milliseconds before the toast message is dismissed. * @param buttonLabel The label for the button. * @param onButtonPress The lambda function to be executed when the button is pressed. * @param format The format string for the content of the toast message. * @param ... The variable arguments to be formatted according to the format string. */ ImGuiToast(ImGuiToastType type, int dismissTime, const char *buttonLabel, const std::function &onButtonPress, const char *format, ...) : ImGuiToast(type, dismissTime) { NOTIFY_FORMAT(this->setContent, format); this->onButtonPress = onButtonPress; this->setButtonLabel(buttonLabel); } }; namespace ImGui { inline std::vector notifications; /** * Inserts a new notification into the notification queue. * @param toast The notification to be inserted. */ inline void InsertNotification(const ImGuiToast &toast) { notifications.push_back(toast); } /** * @brief Removes a notification from the list of notifications. * * @param index The index of the notification to remove. */ inline void RemoveNotification(int index) { notifications.erase(notifications.begin() + index); } /** * Renders all notifications in the notifications vector. * Each notification is rendered as a toast window with a title, content and an optional icon. * If a notification is expired, it is removed from the vector. */ inline void RenderNotifications() { const ImVec2 mainWindowSize = GetMainViewport()->Size; float height = 0.f; for (size_t i = 0; i < notifications.size();) { ImGuiToast *currentToast = ¬ifications[i]; // Remove toast if expired (erase while iterating safely) if (currentToast->getPhase() == ImGuiToastPhase::Expired) { RemoveNotification(static_cast(i)); // Do not increment i: elements shifted left continue; } #if NOTIFY_RENDER_LIMIT > 0 if (i > NOTIFY_RENDER_LIMIT) { continue; } #endif // Get icon, title and other data const char *icon = currentToast->getIcon(); const char *title = currentToast->getTitle(); const char *content = currentToast->getContent(); const char *defaultTitle = currentToast->getDefaultTitle(); const float opacity = currentToast->getFadePercent(); // Get opacity based of the current phase // Window rendering ImVec4 textColor = currentToast->getColor(); textColor.w = opacity; // Generate new unique name for this toast char windowName[50]; #ifdef _WIN32 sprintf_s(windowName, "##TOAST%d", (int)i); #elif defined(__linux__) || defined(__EMSCRIPTEN__) std::sprintf(windowName, "##TOAST%d", (int)i); #elif defined(__APPLE__) std::snprintf(windowName, 50, "##TOAST%d", (int)i); #else throw "Unsupported platform"; #endif // PushStyleColor(ImGuiCol_Text, textColor); SetNextWindowBgAlpha(opacity); #if NOTIFY_RENDER_OUTSIDE_MAIN_WINDOW // Try to obtain the monitor for the main viewport safely. Some backends may not populate PlatformIO monitors. ImGuiViewport *main_vp = GetMainViewport(); int mainMonitorId = -1; if (main_vp) { // ImGuiViewportP::PlatformMonitor is signed short in some versions; read safely mainMonitorId = static_cast(reinterpret_cast(main_vp)->PlatformMonitor); } ImGuiPlatformIO &platformIO = GetPlatformIO(); if (mainMonitorId >= 0 && mainMonitorId < static_cast(platformIO.Monitors.size())) { ImGuiPlatformMonitor &monitor = platformIO.Monitors[mainMonitorId]; // Set notification window position to bottom right corner of the monitor SetNextWindowPos(ImVec2(monitor.WorkPos.x + monitor.WorkSize.x - NOTIFY_PADDING_X, monitor.WorkPos.y + monitor.WorkSize.y - NOTIFY_PADDING_Y - height), ImGuiCond_Always, ImVec2(1.0f, 1.0f)); } else { // Fallback to main viewport position if monitor info not available ImVec2 mainWindowPos = main_vp ? main_vp->Pos : ImVec2(0, 0); SetNextWindowPos(ImVec2(mainWindowPos.x + mainWindowSize.x - NOTIFY_PADDING_X, mainWindowPos.y + mainWindowSize.y - NOTIFY_PADDING_Y - height), ImGuiCond_Always, ImVec2(1.0f, 1.0f)); } #else // Set notification window position to bottom right corner of the main window, considering the main window size and location in relation to the display ImVec2 mainWindowPos = GetMainViewport()->Pos; SetNextWindowPos(ImVec2(mainWindowPos.x + mainWindowSize.x - NOTIFY_PADDING_X, mainWindowPos.y + mainWindowSize.y - NOTIFY_PADDING_Y - height), ImGuiCond_Always, ImVec2(1.0f, 1.0f)); #endif // Set notification window flags if constexpr (!NOTIFY_USE_DISMISS_BUTTON && currentToast->getOnButtonPress() == nullptr) { currentToast->setWindowFlags(NOTIFY_DEFAULT_TOAST_FLAGS | ImGuiWindowFlags_NoInputs); } Begin(windowName, nullptr, currentToast->getWindowFlags()); // Render over all other windows BringWindowToDisplayFront(GetCurrentWindow()); // Here we render the toast content { PushTextWrapPos(mainWindowSize.x / 3.f); // We want to support multi-line text, this will wrap the text after 1/3 of the screen width bool wasTitleRendered = false; // If an icon is set if (!NOTIFY_NULL_OR_EMPTY(icon)) { // Text(icon); // Render icon text TextColored(textColor, "%s", icon); wasTitleRendered = true; } // If a title is set if (!NOTIFY_NULL_OR_EMPTY(title)) { // If a title and an icon is set, we want to render on same line if (!NOTIFY_NULL_OR_EMPTY(icon)) SameLine(); Text("%s", title); // Render title text wasTitleRendered = true; } else if (!NOTIFY_NULL_OR_EMPTY(defaultTitle)) { if (!NOTIFY_NULL_OR_EMPTY(icon)) SameLine(); Text("%s", defaultTitle); // Render default title text (ImGuiToastType_Success -> "Success", etc...) wasTitleRendered = true; } // If a dismiss button is enabled if (NOTIFY_USE_DISMISS_BUTTON) { // If a title or content is set, we want to render the button on the same line if (wasTitleRendered || !NOTIFY_NULL_OR_EMPTY(content)) { SameLine(); } // Render the dismiss button on the top right corner // NEEDS TO BE REWORKED float scale = 0.8f; if (CalcTextSize(content).x > GetContentRegionAvail().x) { scale = 0.8f; } SetCursorPosX(GetCursorPosX() + (GetWindowSize().x - GetCursorPosX()) * scale); // If the button is pressed, we want to remove the notification if (Button(ICON_FA_XMARK)) { RemoveNotification(static_cast(i)); } } // In case ANYTHING was rendered in the top, we want to add a small padding so the text (or icon) looks centered vertically if (wasTitleRendered && !NOTIFY_NULL_OR_EMPTY(content)) { SetCursorPosY(GetCursorPosY() + 5.f); // Must be a better way to do this!!!! } // If a content is set if (!NOTIFY_NULL_OR_EMPTY(content)) { if (wasTitleRendered) { if constexpr (NOTIFY_USE_SEPARATOR) { Separator(); } } Text("%s", content); // Render content text } // If a button is set if (currentToast->getOnButtonPress() != nullptr) { // If the button is pressed, we want to execute the lambda function if (Button(currentToast->getButtonLabel())) { currentToast->getOnButtonPress()(); } } PopTextWrapPos(); } // Save height for next toasts height += GetWindowHeight() + NOTIFY_PADDING_MESSAGE_Y; // End End(); // advance index only when we didn't erase ++i; } } } #endif