ImGui widget for displaying log messages in a scrollable text area. More...
#include <string>
#include <vector>
#include <unordered_map>
#include <mutex>
#include <cstdint>
#include <chrono>
#include <iomanip>
#include <sstream>
#include <atomic>
#include <functional>
#include "imgui.h"
#include <
helios.ext.imgui.ImGuiWidget>
Namespaces Index
| namespace | helios |
|
|
|
| namespace | ext |
|
Platform-specific extensions and backend implementations. More...
|
|
| namespace | imgui |
|
|
|
| namespace | widgets |
|
Debug and developer widgets for ImGui overlays. More...
|
|
Classes Index
| struct | LogEntry |
|
Represents a single log entry with level, scope, and message. More...
|
|
| class | LogWidget |
|
Debug widget for displaying log output in a scrollable ImGui panel. More...
|
|
Description
ImGui widget for displaying log messages in a scrollable text area.
File Listing
The file content with the documentation metadata removed is:
9#include <unordered_map>
19export module helios.ext.imgui.widgets.LogWidget;
21import helios.ext.imgui.ImGuiWidget;
79 static constexpr const char* ALL_SCOPES_KEY = "__all__";
84 static constexpr const char* NONE_SCOPES_KEY = "__none__";
91 std::unordered_map<std::string, std::vector<LogEntry>> scopeBuffers_;
96 std::size_t maxEntries_ = 1000;
101 mutable std::mutex bufferMutex_;
106 bool autoScroll_ = true;
111 bool prevAutoScroll_ = true;
116 bool scrollUpOneEntry_ = false;
121 bool scrollToBottom_ = false;
126 bool wasAtBottom_ = true;
131 std::atomic<bool> acceptNewEntries_{true};
136 std::atomic<std::size_t> skippedEntries_{0};
146 ImGuiTextFilter textFilter_;
151 int filterLevelIndex_ = 0;
156 std::vector<std::string> collectedScopes_;
161 int selectedScopeIndex_ = -1;
166 std::string activeScopeFilter_;
171 bool loggingDisabled_ = true;
179 std::function<void(const std::string&)> onScopeFilterChanged_;
186 void collectScope(const std::string& scope) {
187 for (const auto& existing : collectedScopes_) {
188 if (existing == scope) return;
190 collectedScopes_.push_back(scope);
196 void addToBuffer(std::vector<LogEntry>& buffer, LogEntry entry) {
197 buffer.push_back(std::move(entry));
198 if (maxEntries_ > 0 && buffer.size() > maxEntries_) {
199 const std::size_t overflow = buffer.size() - maxEntries_;
200 buffer.erase(buffer.begin(),
201 buffer.begin() + static_cast<std::ptrdiff_t>(overflow));
208 [[nodiscard]] const std::vector<LogEntry>& activeBuffer() const {
209 if (activeScopeFilter_.empty()) {
210 auto it = scopeBuffers_.find(ALL_SCOPES_KEY);
211 if (it != scopeBuffers_.end()) {
215 auto it = scopeBuffers_.find(activeScopeFilter_);
216 if (it != scopeBuffers_.end()) {
220 static const std::vector<LogEntry> empty;
231 [[nodiscard]] static ImVec4 colorForLevel(LogLevel level) noexcept {
237 default: return ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
248 [[nodiscard]] static const char* labelForLevel(LogLevel level) noexcept {
254 default: return "[?????]";
263 [[nodiscard]] static std::string currentTimestamp() noexcept {
264 auto now = std::chrono::system_clock::now();
265 auto time_t_now = std::chrono::system_clock::to_time_t(now);
266 auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
267 now.time_since_epoch()) % 1000;
271 localtime_s(&tm_buf, &time_t_now);
273 localtime_r(&time_t_now, &tm_buf);
275 std::ostringstream oss;
276 oss << std::put_time(&tm_buf, "%H:%M:%S")
277 << '.' << std::setfill('0') << std::setw(3) << ms.count();
290 void addLog(LogLevel level, const std::string& scope, const std::string& message) {
292 if (loggingDisabled_) {
298 if (!acceptNewEntries_.load(std::memory_order_relaxed)) {
299 skippedEntries_.fetch_add(1, std::memory_order_relaxed);
303 std::lock_guard<std::mutex> lock(bufferMutex_);
315 addToBuffer(scopeBuffers_[scope], entry);
318 addToBuffer(scopeBuffers_[ALL_SCOPES_KEY], entry);
321 scrollToBottom_ = true;
330 void debug(const std::string& scope, const std::string& message) {
340 void info(const std::string& scope, const std::string& message) {
350 void warn(const std::string& scope, const std::string& message) {
360 void error(const std::string& scope, const std::string& message) {
368 std::lock_guard<std::mutex> lock(bufferMutex_);
369 scopeBuffers_.clear();
370 skippedEntries_.store(0, std::memory_order_relaxed);
379 std::lock_guard<std::mutex> lock(bufferMutex_);
382 for (auto& [scope, buffer] : scopeBuffers_) {
383 if (maxEntries_ > 0 && buffer.size() > maxEntries_) {
384 const std::size_t overflow = buffer.size() - maxEntries_;
385 buffer.erase(buffer.begin(),
386 buffer.begin() + static_cast<std::ptrdiff_t>(overflow));
397 autoScroll_ = enabled;
406 filterLevel_ = level;
407 filterLevelIndex_ = static_cast<int>(level);
419 onScopeFilterChanged_ = std::move(callback);
428 std::lock_guard<std::mutex> lock(bufferMutex_);
429 return activeBuffer().size();
436 ImGui::SetNextWindowSize(ImVec2(600, 400), ImGuiCond_FirstUseEver);
439 if (ImGui::Begin("Log Console", nullptr)) {
442 if (ImGui::Button("Clear")) {
447 if (ImGui::Checkbox("Auto-Scroll", &autoScroll_)) {
449 if (prevAutoScroll_ && !autoScroll_) {
450 scrollUpOneEntry_ = true;
453 prevAutoScroll_ = autoScroll_;
457 const char* levelItems[] = { "Debug", "Info", "Warn", "Error" };
458 ImGui::SetNextItemWidth(80);
459 if (ImGui::Combo("Level", &filterLevelIndex_, levelItems, IM_ARRAYSIZE(levelItems))) {
460 filterLevel_ = static_cast<LogLevel>(filterLevelIndex_);
466 std::lock_guard<std::mutex> lock(bufferMutex_);
468 std::string scopePreview;
469 if (loggingDisabled_) {
470 scopePreview = "None";
471 } else if (selectedScopeIndex_ == 0) {
472 scopePreview = "All Scopes";
473 } else if (selectedScopeIndex_ <= static_cast<int>(collectedScopes_.size())) {
474 scopePreview = collectedScopes_[selectedScopeIndex_ - 1];
476 scopePreview = "All Scopes";
479 ImGui::SetNextItemWidth(150);
480 if (ImGui::BeginCombo("Scope", scopePreview.c_str())) {
482 bool isNoneSelected = loggingDisabled_;
483 if (ImGui::Selectable("None", isNoneSelected)) {
484 loggingDisabled_ = true;
485 selectedScopeIndex_ = -1;
486 activeScopeFilter_.clear();
487 if (onScopeFilterChanged_) {
488 onScopeFilterChanged_(NONE_SCOPES_KEY);
491 if (isNoneSelected) {
492 ImGui::SetItemDefaultFocus();
496 bool isSelected = (!loggingDisabled_ && selectedScopeIndex_ == 0);
497 if (ImGui::Selectable("All Scopes", isSelected)) {
498 loggingDisabled_ = false;
499 selectedScopeIndex_ = 0;
500 activeScopeFilter_.clear();
501 if (onScopeFilterChanged_) {
502 onScopeFilterChanged_("");
506 ImGui::SetItemDefaultFocus();
509 for (std::size_t i = 0; i < collectedScopes_.size(); ++i) {
510 isSelected = (!loggingDisabled_ && selectedScopeIndex_ == static_cast<int>(i + 1));
511 if (ImGui::Selectable(collectedScopes_[i].c_str(), isSelected)) {
512 loggingDisabled_ = false;
513 selectedScopeIndex_ = static_cast<int>(i + 1);
514 activeScopeFilter_ = collectedScopes_[i];
515 if (onScopeFilterChanged_) {
516 onScopeFilterChanged_(activeScopeFilter_);
520 ImGui::SetItemDefaultFocus();
528 textFilter_.Draw("Filter", 150);
533 const float footerHeight =
534 ImGui::GetStyle().ItemSpacing.y + ImGui::GetFrameHeightWithSpacing();
535 if (ImGui::BeginChild("LogScrollRegion", ImVec2(0, -footerHeight),
536 ImGuiChildFlags_Borders,
537 ImGuiWindowFlags_HorizontalScrollbar)) {
540 std::vector<LogEntry> bufferCopy;
541 bool hasNewContent = false;
543 std::lock_guard<std::mutex> lock(bufferMutex_);
544 bufferCopy = activeBuffer();
545 hasNewContent = scrollToBottom_;
546 scrollToBottom_ = false;
549 ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 1));
551 for (const auto& entry : bufferCopy) {
553 if (static_cast<int>(entry.level) < static_cast<int>(filterLevel_)) {
559 std::string displayLine;
560 if (activeScopeFilter_.empty()) {
563 labelForLevel(entry.level) + " [" +
572 if (!textFilter_.PassFilter(displayLine.c_str())) {
577 ImVec4 color = colorForLevel(entry.level);
578 ImGui::PushStyleColor(ImGuiCol_Text, color);
579 ImGui::TextUnformatted(displayLine.c_str());
580 ImGui::PopStyleColor();
583 ImGui::PopStyleVar();
586 float scrollY = ImGui::GetScrollY();
587 float scrollMaxY = ImGui::GetScrollMaxY();
588 bool atBottomNow = (scrollMaxY <= 0.0f) || (scrollY >= scrollMaxY - 5.0f);
592 if (scrollUpOneEntry_) {
594 float lineHeight = ImGui::GetTextLineHeightWithSpacing();
595 float nudgeAmount = lineHeight * 2.0f;
598 float targetScrollY = scrollMaxY - nudgeAmount;
599 if (targetScrollY < 0.0f) targetScrollY = 0.0f;
600 ImGui::SetScrollY(targetScrollY);
601 scrollUpOneEntry_ = false;
607 else if (hasNewContent && (autoScroll_ || wasAtBottom_)) {
608 ImGui::SetScrollHereY(1.0f);
613 bool pauseLogging = (!autoScroll_ && !atBottomNow);
614 acceptNewEntries_.store(!pauseLogging, std::memory_order_relaxed);
615 wasAtBottom_ = atBottomNow;
621 std::lock_guard<std::mutex> lock(bufferMutex_);
622 ImGui::Text("Entries: %zu / %zu", activeBuffer().size(), maxEntries_);
625 bool loggingPaused = !acceptNewEntries_.load(std::memory_order_relaxed);
626 auto skipped = skippedEntries_.load(std::memory_order_relaxed);
631 ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f),
632 "Logging paused (%zu entries skipped)", skipped);
634 ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "Logging paused");
636 } else if (skipped > 0) {
638 ImGui::TextColored(ImVec4(0.6f, 0.8f, 1.0f, 1.0f),
639 "%zu entries were skipped", skipped);
640 skippedEntries_.store(0, std::memory_order_relaxed);
Generated via doxygen2docusaurus 2.0.0 by Doxygen 1.15.0.