Unravel Engine C++ Reference
Loading...
Searching...
No Matches
console_log_panel.cpp
Go to the documentation of this file.
1#include "console_log_panel.h"
2#include "../panels_defs.h"
3
9#include <engine/engine.h>
10
11
12#include <imgui/imgui.h>
13#include <imgui/imgui_internal.h>
14#include <imgui_widgets/utils.h>
15
17#include <map>
18
19namespace unravel
20{
21static const std::array<ImColor, size_t(level::n_levels)> colors{ImColor{255, 255, 255},
22 ImColor{255, 255, 255},
23 ImColor{255, 255, 255},
24 ImColor{255, 255, 0},
25 ImColor{255, 0, 0},
26 ImColor{180, 0, 0},
27 ImColor{255, 255, 255}};
28
36
37static const std::array<const char*, size_t(level::n_levels)>
38 levels{"Trace", "Debug", "Info", "Warning", "Error", "Critical", ""};
39
40auto extract_lines(hpp::string_view text, int num_lines, int& found_lines) -> hpp::string_view
41{
42 auto pos = hpp::string_view::size_type{0};
43 found_lines = 1;
44 for(int i = 0; i < num_lines; ++i)
45 {
46 pos = text.find('\n', pos);
47 if(pos == hpp::string_view::npos)
48 {
49 break;
50 }
51 ++pos;
52 found_lines = i + 1;
53 }
54 return text.substr(0, pos);
55}
56
57void open_log_in_environment(const fs::path& entry, int line)
58{
59 if(ex::is_format<script>(entry.extension().string()))
60 {
62 }
63 else
64 {
65 // fs::show_in_graphical_env(entry);
66 }
67}
68
70{
71 set_pattern("[%H:%M:%S] %v");
72
73 enabled_categories_.fill(true);
74 enabled_categories_[level::trace] = false;
75 enabled_categories_[level::debug] = false;
76}
77
78void console_log_panel::sink_it_(const details::log_msg& msg)
79{
80 {
81 std::lock_guard<std::recursive_mutex> lock(entries_mutex_);
82
83 auto log_msg = msg;
84 log_msg.color_range_start = 0;
85 log_msg.color_range_end = 0;
86 log_msg.source = {};
87 memory_buf_t formatted;
88 formatter_->format(log_msg, formatted);
90 entry.formatted.resize(formatted.size());
91 std::memcpy(entry.formatted.data(), formatted.data(), formatted.size() * sizeof(char));
92
93 if(msg.source.filename)
94 {
95 entry.source.filename = msg.source.filename;
96 }
97 if(msg.source.funcname)
98 {
99 entry.source.funcname = msg.source.funcname;
100 }
101 if(msg.source.line)
102 {
103 entry.source.line = msg.source.line;
104 }
105
106 entry.level = msg.level;
107
108 entry.id = current_id_++;
109
110 if(new_entries_begin_idx_ == -1)
111 {
112 new_entries_begin_idx_ = int64_t(entries_.size());
113 }
114 entries_.emplace_back(std::move(entry));
115 }
116 has_new_entries_ = true;
117}
118
122
123auto console_log_panel::snapshot_logs(level::level_enum min_level, size_t max_count, uint64_t after_id) const
124 -> std::vector<log_snapshot_entry>
125{
126 std::lock_guard<std::recursive_mutex> lock(entries_mutex_);
127 std::vector<log_snapshot_entry> matched;
128 matched.reserve(std::min(max_count, entries_.size()));
129 for(size_t i = 0; i < entries_.size(); ++i)
130 {
131 const auto& entry = entries_[i];
132 if(entry.id <= after_id)
133 {
134 continue;
135 }
136 if(entry.level < min_level)
137 {
138 continue;
139 }
141 snap.id = entry.id;
142 snap.level = entry.level;
143 snap.text.assign(entry.formatted.begin(), entry.formatted.end());
144 snap.filename = entry.source.filename;
145 snap.funcname = entry.source.funcname;
146 snap.line = entry.source.line;
147 matched.push_back(std::move(snap));
148 }
149 if(matched.size() > max_count)
150 {
151 matched.erase(matched.begin(), matched.end() - static_cast<std::ptrdiff_t>(max_count));
152 }
153 return matched;
154}
155
156void console_log_panel::clear_log()
157{
158 {
159 std::lock_guard<std::recursive_mutex> lock(entries_mutex_);
160 entries_.clear();
161 selected_log_ = {};
162 }
163 has_new_entries_ = false;
164}
165
166auto console_log_panel::has_new_entries() const -> bool
167{
168 return has_new_entries_;
169}
170
171void console_log_panel::set_has_new_entries(bool val)
172{
173 has_new_entries_ = val;
174}
175
176void console_log_panel::draw_range(const hpp::string_view& formatted, size_t start, size_t end)
177{
178 if(end > start)
179 {
180 auto end_clamped = std::min(end, formatted.size());
181 auto size = (end_clamped - start);
182 auto text_start = formatted.data() + start;
183 auto text_end = text_start + size;
184 ImGui::TextUnformatted(text_start, text_end);
185 }
186}
187
188void console_log_panel::draw_filter_button(level::level_enum level)
189{
190 auto c = colors[level].Value;
191 auto multiplier = enabled_categories_[level] ? ImVec4(1.0f, 1.0f, 1.0f, 1.0f) : ImVec4(0.5f, 0.5f, 0.5f, 0.8f);
192 ImGui::PushStyleColor(ImGuiCol_Text, c * multiplier);
193 if(ImGui::MenuItem(icons[level], nullptr, enabled_categories_[level]))
194 {
195 enabled_categories_[level] = !enabled_categories_[level];
196 }
197 ImGui::PopStyleColor();
198 ImGui::SetItemTooltipEx("%s", fmt::format("Enables/Disables {} logs.", levels[level]).c_str());
199}
200
201auto console_log_panel::draw_log(const log_entry& msg, int num_lines) -> bool
202{
203 ImGui::BeginGroup();
204 auto col = colors[size_t(msg.level)];
205 auto icon = icons[size_t(msg.level)];
206 auto level = levels[size_t(msg.level)];
207
208 ImGui::PushStyleColor(ImGuiCol_Text, col.Value);
209 ImGui::AlignTextToFramePadding();
210
211 int found_lines = 1;
212 auto view = extract_lines({msg.formatted.data(), msg.formatted.size()}, 1, found_lines);
213 ImGui::PushWindowFontSize(ImGui::GetFontSize() * num_lines);
214 ImGui::TextUnformatted(icon);
216 ImGui::SameLine();
217 ImGui::BeginGroup();
218
219 draw_range(view, 0, view.size());
220 if(found_lines != num_lines)
221 {
222 ImGui::TextUnformatted(level);
223 }
224
225 ImGui::EndGroup();
226
227 ImGui::PopStyleColor();
228 ImGui::SameLine();
229 ImGui::Dummy({ImGui::GetContentRegionAvail().x, ImGui::GetFrameHeight() * num_lines});
230 ImGui::EndGroup();
231
232 bool clicked = ImGui::IsItemClicked();
233 if(clicked)
234 {
235 select_log(msg);
236 }
237
238 if(ImGui::IsItemDoubleClicked())
239 {
240 open_log(msg);
241 }
242 return clicked;
243}
244
245auto console_log_panel::get_window_flags() const -> ImGuiWindowFlags
246{
247 return ImGuiWindowFlags_MenuBar | ImGuiWindowFlags_NoScrollbar;
248}
249
251{
252 (void)ctx;
253 draw();
254}
255
257{
258 auto avail = ImGui::GetContentRegionAvail();
259 if(avail.x < 1.0f || avail.y < 1.0f)
260 {
261 return;
262 }
263
264 if(ImGui::BeginMenuBar())
265 {
266 ImGui::PushStyleColor(ImGuiCol_Header, ImGui::GetStyleColorVec4(ImGuiCol_TabSelected));
267 ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImGui::GetStyleColorVec4(ImGuiCol_TabSelected));
268 ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImGui::GetStyleColorVec4(ImGuiCol_TabSelectedOverline));
269
270 ImGui::DrawFilterWithHint(filter_, ICON_MDI_TEXT_BOX_SEARCH " Search...", 200.0f);
271 ImGui::DrawItemActivityOutline();
272 ImGui::SameLine();
273 if(ImGui::MenuItem("Clear"))
274 {
275 clear_log();
276 }
277 ImGui::SameLine();
278 if(ImGui::BeginMenu(ICON_MDI_ARROW_DOWN_BOLD, true))
279 {
280 if(ImGui::MenuItem("Clear on Play", nullptr, clear_on_play_))
281 {
282 clear_on_play_ = !clear_on_play_;
283 }
284 if(ImGui::MenuItem("Clear on Recompile", nullptr, clear_on_recompile_))
285 {
286 clear_on_recompile_ = !clear_on_recompile_;
287 }
288 ImGui::EndMenu();
289 }
290
291 draw_filter_button(level::err);
292 draw_filter_button(level::warn);
293 draw_filter_button(level::info);
294 draw_filter_button(level::trace);
295 draw_filter_button(level::debug);
296
297 ImGui::PopStyleColor(3);
298
299 ImGui::EndMenuBar();
300 }
301
302 avail = ImGui::GetContentRegionAvail();
303
304 ImGui::SetNextWindowSizeConstraints(ImVec2(0.0f, 100.0f), ImVec2(FLT_MAX, FLT_MAX));
305 // ImGui::SetNextWindowSizeConstraints({-1.0f, 100.0f}, {-1.0f, -1.0f});
306 // Display every line as a separate entry so we can change their color or add custom widgets. If you only
307 // want raw text you can use ImGui::TextUnformatted(log.begin(), log.end());
308 // NB- if you have thousands of entries this approach may be too inefficient. You can seek and display
309 // only the lines that are visible - CalcListClipping() is a helper to compute this information.
310 // If your items are of variable size you may want to implement code similar to what CalcListClipping()
311 // does. Or split your data into fixed height items to allow random-seeking into your list.
312
313 ImGui::BeginChild("ScrollingRegion", avail * ImVec2(1.0f, 0.8f), ImGuiChildFlags_ResizeY);
314 if(ImGui::BeginPopupContextWindow(nullptr, ImGuiPopupFlags_MouseButtonRight))
315 {
316 {
318
320 {
321 clear_log();
322 }
323 }
324 ImGui::EndPopup();
325 }
326 ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4, 1)); // Tighten spacing
327
329 {
330 std::lock_guard<std::recursive_mutex> lock(entries_mutex_);
331 for(size_t i = 0; i < entries_.size(); ++i)
332 {
333 const auto& msg = entries_[i];
334
335 if(!enabled_categories_[msg.level])
336 continue;
337
338 if(!filter_.PassFilter(msg.formatted.data(), msg.formatted.data() + msg.formatted.size()))
339 continue;
340
341 entries.emplace_back(msg);
342 }
343 }
344
345
346 ImGuiListClipper clipper;
347 clipper.Begin(int(entries.size()));
348 while(clipper.Step())
349 {
350 for(int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
351 {
352 const auto& msg = entries[i];
353
354 if(selected_log_)
355 {
356 const auto& selected = *selected_log_;
357 if(selected.id == msg.id)
358 {
359 auto min = ImGui::GetCursorScreenPos();
360 auto max = min + ImVec2(ImGui::GetContentRegionAvail().x, ImGui::GetFrameHeight() * 2);
361 ImGui::RenderFrame(min, max, ImColor(80, 80, 0));
362 }
363 }
364
365 draw_log(msg, 2);
366 }
367 }
368
369
370 if(has_new_entries() && ImGui::GetScrollY() > (ImGui::GetScrollMaxY() - 0.01f))
371 ImGui::SetScrollHereY();
372
373 set_has_new_entries(false);
374
375 ImGui::PopStyleVar();
376 ImGui::EndChild();
377
378 // ImGui::SetNextWindowSizeConstraints({-1.0f, 100.0f}, {-1.0f, -1.0f});
379 ImGui::SetNextWindowSizeConstraints(ImVec2(0.0f, 100.0f), ImVec2(FLT_MAX, FLT_MAX));
380 avail = ImGui::GetContentRegionAvail();
381 avail.y = ImMax(avail.y, 100.0f);
382 ImGui::PushStyleColor(ImGuiCol_Separator, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled));
383 ImGui::Separator();
384 ImGui::PopStyleColor();
385 ImGui::BeginChild("DetailsArea", avail, ImGuiChildFlags_None);
386
387 draw_details();
388 ImGui::EndChild();
389}
390
392{
393 log_entry msg;
394
395 {
396 std::lock_guard<std::recursive_mutex> lock(entries_mutex_);
397 for(auto it = std::rbegin(entries_); it != std::rend(entries_); ++it)
398 {
399 const auto& check = *it;
400 if(!enabled_categories_[check.level])
401 {
402 continue;
403 }
404
405 if(!filter_.PassFilter(check.formatted.data(), check.formatted.data() + check.formatted.size()))
406 {
407 continue;
408 }
409 msg = check;
410 break;
411 }
412 }
413
414 if(msg.formatted.empty())
415 {
416 return false;
417 }
418
419 draw_log(msg, 1);
420
421 return true;
422}
423
425{
426 auto pos = ImGui::GetCursorPos();
427
428 if(draw_last_log())
429 {
430 ImGui::SetCursorPos(pos);
431
432 if(ImGui::InvisibleButton("shortcut", ImGui::GetItemRectSize()))
433 {
434 ImGui::FocusWindow(ImGui::FindWindowByName(get_name().c_str()));
435 }
436 }
437
438 uint64_t errors_count = 0;
439 if(new_entries_begin_idx_ != -1)
440 {
441 std::lock_guard<std::recursive_mutex> lock(entries_mutex_);
442 for(size_t i = new_entries_begin_idx_; i < entries_.size(); ++i)
443 {
444 const auto& msg = entries_[i];
445 if(msg.level == level::err)
446 {
447 errors_count++;
448 }
449 }
450
451 new_entries_begin_idx_ = -1;
452 }
453
454 if(errors_count > 0)
455 {
456 ImGui::PushNotification({ ImGuiToastType_Error, 2000, fmt::format("{} Error(s)...", errors_count).c_str() });
457 }
458}
459
461{
462 std::lock_guard<std::recursive_mutex> lock(entries_mutex_);
463
464 if(selected_log_)
465 {
466 const auto& msg = *selected_log_;
467 string_view_t str(msg.formatted.data(), msg.formatted.size());
468 auto desc = fmt::format("{0}{1}() (at [{2}:{3}]({2}:{3}))",
469 str,
470 msg.source.funcname,
471 msg.source.filename,
472 msg.source.line);
473
474 ImGui::MarkdownConfig config{};
475 config.linkCallback = [&](const char* link, uint32_t link_length)
476 {
477 open_log_in_environment(fs::path(msg.source.filename), msg.source.line);
478 };
479 ImGui::Markdown(desc.data(), int32_t(desc.size()), config);
480 }
481}
482
483void console_log_panel::select_log(const log_entry& entry)
484{
485 selected_log_ = entry;
486}
487
488void console_log_panel::open_log(const log_entry& entry)
489{
490 open_log_in_environment(entry.source.filename, entry.source.line);
491}
492
494{
495 if(clear_on_play_)
496 {
497 clear_log();
498 }
499}
500
502{
503 if(clear_on_recompile_)
504 {
505 clear_log();
506 }
507}
508
509} // namespace unravel
console_log_panel(const char *name)
auto snapshot_logs(level::level_enum min_level, size_t max_count, uint64_t after_id=0) const -> std::vector< log_snapshot_entry >
Snapshot recent log entries (thread-safe). Newest-last order. Filters by min_level (inclusive) and op...
hpp::small_vector< log_entry, 1024 > display_entries_t
void draw_ui(rtti::context &ctx) override
auto get_window_flags() const -> ImGuiWindowFlags override
void sink_it_(const details::log_msg &msg) override
auto get_name() const -> const std::string &
Definition panel_base.h:29
float x
const char * icon
uint16_t view
std::vector< render_pass_entry > entries
std::string name
Definition hub.cpp:33
#define ICON_MDI_ARROW_DOWN_BOLD
#define ICON_MDI_ALERT_BOX
#define ICON_MDI_ALERT_CIRCLE
#define ICON_MDI_DELETE_SWEEP
#define ICON_MDI_ALERT_CIRCLE_CHECK
#define ICON_MDI_BUG_CHECK_OUTLINE
#define ICON_MDI_TEXT_BOX_SEARCH
#define ICON_MDI_ALERT_OCTAGON
@ ImGuiToastType_Error
imported_texture desc
void PushWindowFontSize(int size)
Definition imgui.cpp:695
NOTIFY_INLINE void PushNotification(const ImGuiToast &toast)
Insert a new toast in the list.
void PopWindowFontSize()
Definition imgui.cpp:711
auto MenuItemIcon(const char *icon, const char *label, const char *shortcut, bool enabled) -> bool
auto is_format(const std::string &ex) -> bool
auto extract_lines(hpp::string_view text, int num_lines, int &found_lines) -> hpp::string_view
static const std::array< const char *, size_t(level::n_levels)> levels
void open_log_in_environment(const fs::path &entry, int line)
static const std::array< ImColor, size_t(level::n_levels)> colors
static const std::array< const char *, size_t(level::n_levels)> icons
std::vector< math::vec3 > start
mem_buf formatted
uint64_t id
std::string funcname
std::string filename
int line
level::level_enum level
std::string text
static void open_workspace_on_file(const fs::path &file, int line=0)
float size