2026-03-26 22:31:22 +08:00
|
|
|
#include "Actions/ActionRouting.h"
|
2026-03-31 21:28:16 +08:00
|
|
|
#include "Actions/ConsoleActionRouter.h"
|
2026-03-20 17:08:06 +08:00
|
|
|
#include "ConsolePanel.h"
|
2026-03-31 21:28:16 +08:00
|
|
|
#include "Core/EditorConsoleSink.h"
|
|
|
|
|
#include "Core/EditorEvents.h"
|
|
|
|
|
#include "Core/EventBus.h"
|
|
|
|
|
#include "Core/IEditorContext.h"
|
|
|
|
|
#include "Platform/Win32Utf8.h"
|
2026-03-26 21:18:33 +08:00
|
|
|
#include "UI/UI.h"
|
2026-03-31 21:28:16 +08:00
|
|
|
|
2026-03-20 17:08:06 +08:00
|
|
|
#include <imgui.h>
|
2026-03-31 21:28:16 +08:00
|
|
|
#include <shellapi.h>
|
|
|
|
|
|
|
|
|
|
#include <cctype>
|
|
|
|
|
#include <cmath>
|
|
|
|
|
#include <ctime>
|
|
|
|
|
#include <filesystem>
|
2026-03-31 23:05:52 +08:00
|
|
|
#include <functional>
|
2026-03-31 21:28:16 +08:00
|
|
|
#include <string>
|
|
|
|
|
#include <unordered_map>
|
|
|
|
|
#include <vector>
|
|
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
|
|
|
|
|
using XCEngine::Debug::EditorConsoleRecord;
|
|
|
|
|
using XCEngine::Debug::EditorConsoleSink;
|
|
|
|
|
using XCEngine::Debug::LogEntry;
|
|
|
|
|
using XCEngine::Debug::LogLevel;
|
|
|
|
|
|
|
|
|
|
enum class ConsoleSeverityVisual {
|
|
|
|
|
Log,
|
|
|
|
|
Warning,
|
|
|
|
|
Error
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
struct ConsoleSeverityCounts {
|
|
|
|
|
size_t logCount = 0;
|
|
|
|
|
size_t warningCount = 0;
|
|
|
|
|
size_t errorCount = 0;
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-01 16:40:54 +08:00
|
|
|
bool CanOpenSourceLocation(const LogEntry& entry);
|
|
|
|
|
|
2026-03-31 21:28:16 +08:00
|
|
|
struct ConsoleRowData {
|
|
|
|
|
uint64_t serial = 0;
|
|
|
|
|
LogEntry entry = {};
|
|
|
|
|
std::string entryKey;
|
|
|
|
|
size_t count = 1;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
struct ConsoleRowInteraction {
|
|
|
|
|
bool clicked = false;
|
|
|
|
|
bool rightClicked = false;
|
|
|
|
|
bool doubleClicked = false;
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-31 23:05:52 +08:00
|
|
|
bool CanOpenSourceLocation(const LogEntry& entry);
|
|
|
|
|
|
|
|
|
|
constexpr float kConsoleToolbarHeight = 26.0f;
|
|
|
|
|
constexpr float kConsoleToolbarButtonHeight = 20.0f;
|
|
|
|
|
constexpr float kConsoleToolbarRowPaddingY = 3.0f;
|
|
|
|
|
constexpr float kConsoleCounterWidth = 36.0f;
|
|
|
|
|
constexpr float kConsoleSearchWidth = 220.0f;
|
|
|
|
|
constexpr float kConsoleRowHeight = 20.0f;
|
|
|
|
|
constexpr float kConsoleDetailsHeaderHeight = 24.0f;
|
|
|
|
|
constexpr float kConsoleToolbarItemSpacing = 4.0f;
|
|
|
|
|
constexpr ImVec4 kConsoleToolbarBackgroundColor = ImVec4(0.23f, 0.23f, 0.23f, 1.0f);
|
|
|
|
|
|
|
|
|
|
std::filesystem::path ResolveConsoleIconPath(const wchar_t* fileName) {
|
|
|
|
|
const std::filesystem::path exeDir(XCEngine::Editor::Platform::Utf8ToWide(XCEngine::Editor::Platform::GetExecutableDirectoryUtf8()));
|
|
|
|
|
return (exeDir / L".." / L".." / L"resources" / L"Icons" / fileName).lexically_normal();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const std::string& ConsoleInfoRowIconPath() {
|
|
|
|
|
static const std::string path = XCEngine::Editor::Platform::WideToUtf8(ResolveConsoleIconPath(L"console__info_icon.png").wstring());
|
|
|
|
|
return path;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const std::string& ConsoleWarnRowIconPath() {
|
|
|
|
|
static const std::string path = XCEngine::Editor::Platform::WideToUtf8(ResolveConsoleIconPath(L"console_warn_icon.png").wstring());
|
|
|
|
|
return path;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const std::string& ConsoleErrorRowIconPath() {
|
|
|
|
|
static const std::string path = XCEngine::Editor::Platform::WideToUtf8(ResolveConsoleIconPath(L"console_error_icon.png").wstring());
|
|
|
|
|
return path;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const std::string& ConsoleInfoButtonIconPath() {
|
|
|
|
|
static const std::string path = XCEngine::Editor::Platform::WideToUtf8(ResolveConsoleIconPath(L"console_info_button_icon.png").wstring());
|
|
|
|
|
return path;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const std::string& ConsoleWarnButtonIconPath() {
|
|
|
|
|
static const std::string path = XCEngine::Editor::Platform::WideToUtf8(ResolveConsoleIconPath(L"console_warn_button_icon.png").wstring());
|
|
|
|
|
return path;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const std::string& ConsoleErrorButtonIconPath() {
|
|
|
|
|
static const std::string path = XCEngine::Editor::Platform::WideToUtf8(ResolveConsoleIconPath(L"console_error_button_icon.png").wstring());
|
|
|
|
|
return path;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const std::string& IconPathForSeverity(ConsoleSeverityVisual severity, bool toolbarButton) {
|
|
|
|
|
switch (severity) {
|
|
|
|
|
case ConsoleSeverityVisual::Warning:
|
|
|
|
|
return toolbarButton ? ConsoleWarnButtonIconPath() : ConsoleWarnRowIconPath();
|
|
|
|
|
case ConsoleSeverityVisual::Error:
|
|
|
|
|
return toolbarButton ? ConsoleErrorButtonIconPath() : ConsoleErrorRowIconPath();
|
|
|
|
|
case ConsoleSeverityVisual::Log:
|
|
|
|
|
default:
|
|
|
|
|
return toolbarButton ? ConsoleInfoButtonIconPath() : ConsoleInfoRowIconPath();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 21:28:16 +08:00
|
|
|
ConsoleSeverityVisual ResolveSeverity(LogLevel level) {
|
|
|
|
|
switch (level) {
|
|
|
|
|
case LogLevel::Warning:
|
|
|
|
|
return ConsoleSeverityVisual::Warning;
|
|
|
|
|
case LogLevel::Error:
|
|
|
|
|
case LogLevel::Fatal:
|
|
|
|
|
return ConsoleSeverityVisual::Error;
|
|
|
|
|
case LogLevel::Verbose:
|
|
|
|
|
case LogLevel::Debug:
|
|
|
|
|
case LogLevel::Info:
|
|
|
|
|
default:
|
|
|
|
|
return ConsoleSeverityVisual::Log;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImVec4 SeverityColor(ConsoleSeverityVisual severity) {
|
|
|
|
|
switch (severity) {
|
|
|
|
|
case ConsoleSeverityVisual::Warning:
|
|
|
|
|
return XCEngine::Editor::UI::ConsoleWarningColor();
|
|
|
|
|
case ConsoleSeverityVisual::Error:
|
|
|
|
|
return XCEngine::Editor::UI::ConsoleErrorColor();
|
|
|
|
|
case ConsoleSeverityVisual::Log:
|
|
|
|
|
default:
|
|
|
|
|
return XCEngine::Editor::UI::ConsoleLogColor();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool IsErrorLevel(LogLevel level) {
|
|
|
|
|
return level == LogLevel::Error || level == LogLevel::Fatal;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string BuildEntryKey(const LogEntry& entry) {
|
|
|
|
|
std::string key;
|
|
|
|
|
key.reserve(128 + entry.message.Length() + entry.file.Length() + entry.function.Length());
|
|
|
|
|
key += std::to_string(static_cast<int>(entry.level));
|
|
|
|
|
key.push_back('\x1f');
|
|
|
|
|
key += std::to_string(static_cast<int>(entry.category));
|
|
|
|
|
key.push_back('\x1f');
|
|
|
|
|
key += entry.message.CStr();
|
|
|
|
|
key.push_back('\x1f');
|
|
|
|
|
key += entry.file.CStr();
|
|
|
|
|
key.push_back('\x1f');
|
|
|
|
|
key += std::to_string(entry.line);
|
|
|
|
|
key.push_back('\x1f');
|
|
|
|
|
key += entry.function.CStr();
|
|
|
|
|
return key;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string BuildSearchHaystack(const LogEntry& entry) {
|
|
|
|
|
std::string haystack = XCEngine::Editor::UI::BuildConsoleLogText(entry);
|
|
|
|
|
haystack.push_back('\n');
|
|
|
|
|
haystack += entry.file.CStr();
|
|
|
|
|
haystack.push_back('\n');
|
|
|
|
|
haystack += entry.function.CStr();
|
2026-04-01 16:40:54 +08:00
|
|
|
return haystack;
|
2026-03-31 21:28:16 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-01 16:40:54 +08:00
|
|
|
bool MatchesSearch(const LogEntry& entry, const XCEngine::Editor::UI::SearchQuery& searchQuery) {
|
|
|
|
|
return searchQuery.Matches(BuildSearchHaystack(entry));
|
2026-03-31 21:28:16 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void CountSeverity(ConsoleSeverityCounts& counts, LogLevel level) {
|
|
|
|
|
switch (ResolveSeverity(level)) {
|
|
|
|
|
case ConsoleSeverityVisual::Warning:
|
|
|
|
|
++counts.warningCount;
|
|
|
|
|
break;
|
|
|
|
|
case ConsoleSeverityVisual::Error:
|
|
|
|
|
++counts.errorCount;
|
|
|
|
|
break;
|
|
|
|
|
case ConsoleSeverityVisual::Log:
|
|
|
|
|
default:
|
|
|
|
|
++counts.logCount;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint64_t FindLatestSerial(const std::vector<EditorConsoleRecord>& records) {
|
|
|
|
|
uint64_t latestSerial = 0;
|
|
|
|
|
for (const EditorConsoleRecord& record : records) {
|
|
|
|
|
latestSerial = (std::max)(latestSerial, record.serial);
|
|
|
|
|
}
|
|
|
|
|
return latestSerial;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::vector<ConsoleRowData> BuildVisibleRows(
|
|
|
|
|
const std::vector<EditorConsoleRecord>& records,
|
|
|
|
|
const XCEngine::Editor::UI::ConsoleFilterState& filterState,
|
2026-04-01 16:40:54 +08:00
|
|
|
const XCEngine::Editor::UI::SearchQuery& searchQuery,
|
2026-03-31 21:28:16 +08:00
|
|
|
ConsoleSeverityCounts& counts) {
|
|
|
|
|
std::vector<ConsoleRowData> rows;
|
|
|
|
|
rows.reserve(records.size());
|
|
|
|
|
|
|
|
|
|
if (!filterState.Collapse()) {
|
|
|
|
|
for (const EditorConsoleRecord& record : records) {
|
|
|
|
|
CountSeverity(counts, record.entry.level);
|
2026-04-01 16:40:54 +08:00
|
|
|
if (!filterState.Allows(record.entry.level) || !MatchesSearch(record.entry, searchQuery)) {
|
2026-03-31 21:28:16 +08:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rows.push_back(ConsoleRowData{
|
|
|
|
|
record.serial,
|
|
|
|
|
record.entry,
|
|
|
|
|
BuildEntryKey(record.entry),
|
|
|
|
|
1u
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return rows;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::unordered_map<std::string, size_t> rowIndicesByKey;
|
|
|
|
|
rowIndicesByKey.reserve(records.size());
|
|
|
|
|
for (const EditorConsoleRecord& record : records) {
|
|
|
|
|
CountSeverity(counts, record.entry.level);
|
2026-04-01 16:40:54 +08:00
|
|
|
if (!filterState.Allows(record.entry.level) || !MatchesSearch(record.entry, searchQuery)) {
|
2026-03-31 21:28:16 +08:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const std::string entryKey = BuildEntryKey(record.entry);
|
|
|
|
|
const auto it = rowIndicesByKey.find(entryKey);
|
|
|
|
|
if (it == rowIndicesByKey.end()) {
|
|
|
|
|
rowIndicesByKey.emplace(entryKey, rows.size());
|
|
|
|
|
rows.push_back(ConsoleRowData{
|
|
|
|
|
record.serial,
|
|
|
|
|
record.entry,
|
|
|
|
|
entryKey,
|
|
|
|
|
1u
|
|
|
|
|
});
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ConsoleRowData& row = rows[it->second];
|
|
|
|
|
row.serial = record.serial;
|
|
|
|
|
row.entry = record.entry;
|
|
|
|
|
++row.count;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return rows;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ConsoleRowData* ResolveSelectedRow(
|
|
|
|
|
const std::vector<ConsoleRowData>& rows,
|
|
|
|
|
uint64_t& selectedSerial,
|
|
|
|
|
const std::string& selectedEntryKey) {
|
|
|
|
|
if (selectedSerial != 0) {
|
|
|
|
|
for (const ConsoleRowData& row : rows) {
|
|
|
|
|
if (row.serial == selectedSerial) {
|
|
|
|
|
return &row;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!selectedEntryKey.empty()) {
|
|
|
|
|
for (const ConsoleRowData& row : rows) {
|
|
|
|
|
if (row.entryKey == selectedEntryKey) {
|
|
|
|
|
selectedSerial = row.serial;
|
|
|
|
|
return &row;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
selectedSerial = 0;
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool SelectRelativeRow(const std::vector<ConsoleRowData>& rows, uint64_t& selectedSerial, int delta) {
|
|
|
|
|
if (rows.empty() || delta == 0) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int currentIndex = -1;
|
|
|
|
|
for (int i = 0; i < static_cast<int>(rows.size()); ++i) {
|
|
|
|
|
if (rows[i].serial == selectedSerial) {
|
|
|
|
|
currentIndex = i;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int nextIndex = 0;
|
|
|
|
|
if (currentIndex >= 0) {
|
|
|
|
|
nextIndex = std::clamp(currentIndex + delta, 0, static_cast<int>(rows.size()) - 1);
|
|
|
|
|
} else if (delta > 0) {
|
|
|
|
|
nextIndex = 0;
|
|
|
|
|
} else {
|
|
|
|
|
nextIndex = static_cast<int>(rows.size()) - 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (rows[nextIndex].serial == selectedSerial) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
selectedSerial = rows[nextIndex].serial;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 23:05:52 +08:00
|
|
|
void DrawSeverityIcon(
|
|
|
|
|
ImDrawList* drawList,
|
|
|
|
|
const ImVec2& center,
|
|
|
|
|
float radius,
|
|
|
|
|
ConsoleSeverityVisual severity,
|
|
|
|
|
bool toolbarButton = false) {
|
|
|
|
|
const std::string& iconPath = IconPathForSeverity(severity, toolbarButton);
|
|
|
|
|
if (!iconPath.empty() &&
|
|
|
|
|
XCEngine::Editor::UI::DrawTextureAssetPreview(
|
|
|
|
|
drawList,
|
|
|
|
|
ImVec2(center.x - radius, center.y - radius),
|
|
|
|
|
ImVec2(center.x + radius, center.y + radius),
|
|
|
|
|
iconPath)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 21:28:16 +08:00
|
|
|
const ImU32 color = ImGui::GetColorU32(SeverityColor(severity));
|
|
|
|
|
switch (severity) {
|
|
|
|
|
case ConsoleSeverityVisual::Warning:
|
|
|
|
|
drawList->AddTriangleFilled(
|
|
|
|
|
ImVec2(center.x, center.y - radius),
|
|
|
|
|
ImVec2(center.x - radius * 0.90f, center.y + radius * 0.78f),
|
|
|
|
|
ImVec2(center.x + radius * 0.90f, center.y + radius * 0.78f),
|
|
|
|
|
color);
|
|
|
|
|
break;
|
|
|
|
|
case ConsoleSeverityVisual::Error: {
|
|
|
|
|
ImVec2 points[8];
|
|
|
|
|
for (int i = 0; i < 8; ++i) {
|
|
|
|
|
const float angle = (45.0f * static_cast<float>(i) + 22.5f) * 3.14159265f / 180.0f;
|
|
|
|
|
points[i] = ImVec2(center.x + std::cos(angle) * radius, center.y + std::sin(angle) * radius);
|
|
|
|
|
}
|
|
|
|
|
drawList->AddConvexPolyFilled(points, 8, color);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case ConsoleSeverityVisual::Log:
|
|
|
|
|
default:
|
|
|
|
|
drawList->AddCircleFilled(center, radius, color, 16);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 23:05:52 +08:00
|
|
|
bool DrawToolbarDropdownButton(
|
|
|
|
|
const char* id,
|
|
|
|
|
const char* label,
|
|
|
|
|
float width,
|
|
|
|
|
const std::function<void()>& drawPopup,
|
|
|
|
|
bool active = false) {
|
|
|
|
|
const ImVec2 size(width, kConsoleToolbarButtonHeight);
|
|
|
|
|
ImGui::InvisibleButton(id, size);
|
|
|
|
|
const bool hovered = ImGui::IsItemHovered();
|
|
|
|
|
const bool held = ImGui::IsItemActive();
|
|
|
|
|
const bool pressed = ImGui::IsItemClicked(ImGuiMouseButton_Left);
|
|
|
|
|
|
|
|
|
|
const ImVec2 min = ImGui::GetItemRectMin();
|
|
|
|
|
const ImVec2 max = ImGui::GetItemRectMax();
|
|
|
|
|
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
|
|
|
|
drawList->AddRectFilled(
|
|
|
|
|
min,
|
|
|
|
|
max,
|
|
|
|
|
ImGui::GetColorU32(
|
|
|
|
|
held ? XCEngine::Editor::UI::ToolbarButtonActiveColor()
|
|
|
|
|
: hovered ? XCEngine::Editor::UI::ToolbarButtonHoveredColor(active)
|
|
|
|
|
: XCEngine::Editor::UI::ToolbarButtonColor(active)),
|
|
|
|
|
2.0f);
|
|
|
|
|
|
|
|
|
|
const ImVec2 textSize = ImGui::CalcTextSize(label);
|
|
|
|
|
drawList->AddText(
|
|
|
|
|
ImVec2(min.x + 8.0f, min.y + (size.y - textSize.y) * 0.5f),
|
|
|
|
|
ImGui::GetColorU32(ImGuiCol_Text),
|
|
|
|
|
label);
|
|
|
|
|
|
|
|
|
|
const float arrowCenterX = max.x - 9.0f;
|
|
|
|
|
const float arrowCenterY = min.y + size.y * 0.5f + 1.0f;
|
|
|
|
|
drawList->AddTriangleFilled(
|
|
|
|
|
ImVec2(arrowCenterX - 3.0f, arrowCenterY - 2.0f),
|
|
|
|
|
ImVec2(arrowCenterX + 3.0f, arrowCenterY - 2.0f),
|
|
|
|
|
ImVec2(arrowCenterX, arrowCenterY + 2.0f),
|
|
|
|
|
ImGui::GetColorU32(ImGuiCol_Text));
|
|
|
|
|
|
|
|
|
|
if (pressed) {
|
|
|
|
|
ImGui::OpenPopup(id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (XCEngine::Editor::UI::BeginContextMenu(id, ImGuiWindowFlags_AlwaysAutoResize)) {
|
|
|
|
|
drawPopup();
|
|
|
|
|
XCEngine::Editor::UI::EndContextMenu();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return pressed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void DrawToolbarOverflowButton(
|
|
|
|
|
const char* id,
|
|
|
|
|
const std::function<void()>& drawPopup) {
|
|
|
|
|
const ImVec2 size(18.0f, kConsoleToolbarButtonHeight);
|
|
|
|
|
ImGui::InvisibleButton(id, size);
|
|
|
|
|
const bool hovered = ImGui::IsItemHovered();
|
|
|
|
|
const bool held = ImGui::IsItemActive();
|
|
|
|
|
const bool pressed = ImGui::IsItemClicked(ImGuiMouseButton_Left);
|
|
|
|
|
|
|
|
|
|
const ImVec2 min = ImGui::GetItemRectMin();
|
|
|
|
|
const ImVec2 max = ImGui::GetItemRectMax();
|
|
|
|
|
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
|
|
|
|
drawList->AddRectFilled(
|
|
|
|
|
min,
|
|
|
|
|
max,
|
|
|
|
|
ImGui::GetColorU32(
|
|
|
|
|
held ? XCEngine::Editor::UI::ToolbarButtonActiveColor()
|
|
|
|
|
: hovered ? XCEngine::Editor::UI::ToolbarButtonHoveredColor(false)
|
|
|
|
|
: XCEngine::Editor::UI::ToolbarButtonColor(false)),
|
|
|
|
|
2.0f);
|
|
|
|
|
|
|
|
|
|
const float cx = (min.x + max.x) * 0.5f;
|
|
|
|
|
const float startY = min.y + size.y * 0.5f - 4.0f;
|
|
|
|
|
for (int i = 0; i < 3; ++i) {
|
|
|
|
|
drawList->AddCircleFilled(
|
|
|
|
|
ImVec2(cx, startY + i * 4.0f),
|
|
|
|
|
1.2f,
|
|
|
|
|
ImGui::GetColorU32(ImGuiCol_Text),
|
|
|
|
|
8);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (pressed) {
|
|
|
|
|
ImGui::OpenPopup(id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (XCEngine::Editor::UI::BeginContextMenu(id, ImGuiWindowFlags_AlwaysAutoResize)) {
|
|
|
|
|
drawPopup();
|
|
|
|
|
XCEngine::Editor::UI::EndContextMenu();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool DrawToolbarToggleButton(
|
|
|
|
|
const char* id,
|
|
|
|
|
const char* label,
|
|
|
|
|
bool& active,
|
|
|
|
|
float width = 0.0f) {
|
|
|
|
|
const ImVec2 textSize = ImGui::CalcTextSize(label);
|
|
|
|
|
const ImVec2 size(
|
|
|
|
|
width > 0.0f ? width : textSize.x + 18.0f,
|
|
|
|
|
kConsoleToolbarButtonHeight);
|
|
|
|
|
ImGui::InvisibleButton(id, size);
|
|
|
|
|
const bool hovered = ImGui::IsItemHovered();
|
|
|
|
|
const bool held = ImGui::IsItemActive();
|
|
|
|
|
const bool pressed = ImGui::IsItemClicked(ImGuiMouseButton_Left);
|
|
|
|
|
if (pressed) {
|
|
|
|
|
active = !active;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ImVec2 min = ImGui::GetItemRectMin();
|
|
|
|
|
const ImVec2 max = ImGui::GetItemRectMax();
|
|
|
|
|
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
|
|
|
|
drawList->AddRectFilled(
|
|
|
|
|
min,
|
|
|
|
|
max,
|
|
|
|
|
ImGui::GetColorU32(
|
|
|
|
|
held ? XCEngine::Editor::UI::ToolbarButtonActiveColor()
|
|
|
|
|
: hovered ? XCEngine::Editor::UI::ToolbarButtonHoveredColor(active)
|
|
|
|
|
: XCEngine::Editor::UI::ToolbarButtonColor(active)),
|
|
|
|
|
2.0f);
|
|
|
|
|
drawList->AddText(
|
|
|
|
|
ImVec2(min.x + (size.x - textSize.x) * 0.5f, min.y + (size.y - textSize.y) * 0.5f),
|
|
|
|
|
ImGui::GetColorU32(ImGuiCol_Text),
|
|
|
|
|
label);
|
|
|
|
|
return pressed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool DrawToolbarButton(
|
|
|
|
|
const char* id,
|
|
|
|
|
const char* label,
|
|
|
|
|
float width = 0.0f,
|
|
|
|
|
bool active = false) {
|
|
|
|
|
const ImVec2 textSize = ImGui::CalcTextSize(label);
|
|
|
|
|
const ImVec2 size(
|
|
|
|
|
width > 0.0f ? width : textSize.x + 18.0f,
|
|
|
|
|
kConsoleToolbarButtonHeight);
|
|
|
|
|
ImGui::InvisibleButton(id, size);
|
|
|
|
|
const bool hovered = ImGui::IsItemHovered();
|
|
|
|
|
const bool held = ImGui::IsItemActive();
|
|
|
|
|
const bool pressed = ImGui::IsItemClicked(ImGuiMouseButton_Left);
|
|
|
|
|
|
|
|
|
|
const ImVec2 min = ImGui::GetItemRectMin();
|
|
|
|
|
const ImVec2 max = ImGui::GetItemRectMax();
|
|
|
|
|
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
|
|
|
|
drawList->AddRectFilled(
|
|
|
|
|
min,
|
|
|
|
|
max,
|
|
|
|
|
ImGui::GetColorU32(
|
|
|
|
|
held ? XCEngine::Editor::UI::ToolbarButtonActiveColor()
|
|
|
|
|
: hovered ? XCEngine::Editor::UI::ToolbarButtonHoveredColor(active)
|
|
|
|
|
: XCEngine::Editor::UI::ToolbarButtonColor(active)),
|
|
|
|
|
2.0f);
|
|
|
|
|
drawList->AddText(
|
|
|
|
|
ImVec2(min.x + (size.x - textSize.x) * 0.5f, min.y + (size.y - textSize.y) * 0.5f),
|
|
|
|
|
ImGui::GetColorU32(ImGuiCol_Text),
|
|
|
|
|
label);
|
|
|
|
|
return pressed;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-01 16:40:54 +08:00
|
|
|
bool DrawCompactCheckedMenuItem(const char* label, bool checked) {
|
|
|
|
|
if (!label) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const bool clicked = ImGui::Selectable(label, false, ImGuiSelectableFlags_SpanAvailWidth);
|
|
|
|
|
if (checked) {
|
|
|
|
|
const ImRect rect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax());
|
|
|
|
|
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
|
|
|
|
const ImU32 color = ImGui::GetColorU32(ImGuiCol_CheckMark);
|
|
|
|
|
const float height = rect.Max.y - rect.Min.y;
|
|
|
|
|
const float checkWidth = height * 0.28f;
|
|
|
|
|
const float checkHeight = height * 0.18f;
|
|
|
|
|
const float x = rect.Max.x - 12.0f;
|
|
|
|
|
const float y = rect.Min.y + height * 0.52f;
|
|
|
|
|
|
|
|
|
|
drawList->AddLine(
|
|
|
|
|
ImVec2(x - checkWidth, y - checkHeight * 0.15f),
|
|
|
|
|
ImVec2(x - checkWidth * 0.42f, y + checkHeight),
|
|
|
|
|
color,
|
|
|
|
|
1.4f);
|
|
|
|
|
drawList->AddLine(
|
|
|
|
|
ImVec2(x - checkWidth * 0.42f, y + checkHeight),
|
|
|
|
|
ImVec2(x + checkWidth, y - checkHeight),
|
|
|
|
|
color,
|
|
|
|
|
1.4f);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return clicked;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 23:05:52 +08:00
|
|
|
void DrawToolbarArrowDropdownButton(
|
|
|
|
|
const char* id,
|
|
|
|
|
float width,
|
|
|
|
|
const std::function<void()>& drawPopup,
|
|
|
|
|
bool active = false) {
|
|
|
|
|
const ImVec2 size(width, kConsoleToolbarButtonHeight);
|
|
|
|
|
ImGui::InvisibleButton(id, size);
|
|
|
|
|
const bool hovered = ImGui::IsItemHovered();
|
|
|
|
|
const bool held = ImGui::IsItemActive();
|
|
|
|
|
const bool pressed = ImGui::IsItemClicked(ImGuiMouseButton_Left);
|
|
|
|
|
|
|
|
|
|
const ImVec2 min = ImGui::GetItemRectMin();
|
|
|
|
|
const ImVec2 max = ImGui::GetItemRectMax();
|
|
|
|
|
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
|
|
|
|
drawList->AddRectFilled(
|
|
|
|
|
min,
|
|
|
|
|
max,
|
|
|
|
|
ImGui::GetColorU32(
|
|
|
|
|
held ? XCEngine::Editor::UI::ToolbarButtonActiveColor()
|
|
|
|
|
: hovered ? XCEngine::Editor::UI::ToolbarButtonHoveredColor(active)
|
|
|
|
|
: XCEngine::Editor::UI::ToolbarButtonColor(active)),
|
|
|
|
|
2.0f);
|
|
|
|
|
|
|
|
|
|
const float arrowCenterX = (min.x + max.x) * 0.5f;
|
|
|
|
|
const float arrowCenterY = min.y + size.y * 0.5f + 1.0f;
|
|
|
|
|
drawList->AddTriangleFilled(
|
|
|
|
|
ImVec2(arrowCenterX - 3.0f, arrowCenterY - 2.0f),
|
|
|
|
|
ImVec2(arrowCenterX + 3.0f, arrowCenterY - 2.0f),
|
|
|
|
|
ImVec2(arrowCenterX, arrowCenterY + 2.0f),
|
|
|
|
|
ImGui::GetColorU32(ImGuiCol_Text));
|
|
|
|
|
|
|
|
|
|
if (pressed) {
|
|
|
|
|
ImGui::OpenPopup(id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (XCEngine::Editor::UI::BeginContextMenu(id, ImGuiWindowFlags_AlwaysAutoResize)) {
|
|
|
|
|
drawPopup();
|
|
|
|
|
XCEngine::Editor::UI::EndContextMenu();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 21:28:16 +08:00
|
|
|
bool DrawSeverityToggleButton(
|
|
|
|
|
const char* id,
|
|
|
|
|
ConsoleSeverityVisual severity,
|
|
|
|
|
size_t count,
|
|
|
|
|
bool& active,
|
|
|
|
|
const char* tooltip) {
|
2026-03-31 23:05:52 +08:00
|
|
|
const float originalCursorY = ImGui::GetCursorPosY();
|
2026-04-01 16:40:54 +08:00
|
|
|
ImGui::SetCursorPosY((std::max)(0.0f, originalCursorY - kConsoleToolbarRowPaddingY));
|
2026-03-31 23:05:52 +08:00
|
|
|
|
2026-03-31 21:28:16 +08:00
|
|
|
const std::string countText = std::to_string(count);
|
|
|
|
|
const ImVec2 countSize = ImGui::CalcTextSize(countText.c_str());
|
2026-04-01 16:40:54 +08:00
|
|
|
const ImVec2 padding(7.0f, 3.0f);
|
|
|
|
|
const float iconRadius = 10.0f;
|
|
|
|
|
const float gap = 5.0f;
|
|
|
|
|
const float buttonHeight = kConsoleToolbarHeight;
|
2026-03-31 21:28:16 +08:00
|
|
|
const ImVec2 size(
|
|
|
|
|
(std::max)(
|
2026-03-31 23:05:52 +08:00
|
|
|
kConsoleCounterWidth,
|
2026-03-31 21:28:16 +08:00
|
|
|
padding.x * 2.0f + iconRadius * 2.0f + gap + countSize.x),
|
2026-03-31 23:05:52 +08:00
|
|
|
buttonHeight);
|
2026-03-31 21:28:16 +08:00
|
|
|
|
|
|
|
|
ImGui::InvisibleButton(id, size);
|
|
|
|
|
const bool hovered = ImGui::IsItemHovered();
|
|
|
|
|
const bool held = ImGui::IsItemActive();
|
|
|
|
|
const bool pressed = ImGui::IsItemClicked(ImGuiMouseButton_Left);
|
|
|
|
|
if (pressed) {
|
|
|
|
|
active = !active;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ImVec2 min = ImGui::GetItemRectMin();
|
|
|
|
|
const ImVec2 max = ImGui::GetItemRectMax();
|
|
|
|
|
const ImU32 backgroundColor = ImGui::GetColorU32(
|
|
|
|
|
held ? XCEngine::Editor::UI::ToolbarButtonActiveColor()
|
|
|
|
|
: hovered ? XCEngine::Editor::UI::ToolbarButtonHoveredColor(active)
|
|
|
|
|
: XCEngine::Editor::UI::ToolbarButtonColor(active));
|
|
|
|
|
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
|
|
|
|
drawList->AddRectFilled(min, max, backgroundColor, 2.0f);
|
|
|
|
|
|
|
|
|
|
const ImVec2 iconCenter(min.x + padding.x + iconRadius, min.y + size.y * 0.5f);
|
2026-03-31 23:05:52 +08:00
|
|
|
DrawSeverityIcon(drawList, iconCenter, iconRadius, severity, true);
|
2026-03-31 21:28:16 +08:00
|
|
|
drawList->AddText(
|
|
|
|
|
ImVec2(iconCenter.x + iconRadius + gap, min.y + (size.y - countSize.y) * 0.5f),
|
|
|
|
|
ImGui::GetColorU32(ImGuiCol_Text),
|
|
|
|
|
countText.c_str());
|
|
|
|
|
|
|
|
|
|
if (hovered && tooltip && tooltip[0] != '\0') {
|
|
|
|
|
ImGui::SetTooltip("%s", tooltip);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return pressed;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 23:05:52 +08:00
|
|
|
float CalculateSeverityToggleButtonWidth(size_t count) {
|
|
|
|
|
const std::string countText = std::to_string(count);
|
|
|
|
|
const ImVec2 countSize = ImGui::CalcTextSize(countText.c_str());
|
2026-04-01 16:40:54 +08:00
|
|
|
const ImVec2 padding(7.0f, 3.0f);
|
|
|
|
|
const float iconRadius = 10.0f;
|
|
|
|
|
const float gap = 5.0f;
|
2026-03-31 23:05:52 +08:00
|
|
|
return (std::max)(
|
|
|
|
|
kConsoleCounterWidth,
|
|
|
|
|
padding.x * 2.0f + iconRadius * 2.0f + gap + countSize.x);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 21:28:16 +08:00
|
|
|
ConsoleRowInteraction DrawConsoleRow(const ConsoleRowData& row, bool selected) {
|
|
|
|
|
ConsoleRowInteraction interaction;
|
|
|
|
|
|
2026-03-31 23:05:52 +08:00
|
|
|
const float rowHeight = kConsoleRowHeight;
|
2026-03-31 21:28:16 +08:00
|
|
|
const float availableWidth = (std::max)(ImGui::GetContentRegionAvail().x, 1.0f);
|
|
|
|
|
ImGui::InvisibleButton("##ConsoleRow", ImVec2(availableWidth, rowHeight));
|
|
|
|
|
interaction.clicked = ImGui::IsItemClicked(ImGuiMouseButton_Left);
|
|
|
|
|
interaction.rightClicked = ImGui::IsItemClicked(ImGuiMouseButton_Right);
|
|
|
|
|
interaction.doubleClicked = ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left);
|
|
|
|
|
|
|
|
|
|
const bool hovered = ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem);
|
|
|
|
|
const ImVec2 min = ImGui::GetItemRectMin();
|
|
|
|
|
const ImVec2 max = ImGui::GetItemRectMax();
|
|
|
|
|
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
|
|
|
|
if (selected) {
|
|
|
|
|
drawList->AddRectFilled(min, max, ImGui::GetColorU32(XCEngine::Editor::UI::ConsoleRowSelectedFillColor()));
|
|
|
|
|
} else if (hovered) {
|
|
|
|
|
drawList->AddRectFilled(min, max, ImGui::GetColorU32(XCEngine::Editor::UI::ConsoleRowHoverFillColor()));
|
|
|
|
|
}
|
2026-03-31 23:05:52 +08:00
|
|
|
drawList->AddLine(
|
|
|
|
|
ImVec2(min.x, max.y - 0.5f),
|
|
|
|
|
ImVec2(max.x, max.y - 0.5f),
|
|
|
|
|
ImGui::GetColorU32(ImVec4(0.0f, 0.0f, 0.0f, 0.28f)));
|
2026-03-31 21:28:16 +08:00
|
|
|
|
|
|
|
|
const ConsoleSeverityVisual severity = ResolveSeverity(row.entry.level);
|
2026-03-31 23:05:52 +08:00
|
|
|
const ImVec2 iconCenter(min.x + 10.0f, min.y + rowHeight * 0.5f);
|
|
|
|
|
DrawSeverityIcon(drawList, iconCenter, 4.5f, severity);
|
2026-03-31 21:28:16 +08:00
|
|
|
|
2026-03-31 23:05:52 +08:00
|
|
|
float rightEdge = max.x - 6.0f;
|
2026-03-31 21:28:16 +08:00
|
|
|
if (row.count > 1) {
|
|
|
|
|
const std::string countText = std::to_string(row.count);
|
|
|
|
|
const ImVec2 countSize = ImGui::CalcTextSize(countText.c_str());
|
2026-03-31 23:05:52 +08:00
|
|
|
const ImVec2 badgeMin(rightEdge - countSize.x - 8.0f, min.y + 3.0f);
|
2026-03-31 21:28:16 +08:00
|
|
|
const ImVec2 badgeMax(rightEdge, max.y - 3.0f);
|
|
|
|
|
drawList->AddRectFilled(
|
|
|
|
|
badgeMin,
|
|
|
|
|
badgeMax,
|
|
|
|
|
ImGui::GetColorU32(XCEngine::Editor::UI::ConsoleCountBadgeBackgroundColor()),
|
2026-03-31 23:05:52 +08:00
|
|
|
2.0f);
|
2026-03-31 21:28:16 +08:00
|
|
|
drawList->AddText(
|
|
|
|
|
ImVec2(
|
|
|
|
|
badgeMin.x + (badgeMax.x - badgeMin.x - countSize.x) * 0.5f,
|
|
|
|
|
badgeMin.y + (badgeMax.y - badgeMin.y - countSize.y) * 0.5f),
|
|
|
|
|
ImGui::GetColorU32(XCEngine::Editor::UI::ConsoleCountBadgeTextColor()),
|
|
|
|
|
countText.c_str());
|
2026-03-31 23:05:52 +08:00
|
|
|
rightEdge = badgeMin.x - 6.0f;
|
2026-03-31 21:28:16 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const std::string summary = XCEngine::Editor::UI::BuildConsoleSummaryText(row.entry);
|
2026-03-31 23:05:52 +08:00
|
|
|
const ImVec2 textMin(min.x + 22.0f, min.y);
|
2026-03-31 21:28:16 +08:00
|
|
|
const ImVec2 textMax((std::max)(textMin.x, rightEdge), max.y);
|
|
|
|
|
drawList->PushClipRect(textMin, textMax, true);
|
|
|
|
|
drawList->AddText(
|
|
|
|
|
ImVec2(textMin.x, min.y + (rowHeight - ImGui::GetTextLineHeight()) * 0.5f),
|
|
|
|
|
ImGui::GetColorU32(ImGuiCol_Text),
|
|
|
|
|
summary.c_str());
|
|
|
|
|
drawList->PopClipRect();
|
|
|
|
|
|
2026-03-31 23:05:52 +08:00
|
|
|
if (hovered) {
|
|
|
|
|
const std::string sourceText = XCEngine::Editor::UI::BuildConsoleSourceText(row.entry);
|
|
|
|
|
XCEngine::Editor::UI::BeginTitledTooltip(XCEngine::Editor::UI::ConsoleSeverityLabel(row.entry.level));
|
|
|
|
|
ImGui::PushTextWrapPos(ImGui::GetFontSize() * 28.0f);
|
|
|
|
|
ImGui::TextUnformatted(summary.c_str());
|
|
|
|
|
ImGui::PopTextWrapPos();
|
|
|
|
|
if (!sourceText.empty() || row.count > 1 || CanOpenSourceLocation(row.entry)) {
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
if (!sourceText.empty()) {
|
|
|
|
|
ImGui::TextColored(XCEngine::Editor::UI::ConsoleSecondaryTextColor(), "%s", sourceText.c_str());
|
|
|
|
|
}
|
|
|
|
|
if (row.count > 1) {
|
|
|
|
|
ImGui::TextColored(
|
|
|
|
|
XCEngine::Editor::UI::ConsoleSecondaryTextColor(),
|
|
|
|
|
"Occurrences: %zu",
|
|
|
|
|
row.count);
|
|
|
|
|
}
|
|
|
|
|
if (CanOpenSourceLocation(row.entry)) {
|
|
|
|
|
ImGui::TextColored(
|
|
|
|
|
XCEngine::Editor::UI::ConsoleSecondaryTextColor(),
|
|
|
|
|
"Double-click to open source");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
XCEngine::Editor::UI::EndTitledTooltip();
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 21:28:16 +08:00
|
|
|
return interaction;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string FormatTimestamp(uint64_t timestamp) {
|
|
|
|
|
if (timestamp == 0) {
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const std::time_t seconds = static_cast<std::time_t>(timestamp);
|
|
|
|
|
std::tm localTime = {};
|
|
|
|
|
localtime_s(&localTime, &seconds);
|
|
|
|
|
char buffer[32] = {};
|
|
|
|
|
if (std::strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", &localTime) == 0) {
|
|
|
|
|
return std::to_string(timestamp);
|
|
|
|
|
}
|
|
|
|
|
return buffer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool TryResolveSourcePath(const LogEntry& entry, std::filesystem::path& outPath) {
|
|
|
|
|
const std::string filePath = entry.file.CStr();
|
|
|
|
|
if (filePath.empty()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::error_code ec;
|
|
|
|
|
std::filesystem::path path(XCEngine::Editor::Platform::Utf8ToWide(filePath));
|
|
|
|
|
if (path.is_relative()) {
|
|
|
|
|
path = std::filesystem::current_path(ec) / path;
|
|
|
|
|
ec.clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
path = std::filesystem::weakly_canonical(path, ec);
|
|
|
|
|
if (ec) {
|
|
|
|
|
path = std::filesystem::path(XCEngine::Editor::Platform::Utf8ToWide(filePath)).lexically_normal();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!std::filesystem::exists(path)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
outPath = path;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool CanOpenSourceLocation(const LogEntry& entry) {
|
|
|
|
|
std::filesystem::path resolvedPath;
|
|
|
|
|
return TryResolveSourcePath(entry, resolvedPath);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool OpenSourceLocation(const LogEntry& entry) {
|
|
|
|
|
std::filesystem::path resolvedPath;
|
|
|
|
|
if (!TryResolveSourcePath(entry, resolvedPath)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const HINSTANCE result = ShellExecuteW(
|
|
|
|
|
nullptr,
|
|
|
|
|
L"open",
|
|
|
|
|
resolvedPath.c_str(),
|
|
|
|
|
nullptr,
|
|
|
|
|
resolvedPath.parent_path().c_str(),
|
|
|
|
|
SW_SHOWNORMAL);
|
|
|
|
|
return reinterpret_cast<INT_PTR>(result) > 32;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void CopySummaryToClipboard(const ConsoleRowData& row) {
|
|
|
|
|
const std::string copyText = XCEngine::Editor::UI::BuildConsoleLogText(row.entry);
|
|
|
|
|
ImGui::SetClipboardText(copyText.c_str());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void CopyToClipboard(const ConsoleRowData& row) {
|
|
|
|
|
const std::string copyText = XCEngine::Editor::UI::BuildConsoleCopyText(row.entry);
|
|
|
|
|
ImGui::SetClipboardText(copyText.c_str());
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 23:05:52 +08:00
|
|
|
std::string BuildDetailsTraceText(const ConsoleRowData& row) {
|
|
|
|
|
std::string traceText;
|
|
|
|
|
const LogEntry& entry = row.entry;
|
|
|
|
|
const std::string fileText = entry.file.CStr();
|
|
|
|
|
const std::string functionText = entry.function.CStr();
|
|
|
|
|
const std::string timeText = FormatTimestamp(entry.timestamp);
|
|
|
|
|
|
|
|
|
|
if (!functionText.empty() || !fileText.empty()) {
|
|
|
|
|
traceText += "at ";
|
|
|
|
|
traceText += functionText.empty() ? "<unknown>" : functionText;
|
|
|
|
|
if (!fileText.empty()) {
|
|
|
|
|
traceText += " (";
|
|
|
|
|
traceText += fileText;
|
|
|
|
|
if (entry.line > 0) {
|
|
|
|
|
traceText += ":";
|
|
|
|
|
traceText += std::to_string(entry.line);
|
|
|
|
|
}
|
|
|
|
|
traceText += ")";
|
|
|
|
|
}
|
|
|
|
|
traceText += "\n";
|
|
|
|
|
} else {
|
|
|
|
|
traceText += "No source location captured.\n";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const char* category = XCEngine::Debug::LogCategoryToString(entry.category);
|
|
|
|
|
if (category && category[0] != '\0') {
|
|
|
|
|
traceText += "category: ";
|
|
|
|
|
traceText += category;
|
|
|
|
|
traceText += "\n";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
traceText += "thread: ";
|
|
|
|
|
traceText += std::to_string(entry.threadId);
|
|
|
|
|
traceText += "\n";
|
|
|
|
|
|
|
|
|
|
if (!timeText.empty()) {
|
|
|
|
|
traceText += "time: ";
|
|
|
|
|
traceText += timeText;
|
|
|
|
|
traceText += "\n";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (row.count > 1) {
|
|
|
|
|
traceText += "occurrences: ";
|
|
|
|
|
traceText += std::to_string(row.count);
|
|
|
|
|
traceText += "\n";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return traceText;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 21:28:16 +08:00
|
|
|
} // namespace
|
2026-03-20 17:08:06 +08:00
|
|
|
|
2026-03-24 20:02:38 +08:00
|
|
|
namespace XCEngine {
|
|
|
|
|
namespace Editor {
|
2026-03-20 17:08:06 +08:00
|
|
|
|
|
|
|
|
ConsolePanel::ConsolePanel() : Panel("Console") {
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 21:28:16 +08:00
|
|
|
void ConsolePanel::OnAttach() {
|
|
|
|
|
if (!m_context) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (m_detailsHeight <= 0.0f) {
|
|
|
|
|
m_detailsHeight = UI::ConsoleDetailsDefaultHeight();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
EditorConsoleSink* sink = EditorConsoleSink::GetInstance();
|
|
|
|
|
m_lastSeenRevision = sink ? sink->GetRevision() : 0;
|
|
|
|
|
m_lastErrorPauseScanSerial = sink ? FindLatestSerial(sink->GetRecords()) : 0;
|
|
|
|
|
|
|
|
|
|
if (!m_playModeStartedHandlerId) {
|
|
|
|
|
m_playModeStartedHandlerId = m_context->GetEventBus().Subscribe<PlayModeStartedEvent>(
|
|
|
|
|
[this](const PlayModeStartedEvent&) {
|
|
|
|
|
HandlePlayModeStarted();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
if (!m_playModeStoppedHandlerId) {
|
|
|
|
|
m_playModeStoppedHandlerId = m_context->GetEventBus().Subscribe<PlayModeStoppedEvent>(
|
|
|
|
|
[this](const PlayModeStoppedEvent&) {
|
|
|
|
|
HandlePlayModeStopped();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
if (!m_playModePausedHandlerId) {
|
|
|
|
|
m_playModePausedHandlerId = m_context->GetEventBus().Subscribe<PlayModePausedEvent>(
|
|
|
|
|
[this](const PlayModePausedEvent&) {
|
|
|
|
|
HandlePlayModePaused();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ConsolePanel::OnDetach() {
|
|
|
|
|
if (!m_context) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (m_playModeStartedHandlerId) {
|
|
|
|
|
m_context->GetEventBus().Unsubscribe<PlayModeStartedEvent>(m_playModeStartedHandlerId);
|
|
|
|
|
m_playModeStartedHandlerId = 0;
|
|
|
|
|
}
|
|
|
|
|
if (m_playModeStoppedHandlerId) {
|
|
|
|
|
m_context->GetEventBus().Unsubscribe<PlayModeStoppedEvent>(m_playModeStoppedHandlerId);
|
|
|
|
|
m_playModeStoppedHandlerId = 0;
|
|
|
|
|
}
|
|
|
|
|
if (m_playModePausedHandlerId) {
|
|
|
|
|
m_context->GetEventBus().Unsubscribe<PlayModePausedEvent>(m_playModePausedHandlerId);
|
|
|
|
|
m_playModePausedHandlerId = 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ConsolePanel::HandlePlayModeStarted() {
|
|
|
|
|
m_playModeActive = true;
|
|
|
|
|
m_playModePaused = false;
|
|
|
|
|
|
|
|
|
|
EditorConsoleSink* sink = EditorConsoleSink::GetInstance();
|
|
|
|
|
if (!sink) {
|
|
|
|
|
m_lastErrorPauseScanSerial = 0;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (m_filterState.ClearOnPlay()) {
|
|
|
|
|
sink->Clear();
|
|
|
|
|
m_selectedSerial = 0;
|
|
|
|
|
m_selectedEntryKey.clear();
|
|
|
|
|
m_lastErrorPauseScanSerial = 0;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m_lastErrorPauseScanSerial = FindLatestSerial(sink->GetRecords());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ConsolePanel::HandlePlayModeStopped() {
|
|
|
|
|
m_playModeActive = false;
|
|
|
|
|
m_playModePaused = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ConsolePanel::HandlePlayModePaused() {
|
|
|
|
|
m_playModeActive = true;
|
|
|
|
|
m_playModePaused = true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 17:08:06 +08:00
|
|
|
void ConsolePanel::Render() {
|
2026-03-26 16:43:06 +08:00
|
|
|
UI::PanelWindowScope panel(m_name.c_str());
|
|
|
|
|
if (!panel.IsOpen()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-03-26 01:26:26 +08:00
|
|
|
|
2026-03-31 21:28:16 +08:00
|
|
|
EditorConsoleSink* sink = EditorConsoleSink::GetInstance();
|
|
|
|
|
if (!sink) {
|
|
|
|
|
UI::PanelContentScope content("ConsoleUnavailable", UI::DefaultPanelContentPadding());
|
|
|
|
|
if (content.IsOpen()) {
|
|
|
|
|
UI::DrawEmptyState("Console unavailable");
|
|
|
|
|
}
|
|
|
|
|
Actions::ObserveInactiveActionRoute(*m_context);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const std::vector<EditorConsoleRecord> records = sink->GetRecords();
|
|
|
|
|
const uint64_t revision = sink->GetRevision();
|
|
|
|
|
const bool hasNewRevision = revision != m_lastSeenRevision;
|
|
|
|
|
m_lastSeenRevision = revision;
|
|
|
|
|
|
|
|
|
|
if (m_playModeActive && m_filterState.ErrorPause() && !m_playModePaused) {
|
|
|
|
|
bool shouldPause = false;
|
|
|
|
|
uint64_t latestSerial = m_lastErrorPauseScanSerial;
|
|
|
|
|
for (const EditorConsoleRecord& record : records) {
|
|
|
|
|
latestSerial = (std::max)(latestSerial, record.serial);
|
|
|
|
|
if (record.serial > m_lastErrorPauseScanSerial && IsErrorLevel(record.entry.level)) {
|
|
|
|
|
shouldPause = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (shouldPause) {
|
|
|
|
|
m_context->GetEventBus().Publish(PlayModePausedEvent{});
|
|
|
|
|
m_playModePaused = true;
|
|
|
|
|
}
|
|
|
|
|
m_lastErrorPauseScanSerial = latestSerial;
|
|
|
|
|
} else {
|
|
|
|
|
m_lastErrorPauseScanSerial = FindLatestSerial(records);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-01 16:40:54 +08:00
|
|
|
const UI::SearchQuery searchQuery(m_searchBuffer);
|
2026-03-31 21:28:16 +08:00
|
|
|
ConsoleSeverityCounts counts;
|
2026-04-01 16:40:54 +08:00
|
|
|
std::vector<ConsoleRowData> rows = BuildVisibleRows(records, m_filterState, searchQuery, counts);
|
2026-03-31 21:28:16 +08:00
|
|
|
const ConsoleRowData* selectedRow = ResolveSelectedRow(rows, m_selectedSerial, m_selectedEntryKey);
|
|
|
|
|
if (selectedRow) {
|
|
|
|
|
m_selectedEntryKey = selectedRow->entryKey;
|
|
|
|
|
} else if (m_selectedSerial == 0) {
|
|
|
|
|
m_selectedEntryKey.clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const bool panelFocused = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows);
|
|
|
|
|
bool scrollSelectionIntoView = false;
|
|
|
|
|
if (panelFocused && !ImGui::GetIO().WantTextInput) {
|
|
|
|
|
if (ImGui::GetIO().KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_F, false)) {
|
|
|
|
|
m_requestSearchFocus = true;
|
|
|
|
|
}
|
|
|
|
|
if (ImGui::GetIO().KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_C, false) && selectedRow) {
|
|
|
|
|
CopyToClipboard(*selectedRow);
|
|
|
|
|
}
|
|
|
|
|
if (ImGui::IsKeyPressed(ImGuiKey_UpArrow, false) && SelectRelativeRow(rows, m_selectedSerial, -1)) {
|
|
|
|
|
scrollSelectionIntoView = true;
|
|
|
|
|
}
|
|
|
|
|
if (ImGui::IsKeyPressed(ImGuiKey_DownArrow, false) && SelectRelativeRow(rows, m_selectedSerial, 1)) {
|
|
|
|
|
scrollSelectionIntoView = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (scrollSelectionIntoView) {
|
|
|
|
|
selectedRow = ResolveSelectedRow(rows, m_selectedSerial, m_selectedEntryKey);
|
|
|
|
|
if (selectedRow) {
|
|
|
|
|
m_selectedEntryKey = selectedRow->entryKey;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 23:05:52 +08:00
|
|
|
const float logFilterWidth = CalculateSeverityToggleButtonWidth(counts.logCount);
|
|
|
|
|
const float warningFilterWidth = CalculateSeverityToggleButtonWidth(counts.warningCount);
|
|
|
|
|
const float errorFilterWidth = CalculateSeverityToggleButtonWidth(counts.errorCount);
|
|
|
|
|
const float severityGroupWidth = logFilterWidth + warningFilterWidth + errorFilterWidth;
|
|
|
|
|
|
2026-03-31 21:28:16 +08:00
|
|
|
UI::PanelToolbarScope toolbar(
|
|
|
|
|
"ConsoleToolbar",
|
2026-03-31 23:05:52 +08:00
|
|
|
kConsoleToolbarHeight,
|
2026-03-31 21:28:16 +08:00
|
|
|
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse,
|
|
|
|
|
true,
|
2026-03-31 23:05:52 +08:00
|
|
|
ImVec2(6.0f, kConsoleToolbarRowPaddingY),
|
|
|
|
|
ImVec2(kConsoleToolbarItemSpacing, 0.0f),
|
|
|
|
|
kConsoleToolbarBackgroundColor);
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(0.0f, 0.0f));
|
2026-03-31 21:28:16 +08:00
|
|
|
if (toolbar.IsOpen() &&
|
|
|
|
|
ImGui::BeginTable(
|
|
|
|
|
"##ConsoleToolbarLayout",
|
|
|
|
|
3,
|
|
|
|
|
ImGuiTableFlags_NoSavedSettings | ImGuiTableFlags_SizingStretchProp))
|
|
|
|
|
{
|
|
|
|
|
ImGui::TableSetupColumn("##Primary", ImGuiTableColumnFlags_WidthStretch);
|
2026-03-31 23:05:52 +08:00
|
|
|
ImGui::TableSetupColumn("##Search", ImGuiTableColumnFlags_WidthFixed, kConsoleSearchWidth);
|
|
|
|
|
ImGui::TableSetupColumn("##Severity", ImGuiTableColumnFlags_WidthFixed, severityGroupWidth);
|
2026-03-31 21:28:16 +08:00
|
|
|
ImGui::TableNextRow();
|
|
|
|
|
|
|
|
|
|
ImGui::TableNextColumn();
|
2026-03-31 23:05:52 +08:00
|
|
|
if (DrawToolbarButton("##ConsoleClearButton", "Clear", 42.0f)) {
|
2026-03-31 21:28:16 +08:00
|
|
|
sink->Clear();
|
|
|
|
|
m_selectedSerial = 0;
|
|
|
|
|
m_selectedEntryKey.clear();
|
|
|
|
|
}
|
2026-03-31 23:05:52 +08:00
|
|
|
ImGui::SameLine(0.0f, 1.0f);
|
|
|
|
|
DrawToolbarArrowDropdownButton("##ConsoleClearOptions", 16.0f, [&]() {
|
2026-04-01 16:40:54 +08:00
|
|
|
if (DrawCompactCheckedMenuItem("Clear on Play", m_filterState.ClearOnPlay())) {
|
2026-03-31 23:05:52 +08:00
|
|
|
m_filterState.ClearOnPlay() = !m_filterState.ClearOnPlay();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
ImGui::SameLine(0.0f, kConsoleToolbarItemSpacing);
|
|
|
|
|
DrawToolbarToggleButton("##ConsoleCollapseToggle", "Collapse", m_filterState.Collapse(), 64.0f);
|
|
|
|
|
ImGui::SameLine(0.0f, kConsoleToolbarItemSpacing);
|
|
|
|
|
DrawToolbarToggleButton("##ConsoleErrorPauseToggle", "Error Pause", m_filterState.ErrorPause(), 82.0f);
|
|
|
|
|
ImGui::SameLine(0.0f, kConsoleToolbarItemSpacing);
|
|
|
|
|
DrawToolbarDropdownButton("##ConsoleSourceDropdown", "Editor", 58.0f, [&]() {
|
|
|
|
|
ImGui::MenuItem("Editor", nullptr, true, true);
|
|
|
|
|
ImGui::MenuItem("Player", nullptr, false, false);
|
|
|
|
|
});
|
2026-03-31 21:28:16 +08:00
|
|
|
|
|
|
|
|
ImGui::TableNextColumn();
|
|
|
|
|
if (m_requestSearchFocus) {
|
|
|
|
|
ImGui::SetKeyboardFocusHere();
|
|
|
|
|
m_requestSearchFocus = false;
|
|
|
|
|
}
|
2026-04-01 16:40:54 +08:00
|
|
|
UI::ToolbarSearchField("##ConsoleSearch", "Search", m_searchBuffer, sizeof(m_searchBuffer));
|
2026-03-31 21:28:16 +08:00
|
|
|
|
|
|
|
|
ImGui::TableNextColumn();
|
|
|
|
|
DrawSeverityToggleButton("##ConsoleLogFilter", ConsoleSeverityVisual::Log, counts.logCount, m_filterState.ShowLog(), "Log");
|
2026-03-31 23:05:52 +08:00
|
|
|
ImGui::SameLine(0.0f, 0.0f);
|
2026-03-31 21:28:16 +08:00
|
|
|
DrawSeverityToggleButton("##ConsoleWarningFilter", ConsoleSeverityVisual::Warning, counts.warningCount, m_filterState.ShowWarning(), "Warnings");
|
2026-03-31 23:05:52 +08:00
|
|
|
ImGui::SameLine(0.0f, 0.0f);
|
2026-03-31 21:28:16 +08:00
|
|
|
DrawSeverityToggleButton("##ConsoleErrorFilter", ConsoleSeverityVisual::Error, counts.errorCount, m_filterState.ShowError(), "Errors");
|
|
|
|
|
|
|
|
|
|
ImGui::EndTable();
|
|
|
|
|
}
|
2026-03-31 23:05:52 +08:00
|
|
|
ImGui::PopStyleVar();
|
2026-03-31 21:28:16 +08:00
|
|
|
|
|
|
|
|
UI::PanelContentScope content("ConsoleRoot", ImVec2(0.0f, 0.0f));
|
|
|
|
|
if (!content.IsOpen()) {
|
|
|
|
|
Actions::ObserveInactiveActionRoute(*m_context);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const float splitterThickness = UI::PanelSplitterHitThickness();
|
|
|
|
|
const float totalHeight = ImGui::GetContentRegionAvail().y;
|
|
|
|
|
const float minDetailsHeight = UI::ConsoleDetailsMinHeight();
|
|
|
|
|
const float minListHeight = UI::ConsoleListMinHeight();
|
|
|
|
|
const float maxDetailsHeight = (std::max)(minDetailsHeight, totalHeight - minListHeight - splitterThickness);
|
|
|
|
|
m_detailsHeight = std::clamp(m_detailsHeight, minDetailsHeight, maxDetailsHeight);
|
|
|
|
|
const float listHeight = (std::max)(minListHeight, totalHeight - m_detailsHeight - splitterThickness);
|
|
|
|
|
|
2026-03-31 23:05:52 +08:00
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0f, 0.0f));
|
2026-03-31 21:28:16 +08:00
|
|
|
ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ConsoleListBackgroundColor());
|
|
|
|
|
const bool listOpen = ImGui::BeginChild(
|
|
|
|
|
"ConsoleLogList",
|
|
|
|
|
ImVec2(0.0f, listHeight),
|
|
|
|
|
false,
|
|
|
|
|
ImGuiWindowFlags_HorizontalScrollbar);
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
if (listOpen) {
|
|
|
|
|
const bool wasAtBottom = ImGui::GetScrollY() >= ImGui::GetScrollMaxY() - 4.0f;
|
|
|
|
|
bool openSelectedSource = false;
|
|
|
|
|
ImGuiListClipper clipper;
|
2026-03-31 23:05:52 +08:00
|
|
|
clipper.Begin(static_cast<int>(rows.size()), kConsoleRowHeight);
|
2026-03-31 21:28:16 +08:00
|
|
|
while (clipper.Step()) {
|
|
|
|
|
for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; ++i) {
|
|
|
|
|
ConsoleRowData& row = rows[static_cast<size_t>(i)];
|
|
|
|
|
ImGui::PushID(static_cast<int>(row.serial));
|
|
|
|
|
const bool selected = row.serial == m_selectedSerial;
|
|
|
|
|
const ConsoleRowInteraction interaction = DrawConsoleRow(row, selected);
|
|
|
|
|
if (interaction.clicked || interaction.rightClicked) {
|
|
|
|
|
m_selectedSerial = row.serial;
|
|
|
|
|
m_selectedEntryKey = row.entryKey;
|
|
|
|
|
selectedRow = &row;
|
|
|
|
|
}
|
|
|
|
|
if (interaction.doubleClicked) {
|
|
|
|
|
openSelectedSource = true;
|
|
|
|
|
}
|
|
|
|
|
if (scrollSelectionIntoView && row.serial == m_selectedSerial) {
|
|
|
|
|
ImGui::SetScrollHereY(0.5f);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (UI::BeginContextMenuForLastItem("##ConsoleRowContext")) {
|
|
|
|
|
Actions::DrawMenuAction(Actions::MakeAction("Copy", "Ctrl+C", false, true), [&]() {
|
|
|
|
|
CopySummaryToClipboard(row);
|
|
|
|
|
});
|
|
|
|
|
Actions::DrawMenuAction(Actions::MakeAction("Copy Full", nullptr, false, true), [&]() {
|
|
|
|
|
CopyToClipboard(row);
|
|
|
|
|
});
|
|
|
|
|
Actions::DrawMenuAction(
|
|
|
|
|
Actions::MakeAction("Open Source", nullptr, false, CanOpenSourceLocation(row.entry)),
|
|
|
|
|
[&]() {
|
|
|
|
|
OpenSourceLocation(row.entry);
|
|
|
|
|
});
|
|
|
|
|
Actions::DrawMenuSeparator();
|
|
|
|
|
Actions::DrawMenuAction(Actions::MakeClearConsoleAction(), [&]() {
|
|
|
|
|
sink->Clear();
|
|
|
|
|
m_selectedSerial = 0;
|
|
|
|
|
m_selectedEntryKey.clear();
|
|
|
|
|
});
|
2026-03-31 23:05:52 +08:00
|
|
|
XCEngine::Editor::UI::EndContextMenu();
|
2026-03-31 21:28:16 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (rows.empty()) {
|
|
|
|
|
UI::DrawEmptyState(
|
2026-04-01 16:40:54 +08:00
|
|
|
searchQuery.Empty() ? "No messages" : "No search results",
|
|
|
|
|
searchQuery.Empty() ? nullptr : "No console entries match the current search",
|
2026-03-31 21:28:16 +08:00
|
|
|
ImVec2(12.0f, 12.0f));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (openSelectedSource && selectedRow != nullptr && !OpenSourceLocation(selectedRow->entry)) {
|
|
|
|
|
CopyToClipboard(*selectedRow);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (UI::BeginContextMenuForWindow("##ConsoleBackgroundContext")) {
|
|
|
|
|
Actions::DrawMenuAction(Actions::MakeClearConsoleAction(), [&]() {
|
|
|
|
|
sink->Clear();
|
|
|
|
|
m_selectedSerial = 0;
|
|
|
|
|
m_selectedEntryKey.clear();
|
|
|
|
|
});
|
2026-03-31 23:05:52 +08:00
|
|
|
XCEngine::Editor::UI::EndContextMenu();
|
2026-03-31 21:28:16 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (hasNewRevision && wasAtBottom) {
|
|
|
|
|
ImGui::SetScrollHereY(1.0f);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ImGui::EndChild();
|
2026-03-31 23:05:52 +08:00
|
|
|
ImGui::PopStyleVar();
|
2026-03-31 21:28:16 +08:00
|
|
|
|
|
|
|
|
const UI::SplitterResult splitter = UI::DrawSplitter("##ConsoleDetailsSplitter", UI::SplitterAxis::Horizontal, splitterThickness);
|
|
|
|
|
if (splitter.active) {
|
|
|
|
|
m_detailsHeight = std::clamp(m_detailsHeight - splitter.delta, minDetailsHeight, maxDetailsHeight);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
selectedRow = ResolveSelectedRow(rows, m_selectedSerial, m_selectedEntryKey);
|
|
|
|
|
if (selectedRow) {
|
|
|
|
|
m_selectedEntryKey = selectedRow->entryKey;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ConsoleDetailsBackgroundColor());
|
|
|
|
|
const bool detailsOpen = ImGui::BeginChild("ConsoleDetails", ImVec2(0.0f, 0.0f), false);
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
if (detailsOpen) {
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ConsoleDetailsHeaderBackgroundColor());
|
|
|
|
|
const bool headerOpen = ImGui::BeginChild(
|
|
|
|
|
"ConsoleDetailsHeader",
|
2026-03-31 23:05:52 +08:00
|
|
|
ImVec2(0.0f, kConsoleDetailsHeaderHeight),
|
2026-03-31 21:28:16 +08:00
|
|
|
false,
|
|
|
|
|
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
if (headerOpen) {
|
|
|
|
|
if (selectedRow) {
|
|
|
|
|
const std::string sourceText = UI::BuildConsoleSourceText(selectedRow->entry);
|
2026-03-31 23:05:52 +08:00
|
|
|
const bool canOpenSource = CanOpenSourceLocation(selectedRow->entry);
|
|
|
|
|
std::string headerText = UI::ConsoleSeverityLabel(selectedRow->entry.level);
|
2026-03-31 21:28:16 +08:00
|
|
|
if (!sourceText.empty()) {
|
2026-03-31 23:05:52 +08:00
|
|
|
headerText += " ";
|
|
|
|
|
headerText += sourceText;
|
2026-03-31 21:28:16 +08:00
|
|
|
}
|
2026-03-31 23:05:52 +08:00
|
|
|
|
|
|
|
|
if (ImGui::BeginTable(
|
|
|
|
|
"##ConsoleDetailsHeaderLayout",
|
|
|
|
|
2,
|
|
|
|
|
ImGuiTableFlags_NoSavedSettings | ImGuiTableFlags_SizingStretchProp))
|
|
|
|
|
{
|
|
|
|
|
ImGui::TableSetupColumn("##Left", ImGuiTableColumnFlags_WidthStretch);
|
|
|
|
|
ImGui::TableSetupColumn("##Right", ImGuiTableColumnFlags_WidthFixed, 128.0f);
|
|
|
|
|
ImGui::TableNextRow();
|
|
|
|
|
|
|
|
|
|
ImGui::TableNextColumn();
|
|
|
|
|
ImGui::AlignTextToFramePadding();
|
|
|
|
|
ImGui::TextUnformatted(headerText.c_str());
|
|
|
|
|
|
|
|
|
|
ImGui::TableNextColumn();
|
|
|
|
|
if (selectedRow->count > 1) {
|
|
|
|
|
ImGui::AlignTextToFramePadding();
|
|
|
|
|
ImGui::TextColored(UI::ConsoleSecondaryTextColor(), "x%zu", selectedRow->count);
|
|
|
|
|
ImGui::SameLine(0.0f, 6.0f);
|
|
|
|
|
}
|
|
|
|
|
if (ImGui::SmallButton("Copy")) {
|
|
|
|
|
CopyToClipboard(*selectedRow);
|
|
|
|
|
}
|
|
|
|
|
ImGui::SameLine(0.0f, 4.0f);
|
|
|
|
|
ImGui::BeginDisabled(!canOpenSource);
|
|
|
|
|
if (ImGui::SmallButton("Open")) {
|
|
|
|
|
OpenSourceLocation(selectedRow->entry);
|
|
|
|
|
}
|
|
|
|
|
ImGui::EndDisabled();
|
|
|
|
|
|
|
|
|
|
ImGui::EndTable();
|
2026-03-31 21:28:16 +08:00
|
|
|
}
|
|
|
|
|
} else {
|
2026-03-31 23:05:52 +08:00
|
|
|
ImGui::TextColored(UI::ConsoleSecondaryTextColor(), "Stack Trace");
|
2026-03-31 21:28:16 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ImGui::EndChild();
|
|
|
|
|
|
|
|
|
|
if (selectedRow) {
|
|
|
|
|
const std::string message = selectedRow->entry.message.CStr();
|
2026-03-31 23:05:52 +08:00
|
|
|
const std::string traceText = BuildDetailsTraceText(*selectedRow);
|
|
|
|
|
|
2026-03-31 21:28:16 +08:00
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(10.0f, 8.0f));
|
2026-03-31 23:05:52 +08:00
|
|
|
const bool bodyOpen = ImGui::BeginChild(
|
|
|
|
|
"ConsoleDetailsBody",
|
|
|
|
|
ImVec2(0.0f, 0.0f),
|
|
|
|
|
false,
|
|
|
|
|
ImGuiWindowFlags_HorizontalScrollbar);
|
|
|
|
|
ImGui::PopStyleVar();
|
2026-03-31 21:28:16 +08:00
|
|
|
if (bodyOpen) {
|
2026-03-31 23:05:52 +08:00
|
|
|
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x);
|
|
|
|
|
ImGui::TextUnformatted(message.c_str());
|
|
|
|
|
ImGui::PopTextWrapPos();
|
2026-03-31 21:28:16 +08:00
|
|
|
ImGui::Spacing();
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
ImGui::Spacing();
|
2026-03-31 23:05:52 +08:00
|
|
|
ImGui::PushStyleColor(ImGuiCol_Text, UI::ConsoleSecondaryTextColor());
|
|
|
|
|
ImGui::TextUnformatted(traceText.c_str());
|
|
|
|
|
ImGui::PopStyleColor();
|
2026-03-31 21:28:16 +08:00
|
|
|
}
|
|
|
|
|
ImGui::EndChild();
|
|
|
|
|
} else {
|
|
|
|
|
UI::PanelContentScope detailsContent("ConsoleDetailsEmpty", ImVec2(10.0f, 8.0f));
|
|
|
|
|
if (detailsContent.IsOpen()) {
|
2026-03-31 23:05:52 +08:00
|
|
|
UI::DrawEmptyState("No message selected", "Select a console entry to inspect its stack trace");
|
2026-03-31 21:28:16 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ImGui::EndChild();
|
|
|
|
|
|
2026-03-26 22:31:22 +08:00
|
|
|
Actions::ObserveInactiveActionRoute(*m_context);
|
2026-03-25 12:30:05 +08:00
|
|
|
}
|
2026-03-20 17:08:06 +08:00
|
|
|
|
2026-03-31 21:28:16 +08:00
|
|
|
} // namespace Editor
|
|
|
|
|
} // namespace XCEngine
|