From ad237cb81efc9b266b9cad3eb39639d4985eb556 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Tue, 31 Mar 2026 21:28:16 +0800 Subject: [PATCH] feat: overhaul editor console panel and diagnostics --- editor/src/Actions/ConsoleActionRouter.h | 20 +- editor/src/Core/EditorConsoleSink.cpp | 48 +- editor/src/Core/EditorConsoleSink.h | 11 +- .../src/Platform/WindowsProcessDiagnostics.h | 56 ++ editor/src/UI/ConsoleLogFormatter.h | 67 ++ editor/src/UI/Core.h | 14 +- editor/src/UI/StyleTokens.h | 70 +- editor/src/panels/ConsolePanel.cpp | 885 +++++++++++++++++- editor/src/panels/ConsolePanel.h | 25 + 9 files changed, 1176 insertions(+), 20 deletions(-) diff --git a/editor/src/Actions/ConsoleActionRouter.h b/editor/src/Actions/ConsoleActionRouter.h index d73bfcfd..76df1854 100644 --- a/editor/src/Actions/ConsoleActionRouter.h +++ b/editor/src/Actions/ConsoleActionRouter.h @@ -9,15 +9,33 @@ namespace XCEngine { namespace Editor { namespace Actions { +inline ActionBinding MakeConsoleCollapseAction(bool active, bool enabled = true) { + return MakeAction("Collapse", nullptr, active, enabled); +} + +inline ActionBinding MakeConsoleClearOnPlayAction(bool active, bool enabled = true) { + return MakeAction("Clear on Play", nullptr, active, enabled); +} + +inline ActionBinding MakeConsoleErrorPauseAction(bool active, bool enabled = true) { + return MakeAction("Error Pause", nullptr, active, enabled); +} + inline void DrawConsoleToolbarActions(Debug::EditorConsoleSink& sink, UI::ConsoleFilterState& filterState) { if (DrawToolbarAction(MakeClearConsoleAction())) { sink.Clear(); } + ImGui::SameLine(); + DrawToolbarToggleAction(MakeConsoleCollapseAction(filterState.Collapse()), filterState.Collapse()); + ImGui::SameLine(); + DrawToolbarToggleAction(MakeConsoleClearOnPlayAction(filterState.ClearOnPlay()), filterState.ClearOnPlay()); + ImGui::SameLine(); + DrawToolbarToggleAction(MakeConsoleErrorPauseAction(filterState.ErrorPause()), filterState.ErrorPause()); ImGui::SameLine(); UI::DrawToolbarLabel("Filter"); ImGui::SameLine(); - DrawToolbarToggleAction(MakeConsoleInfoFilterAction(filterState.ShowInfo()), filterState.ShowInfo()); + DrawToolbarToggleAction(MakeConsoleInfoFilterAction(filterState.ShowLog()), filterState.ShowLog()); ImGui::SameLine(); DrawToolbarToggleAction(MakeConsoleWarningFilterAction(filterState.ShowWarning()), filterState.ShowWarning()); ImGui::SameLine(); diff --git a/editor/src/Core/EditorConsoleSink.cpp b/editor/src/Core/EditorConsoleSink.cpp index d0a07309..11140be5 100644 --- a/editor/src/Core/EditorConsoleSink.cpp +++ b/editor/src/Core/EditorConsoleSink.cpp @@ -21,13 +21,18 @@ EditorConsoleSink::~EditorConsoleSink() { } void EditorConsoleSink::Log(const LogEntry& entry) { - std::lock_guard lock(m_mutex); - if (m_logs.size() >= MAX_LOGS) { - m_logs.erase(m_logs.begin()); + std::function callback; + { + std::lock_guard lock(m_mutex); + if (m_logs.size() >= MAX_LOGS) { + m_logs.erase(m_logs.begin()); + } + m_logs.push_back(EditorConsoleRecord{ m_nextSerial++, entry }); + ++m_revision; + callback = m_callback; } - m_logs.push_back(entry); - if (m_callback) { - m_callback(); + if (callback) { + callback(); } } @@ -35,16 +40,43 @@ void EditorConsoleSink::Flush() { } std::vector EditorConsoleSink::GetLogs() const { + std::lock_guard lock(m_mutex); + std::vector logs; + logs.reserve(m_logs.size()); + for (const EditorConsoleRecord& record : m_logs) { + logs.push_back(record.entry); + } + return logs; +} + +std::vector EditorConsoleSink::GetRecords() const { std::lock_guard lock(m_mutex); return m_logs; } -void EditorConsoleSink::Clear() { +uint64_t EditorConsoleSink::GetRevision() const { std::lock_guard lock(m_mutex); - m_logs.clear(); + return m_revision; +} + +void EditorConsoleSink::Clear() { + std::function callback; + { + std::lock_guard lock(m_mutex); + if (m_logs.empty()) { + return; + } + m_logs.clear(); + ++m_revision; + callback = m_callback; + } + if (callback) { + callback(); + } } void EditorConsoleSink::SetCallback(std::function callback) { + std::lock_guard lock(m_mutex); m_callback = std::move(callback); } diff --git a/editor/src/Core/EditorConsoleSink.h b/editor/src/Core/EditorConsoleSink.h index 22befcf9..3a034874 100644 --- a/editor/src/Core/EditorConsoleSink.h +++ b/editor/src/Core/EditorConsoleSink.h @@ -9,6 +9,11 @@ namespace XCEngine { namespace Debug { +struct EditorConsoleRecord { + uint64_t serial = 0; + LogEntry entry = {}; +}; + class EditorConsoleSink : public ILogSink { public: static EditorConsoleSink* GetInstance(); @@ -20,13 +25,17 @@ public: void Flush() override; std::vector GetLogs() const; + std::vector GetRecords() const; + uint64_t GetRevision() const; void Clear(); void SetCallback(std::function callback); private: mutable std::mutex m_mutex; - std::vector m_logs; + std::vector m_logs; std::function m_callback; + uint64_t m_nextSerial = 1; + uint64_t m_revision = 0; static EditorConsoleSink* s_instance; static constexpr size_t MAX_LOGS = 1000; }; diff --git a/editor/src/Platform/WindowsProcessDiagnostics.h b/editor/src/Platform/WindowsProcessDiagnostics.h index 16eb2b5a..d3e1335c 100644 --- a/editor/src/Platform/WindowsProcessDiagnostics.h +++ b/editor/src/Platform/WindowsProcessDiagnostics.h @@ -2,6 +2,9 @@ #include "Platform/Win32Utf8.h" +#ifdef _MSC_VER +#include +#endif #include #include #include @@ -15,6 +18,46 @@ inline std::string GetExecutableLogPath(const char* fileName) { return GetExecutableDirectoryUtf8() + "\\" + fileName; } +#ifdef _MSC_VER +inline void WriteInvalidParameterReport( + const wchar_t* expression, + const wchar_t* function, + const wchar_t* file, + unsigned int line) { + const std::string logPath = GetExecutableLogPath("crash.log"); + FILE* logFile = nullptr; + fopen_s(&logFile, logPath.c_str(), "a"); + if (logFile != nullptr) { + fwprintf( + logFile, + L"[CRT] Invalid parameter. function=%s file=%s line=%u expression=%s\n", + function != nullptr ? function : L"(null)", + file != nullptr ? file : L"(null)", + line, + expression != nullptr ? expression : L"(null)"); + fclose(logFile); + } + + fwprintf( + stderr, + L"[CRT] Invalid parameter. function=%s file=%s line=%u expression=%s\n", + function != nullptr ? function : L"(null)", + file != nullptr ? file : L"(null)", + line, + expression != nullptr ? expression : L"(null)"); + fflush(stderr); +} + +inline void InvalidParameterHandler( + const wchar_t* expression, + const wchar_t* function, + const wchar_t* file, + unsigned int line, + uintptr_t) { + WriteInvalidParameterReport(expression, function, file, line); +} +#endif + inline void WriteCrashStackTrace(FILE* file) { if (file == nullptr) { return; @@ -84,11 +127,24 @@ inline LONG WINAPI CrashExceptionFilter(EXCEPTION_POINTERS* exceptionPointers) { inline void InstallCrashExceptionFilter() { SetUnhandledExceptionFilter(CrashExceptionFilter); + +#ifdef _MSC_VER + _set_invalid_parameter_handler(InvalidParameterHandler); +#endif } inline void RedirectStderrToExecutableLog() { const std::string stderrPath = GetExecutableLogPath("stderr.log"); freopen(stderrPath.c_str(), "w", stderr); + +#ifdef _MSC_VER + _CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_FILE); + _CrtSetReportFile(_CRT_WARN, _CRTDBG_FILE_STDERR); + _CrtSetReportMode(_CRT_ERROR, _CRTDBG_MODE_FILE); + _CrtSetReportFile(_CRT_ERROR, _CRTDBG_FILE_STDERR); + _CrtSetReportMode(_CRT_ASSERT, _CRTDBG_MODE_FILE); + _CrtSetReportFile(_CRT_ASSERT, _CRTDBG_FILE_STDERR); +#endif } } // namespace Platform diff --git a/editor/src/UI/ConsoleLogFormatter.h b/editor/src/UI/ConsoleLogFormatter.h index 76b6e71e..4dbcd6b2 100644 --- a/editor/src/UI/ConsoleLogFormatter.h +++ b/editor/src/UI/ConsoleLogFormatter.h @@ -4,6 +4,7 @@ #include #include +#include #include namespace XCEngine { @@ -26,11 +27,77 @@ inline const char* ConsoleLogPrefix(::XCEngine::Debug::LogLevel level) { return "[LOG] "; } +inline const char* ConsoleSeverityLabel(::XCEngine::Debug::LogLevel level) { + switch (level) { + case ::XCEngine::Debug::LogLevel::Verbose: + case ::XCEngine::Debug::LogLevel::Debug: + case ::XCEngine::Debug::LogLevel::Info: + return "Log"; + case ::XCEngine::Debug::LogLevel::Warning: + return "Warning"; + case ::XCEngine::Debug::LogLevel::Error: + case ::XCEngine::Debug::LogLevel::Fatal: + return "Error"; + } + + return "Log"; +} + +inline std::string BuildConsoleSummaryText(const ::XCEngine::Debug::LogEntry& log) { + return log.message.CStr(); +} + +inline std::string BuildConsoleSourceText(const ::XCEngine::Debug::LogEntry& log) { + std::string source; + const char* category = ::XCEngine::Debug::LogCategoryToString(log.category); + if (category && category[0] != '\0') { + source += "["; + source += category; + source += "]"; + } + + const std::string filePath = log.file.CStr(); + if (!filePath.empty()) { + if (!source.empty()) { + source += " "; + } + + source += std::filesystem::path(filePath).filename().string(); + if (log.line > 0) { + source += ":"; + source += std::to_string(log.line); + } + } + + return source; +} + inline std::string BuildConsoleLogText(const ::XCEngine::Debug::LogEntry& log) { const char* category = ::XCEngine::Debug::LogCategoryToString(log.category); return std::string(ConsoleLogPrefix(log.level)) + "[" + category + "] " + log.message.CStr(); } +inline std::string BuildConsoleCopyText(const ::XCEngine::Debug::LogEntry& log) { + std::string text = BuildConsoleLogText(log); + if (log.file.Length() > 0) { + text += "\nFile: "; + text += log.file.CStr(); + if (log.line > 0) { + text += ":"; + text += std::to_string(log.line); + } + } + if (log.function.Length() > 0) { + text += "\nFunction: "; + text += log.function.CStr(); + } + text += "\nThread: "; + text += std::to_string(log.threadId); + text += "\nTimestamp: "; + text += std::to_string(log.timestamp); + return text; +} + } // namespace UI } // namespace Editor } // namespace XCEngine diff --git a/editor/src/UI/Core.h b/editor/src/UI/Core.h index d8ff8857..be3040c2 100644 --- a/editor/src/UI/Core.h +++ b/editor/src/UI/Core.h @@ -39,24 +39,24 @@ inline void DrawDisclosureArrow(ImDrawList* drawList, const ImVec2& min, const I return; } + constexpr float kSqrt3 = 1.7320508f; const ImVec2 center( static_cast(std::floor((min.x + max.x) * 0.5f)) + 0.5f, static_cast(std::floor((min.y + max.y) * 0.5f)) + 0.5f); const float width = max.x - min.x; const float height = max.y - min.y; - const float radius = (std::max)( + const float halfBaseExtent = (std::max)( 3.0f, static_cast(std::floor((width < height ? width : height) * DisclosureArrowScale()))); - const float baseHalfExtent = radius; - const float tipExtent = (std::max)(2.0f, static_cast(std::floor(radius * 0.70f))); - if (baseHalfExtent < 1.0f || tipExtent < 1.0f) { + const float triangleHeight = kSqrt3 * halfBaseExtent; + if (halfBaseExtent < 1.0f || triangleHeight < 1.0f) { return; } ImVec2 points[3] = { - ImVec2(-baseHalfExtent, -tipExtent), - ImVec2(baseHalfExtent, -tipExtent), - ImVec2(0.0f, tipExtent) + ImVec2(-halfBaseExtent, -triangleHeight / 3.0f), + ImVec2(halfBaseExtent, -triangleHeight / 3.0f), + ImVec2(0.0f, triangleHeight * 2.0f / 3.0f) }; if (!open) { diff --git a/editor/src/UI/StyleTokens.h b/editor/src/UI/StyleTokens.h index d4f3e2d2..f74b5b1a 100644 --- a/editor/src/UI/StyleTokens.h +++ b/editor/src/UI/StyleTokens.h @@ -269,7 +269,7 @@ inline float NavigationTreePrefixLabelGap() { } inline float DisclosureArrowScale() { - return 0.28f; + return 0.18f; } inline ImVec4 NavigationTreePrefixColor(bool selected = false, bool hovered = false) { @@ -704,6 +704,74 @@ inline ImVec4 ConsoleRowHoverFillColor() { return ImVec4(1.0f, 1.0f, 1.0f, 0.03f); } +inline ImVec4 ConsoleRowSelectedFillColor() { + return ImVec4(0.26f, 0.33f, 0.43f, 0.95f); +} + +inline ImVec4 ConsoleListBackgroundColor() { + return ImVec4(0.205f, 0.205f, 0.205f, 1.0f); +} + +inline ImVec4 ConsoleDetailsBackgroundColor() { + return ImVec4(0.185f, 0.185f, 0.185f, 1.0f); +} + +inline ImVec4 ConsoleDetailsHeaderBackgroundColor() { + return ImVec4(0.19f, 0.19f, 0.19f, 1.0f); +} + +inline ImVec4 ConsoleSecondaryTextColor() { + return ImVec4(0.57f, 0.57f, 0.57f, 1.0f); +} + +inline ImVec4 ConsoleCountBadgeBackgroundColor() { + return ImVec4(0.29f, 0.29f, 0.29f, 1.0f); +} + +inline ImVec4 ConsoleCountBadgeTextColor() { + return ImVec4(0.88f, 0.88f, 0.88f, 1.0f); +} + +inline ImVec4 ConsoleLogColor() { + return ImVec4(0.77f, 0.77f, 0.77f, 1.0f); +} + +inline ImVec4 ConsoleWarningColor() { + return ImVec4(0.93f, 0.72f, 0.26f, 1.0f); +} + +inline ImVec4 ConsoleErrorColor() { + return ImVec4(0.92f, 0.37f, 0.33f, 1.0f); +} + +inline float ConsoleRowHeight() { + return 22.0f; +} + +inline float ConsoleSeverityButtonMinWidth() { + return 54.0f; +} + +inline ImVec2 ConsoleSeverityButtonPadding() { + return ImVec2(8.0f, 4.0f); +} + +inline float ConsoleBadgeRounding() { + return 3.0f; +} + +inline float ConsoleDetailsDefaultHeight() { + return 168.0f; +} + +inline float ConsoleDetailsMinHeight() { + return 92.0f; +} + +inline float ConsoleListMinHeight() { + return 96.0f; +} + inline float MenuBarStatusRightPadding() { return 20.0f; } diff --git a/editor/src/panels/ConsolePanel.cpp b/editor/src/panels/ConsolePanel.cpp index b23da1dc..c19dc525 100644 --- a/editor/src/panels/ConsolePanel.cpp +++ b/editor/src/panels/ConsolePanel.cpp @@ -1,7 +1,481 @@ #include "Actions/ActionRouting.h" +#include "Actions/ConsoleActionRouter.h" #include "ConsolePanel.h" +#include "Core/EditorConsoleSink.h" +#include "Core/EditorEvents.h" +#include "Core/EventBus.h" +#include "Core/IEditorContext.h" +#include "Platform/Win32Utf8.h" #include "UI/UI.h" + #include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +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; +}; + +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; +}; + +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 ToLowerCopy(std::string text) { + std::transform(text.begin(), text.end(), text.begin(), [](unsigned char ch) { + return static_cast(std::tolower(ch)); + }); + return text; +} + +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(entry.level)); + key.push_back('\x1f'); + key += std::to_string(static_cast(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(); + return ToLowerCopy(std::move(haystack)); +} + +bool MatchesSearch(const LogEntry& entry, const std::string& searchText) { + if (searchText.empty()) { + return true; + } + + return BuildSearchHaystack(entry).find(searchText) != std::string::npos; +} + +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& records) { + uint64_t latestSerial = 0; + for (const EditorConsoleRecord& record : records) { + latestSerial = (std::max)(latestSerial, record.serial); + } + return latestSerial; +} + +std::vector BuildVisibleRows( + const std::vector& records, + const XCEngine::Editor::UI::ConsoleFilterState& filterState, + const std::string& searchText, + ConsoleSeverityCounts& counts) { + std::vector rows; + rows.reserve(records.size()); + + if (!filterState.Collapse()) { + for (const EditorConsoleRecord& record : records) { + CountSeverity(counts, record.entry.level); + if (!filterState.Allows(record.entry.level) || !MatchesSearch(record.entry, searchText)) { + continue; + } + + rows.push_back(ConsoleRowData{ + record.serial, + record.entry, + BuildEntryKey(record.entry), + 1u + }); + } + return rows; + } + + std::unordered_map rowIndicesByKey; + rowIndicesByKey.reserve(records.size()); + for (const EditorConsoleRecord& record : records) { + CountSeverity(counts, record.entry.level); + if (!filterState.Allows(record.entry.level) || !MatchesSearch(record.entry, searchText)) { + 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& 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& rows, uint64_t& selectedSerial, int delta) { + if (rows.empty() || delta == 0) { + return false; + } + + int currentIndex = -1; + for (int i = 0; i < static_cast(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(rows.size()) - 1); + } else if (delta > 0) { + nextIndex = 0; + } else { + nextIndex = static_cast(rows.size()) - 1; + } + + if (rows[nextIndex].serial == selectedSerial) { + return false; + } + + selectedSerial = rows[nextIndex].serial; + return true; +} + +void DrawSeverityIcon(ImDrawList* drawList, const ImVec2& center, float radius, ConsoleSeverityVisual severity) { + 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(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; + } +} + +bool DrawSeverityToggleButton( + const char* id, + ConsoleSeverityVisual severity, + size_t count, + bool& active, + const char* tooltip) { + const std::string countText = std::to_string(count); + const ImVec2 countSize = ImGui::CalcTextSize(countText.c_str()); + const ImVec2 padding = XCEngine::Editor::UI::ConsoleSeverityButtonPadding(); + const float iconRadius = 5.0f; + const float gap = 8.0f; + const ImVec2 size( + (std::max)( + XCEngine::Editor::UI::ConsoleSeverityButtonMinWidth(), + padding.x * 2.0f + iconRadius * 2.0f + gap + countSize.x), + ImGui::GetFrameHeight()); + + 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); + DrawSeverityIcon(drawList, iconCenter, iconRadius, severity); + 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; +} + +ConsoleRowInteraction DrawConsoleRow(const ConsoleRowData& row, bool selected) { + ConsoleRowInteraction interaction; + + const float rowHeight = XCEngine::Editor::UI::ConsoleRowHeight(); + 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())); + } + + const ConsoleSeverityVisual severity = ResolveSeverity(row.entry.level); + const ImVec2 iconCenter(min.x + 12.0f, min.y + rowHeight * 0.5f); + DrawSeverityIcon(drawList, iconCenter, 5.0f, severity); + + float rightEdge = max.x - 8.0f; + if (row.count > 1) { + const std::string countText = std::to_string(row.count); + const ImVec2 countSize = ImGui::CalcTextSize(countText.c_str()); + const ImVec2 badgeMin(rightEdge - countSize.x - 10.0f, min.y + 3.0f); + const ImVec2 badgeMax(rightEdge, max.y - 3.0f); + drawList->AddRectFilled( + badgeMin, + badgeMax, + ImGui::GetColorU32(XCEngine::Editor::UI::ConsoleCountBadgeBackgroundColor()), + XCEngine::Editor::UI::ConsoleBadgeRounding()); + 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()); + rightEdge = badgeMin.x - 8.0f; + } + + const std::string sourceText = XCEngine::Editor::UI::BuildConsoleSourceText(row.entry); + if (!sourceText.empty()) { + const ImVec2 sourceSize = ImGui::CalcTextSize(sourceText.c_str()); + const float sourceWidth = (std::min)(sourceSize.x, availableWidth * 0.32f); + const ImVec2 sourceMin(rightEdge - sourceWidth, min.y); + const ImVec2 sourceMax(rightEdge, max.y); + drawList->PushClipRect(sourceMin, sourceMax, true); + drawList->AddText( + ImVec2(sourceMin.x, min.y + (rowHeight - sourceSize.y) * 0.5f), + ImGui::GetColorU32(XCEngine::Editor::UI::ConsoleSecondaryTextColor()), + sourceText.c_str()); + drawList->PopClipRect(); + rightEdge = sourceMin.x - 10.0f; + } + + const std::string summary = XCEngine::Editor::UI::BuildConsoleSummaryText(row.entry); + const ImVec2 textMin(min.x + 24.0f, min.y); + 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(); + + return interaction; +} + +std::string FormatTimestamp(uint64_t timestamp) { + if (timestamp == 0) { + return {}; + } + + const std::time_t seconds = static_cast(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(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()); +} + +} // namespace namespace XCEngine { namespace Editor { @@ -9,14 +483,421 @@ namespace Editor { ConsolePanel::ConsolePanel() : Panel("Console") { } +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( + [this](const PlayModeStartedEvent&) { + HandlePlayModeStarted(); + }); + } + if (!m_playModeStoppedHandlerId) { + m_playModeStoppedHandlerId = m_context->GetEventBus().Subscribe( + [this](const PlayModeStoppedEvent&) { + HandlePlayModeStopped(); + }); + } + if (!m_playModePausedHandlerId) { + m_playModePausedHandlerId = m_context->GetEventBus().Subscribe( + [this](const PlayModePausedEvent&) { + HandlePlayModePaused(); + }); + } +} + +void ConsolePanel::OnDetach() { + if (!m_context) { + return; + } + + if (m_playModeStartedHandlerId) { + m_context->GetEventBus().Unsubscribe(m_playModeStartedHandlerId); + m_playModeStartedHandlerId = 0; + } + if (m_playModeStoppedHandlerId) { + m_context->GetEventBus().Unsubscribe(m_playModeStoppedHandlerId); + m_playModeStoppedHandlerId = 0; + } + if (m_playModePausedHandlerId) { + m_context->GetEventBus().Unsubscribe(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; +} + void ConsolePanel::Render() { UI::PanelWindowScope panel(m_name.c_str()); if (!panel.IsOpen()) { return; } + 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 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); + } + + const std::string searchText = ToLowerCopy(std::string(m_searchBuffer)); + ConsoleSeverityCounts counts; + std::vector rows = BuildVisibleRows(records, m_filterState, searchText, counts); + 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; + } + } + + UI::PanelToolbarScope toolbar( + "ConsoleToolbar", + UI::StandardPanelToolbarHeight(), + ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse, + true, + UI::ToolbarPadding(), + UI::ToolbarItemSpacing(), + UI::ToolbarBackgroundColor()); + if (toolbar.IsOpen() && + ImGui::BeginTable( + "##ConsoleToolbarLayout", + 3, + ImGuiTableFlags_NoSavedSettings | ImGuiTableFlags_SizingStretchProp)) + { + ImGui::TableSetupColumn("##Primary", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("##Search", ImGuiTableColumnFlags_WidthFixed, 240.0f); + ImGui::TableSetupColumn("##Severity", ImGuiTableColumnFlags_WidthFixed, 250.0f); + ImGui::TableNextRow(); + + ImGui::TableNextColumn(); + if (Actions::DrawToolbarAction(Actions::MakeClearConsoleAction())) { + sink->Clear(); + m_selectedSerial = 0; + m_selectedEntryKey.clear(); + } + ImGui::SameLine(); + Actions::DrawToolbarToggleAction( + Actions::MakeConsoleCollapseAction(m_filterState.Collapse()), + m_filterState.Collapse()); + ImGui::SameLine(); + Actions::DrawToolbarToggleAction( + Actions::MakeConsoleClearOnPlayAction(m_filterState.ClearOnPlay()), + m_filterState.ClearOnPlay()); + ImGui::SameLine(); + Actions::DrawToolbarToggleAction( + Actions::MakeConsoleErrorPauseAction(m_filterState.ErrorPause()), + m_filterState.ErrorPause()); + + ImGui::TableNextColumn(); + if (m_requestSearchFocus) { + ImGui::SetKeyboardFocusHere(); + m_requestSearchFocus = false; + } + UI::ToolbarSearchField("##ConsoleSearch", "Search", m_searchBuffer, sizeof(m_searchBuffer)); + + ImGui::TableNextColumn(); + DrawSeverityToggleButton("##ConsoleLogFilter", ConsoleSeverityVisual::Log, counts.logCount, m_filterState.ShowLog(), "Log"); + ImGui::SameLine(0.0f, 6.0f); + DrawSeverityToggleButton("##ConsoleWarningFilter", ConsoleSeverityVisual::Warning, counts.warningCount, m_filterState.ShowWarning(), "Warnings"); + ImGui::SameLine(0.0f, 6.0f); + DrawSeverityToggleButton("##ConsoleErrorFilter", ConsoleSeverityVisual::Error, counts.errorCount, m_filterState.ShowError(), "Errors"); + + ImGui::EndTable(); + } + + 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); + + 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; + clipper.Begin(static_cast(rows.size()), UI::ConsoleRowHeight()); + while (clipper.Step()) { + for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; ++i) { + ConsoleRowData& row = rows[static_cast(i)]; + ImGui::PushID(static_cast(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(); + }); + UI::EndContextMenu(); + } + + ImGui::PopID(); + } + } + + if (rows.empty()) { + UI::DrawEmptyState( + searchText.empty() ? "No messages" : "No search results", + searchText.empty() ? nullptr : "No console entries match the current search", + 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(); + }); + UI::EndContextMenu(); + } + + if (hasNewRevision && wasAtBottom) { + ImGui::SetScrollHereY(1.0f); + } + } + ImGui::EndChild(); + + 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", + ImVec2(0.0f, 28.0f), + false, + ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); + ImGui::PopStyleColor(); + if (headerOpen) { + if (selectedRow) { + const char* severityLabel = UI::ConsoleSeverityLabel(selectedRow->entry.level); + const std::string sourceText = UI::BuildConsoleSourceText(selectedRow->entry); + ImGui::Text("%s", severityLabel); + if (!sourceText.empty()) { + ImGui::SameLine(); + ImGui::TextColored(UI::ConsoleSecondaryTextColor(), "%s", sourceText.c_str()); + } + if (selectedRow->count > 1) { + const std::string countLabel = "x" + std::to_string(selectedRow->count); + ImGui::SameLine(); + UI::DrawRightAlignedText(countLabel.c_str(), UI::ConsoleSecondaryTextColor(), 8.0f); + } + } else { + ImGui::TextColored(UI::ConsoleSecondaryTextColor(), "Details"); + } + } + ImGui::EndChild(); + + if (selectedRow) { + const std::string message = selectedRow->entry.message.CStr(); + const std::string fileText = selectedRow->entry.file.CStr(); + const std::string functionText = selectedRow->entry.function.CStr(); + const std::string timeText = FormatTimestamp(selectedRow->entry.timestamp); + const std::string sourceText = UI::BuildConsoleSourceText(selectedRow->entry); + const std::string threadText = std::to_string(selectedRow->entry.threadId); + const std::string lineText = + selectedRow->entry.line > 0 ? std::to_string(selectedRow->entry.line) : std::string(); + + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(6.0f, 6.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(10.0f, 8.0f)); + const bool bodyOpen = ImGui::BeginChild("ConsoleDetailsBody", ImVec2(0.0f, 0.0f), false); + ImGui::PopStyleVar(2); + if (bodyOpen) { + ImGui::TextWrapped("%s", message.c_str()); + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + if (ImGui::BeginTable("##ConsoleDetailsMeta", 2, ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_NoSavedSettings)) { + auto drawMetaRow = [](const char* label, const std::string& value) { + if (value.empty()) { + return; + } + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::TextColored(XCEngine::Editor::UI::ConsoleSecondaryTextColor(), "%s", label); + ImGui::TableNextColumn(); + ImGui::TextWrapped("%s", value.c_str()); + }; + + drawMetaRow("Type", UI::ConsoleSeverityLabel(selectedRow->entry.level)); + drawMetaRow("Category", XCEngine::Debug::LogCategoryToString(selectedRow->entry.category)); + drawMetaRow("Source", sourceText); + drawMetaRow("File", fileText); + drawMetaRow("Line", lineText); + drawMetaRow("Function", functionText); + drawMetaRow("Time", timeText); + drawMetaRow("Thread", threadText); + ImGui::EndTable(); + } + + ImGui::Spacing(); + if (ImGui::Button("Copy", ImVec2(72.0f, 0.0f))) { + CopyToClipboard(*selectedRow); + } + ImGui::SameLine(); + ImGui::BeginDisabled(!CanOpenSourceLocation(selectedRow->entry)); + if (ImGui::Button("Open Source", ImVec2(96.0f, 0.0f))) { + OpenSourceLocation(selectedRow->entry); + } + ImGui::EndDisabled(); + } + ImGui::EndChild(); + } else { + UI::PanelContentScope detailsContent("ConsoleDetailsEmpty", ImVec2(10.0f, 8.0f)); + if (detailsContent.IsOpen()) { + UI::DrawEmptyState("No message selected"); + } + } + } + ImGui::EndChild(); + Actions::ObserveInactiveActionRoute(*m_context); } -} -} +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/panels/ConsolePanel.h b/editor/src/panels/ConsolePanel.h index 1a3682c8..a23f60f5 100644 --- a/editor/src/panels/ConsolePanel.h +++ b/editor/src/panels/ConsolePanel.h @@ -1,6 +1,10 @@ #pragma once #include "Panel.h" +#include "UI/ConsoleFilterState.h" + +#include +#include namespace XCEngine { namespace Editor { @@ -8,7 +12,28 @@ namespace Editor { class ConsolePanel : public Panel { public: ConsolePanel(); + void OnAttach() override; + void OnDetach() override; void Render() override; + +private: + void HandlePlayModeStarted(); + void HandlePlayModeStopped(); + void HandlePlayModePaused(); + + UI::ConsoleFilterState m_filterState; + char m_searchBuffer[256] = ""; + uint64_t m_selectedSerial = 0; + uint64_t m_lastSeenRevision = 0; + uint64_t m_lastErrorPauseScanSerial = 0; + uint64_t m_playModeStartedHandlerId = 0; + uint64_t m_playModeStoppedHandlerId = 0; + uint64_t m_playModePausedHandlerId = 0; + std::string m_selectedEntryKey; + float m_detailsHeight = 0.0f; + bool m_playModeActive = false; + bool m_playModePaused = false; + bool m_requestSearchFocus = false; }; }