From 7ff5cd4cf2a471cb66d3244ef28aec2046b239d4 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Tue, 31 Mar 2026 23:05:52 +0800 Subject: [PATCH] Refine console panel to match Unity --- editor/resources/Icons/console__info_icon.png | Bin 0 -> 862 bytes .../Icons/console_error_button_icon.png | Bin 0 -> 351 bytes editor/resources/Icons/console_error_icon.png | Bin 0 -> 432 bytes .../Icons/console_info_button_icon.png | Bin 0 -> 511 bytes .../Icons/console_warn_button_icon.png | Bin 0 -> 486 bytes editor/resources/Icons/console_warn_icon.png | Bin 0 -> 624 bytes editor/src/panels/ConsolePanel.cpp | 615 ++++++++++++++---- 7 files changed, 503 insertions(+), 112 deletions(-) create mode 100644 editor/resources/Icons/console__info_icon.png create mode 100644 editor/resources/Icons/console_error_button_icon.png create mode 100644 editor/resources/Icons/console_error_icon.png create mode 100644 editor/resources/Icons/console_info_button_icon.png create mode 100644 editor/resources/Icons/console_warn_button_icon.png create mode 100644 editor/resources/Icons/console_warn_icon.png diff --git a/editor/resources/Icons/console__info_icon.png b/editor/resources/Icons/console__info_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f17c65b1cc99a9417302748fc473472d6c506b97 GIT binary patch literal 862 zcmV-k1EKthP)kdg0009eNkl?n|t1y_lBU*pchA_mn-1}utIAh zj@kY=l%m4MKs$GkLrN)`nE@gqA%x%!8o&DbKY-w2_H%`4IO&+Dv9-L;?g7$?)KV-w zomgQ_r*#?*%#{QLPqWX=)KYhs>tykpmM0%SNwQ|6vA{DWAb5^j(iwmA!Sg7o=+JP) zZljsN!+$+(>+Q#R^HNV;H@1P*%>>oT| zOI$I~z6Ib8!|3iRPFw~CqfD_D-Cc&k9XY_DmmJlk+PHE{Er@Zln#eI&pPk^Slr0|B znpatBp;|mrq8R{$^h(*-sxmpjU^tb4##Sk%7XSoTGMCq>OfE|;idVwP@=hI6>v~MB=k=JAo}CH@spP) zi%s_}HDAA8C8bDR&P@Os*Y(Fwumin#q3dFN>o8q?P3uXH4i(xiTB=nG_ZJPM&%UwT z2Q;kNh9k>U`NmG%#a(n3ZeVP5PSb5ed!+>sn4>Yf?Lnvzq0%ll6Ps5S@@^y`@Qh|? z*S*o=AVr^G)gBF6_9$B}RbV92E3~HhAJOp!yQ5({#CkZ95`1pr_q+Pg?izwPhm zzl`y`eN$xgGMMSaYGnS=)M(YJ^HA2OCuif}(8nG9+K5cEhR*nEd_ZO?cw0BG-6)zM ztI+q*g#WWh69j1vjc8pkdg0003dNklE!WG9{wduR(yGmzfEB=E`S}o1+upYVSkP_;wy4`rAn_z1us| z=VK-;Gd*Wkz|Q{swg*0+PJ7oXM1Aty;^_ncMbRENVVUg~4M3G}dpCjAHlj)zfbP8m z00mY%vF#ipGmmezlfwvr2&_7)`cqY4brOJltCIlm&-4nwz!q*Cxd;6Ml=qVX28A9r z6v4Vm9jWxBht%yu^%*FxNXF~K`XFN#t{MOlQGSsR<{}#Nol_YD-ye8)eP_DFk2p`d x+R}d0RHxZ`>nU%JVnvFWdnYAU-S+8q4Z{%;u?!5jA|mYG-s{}lvJepCyu9%7@!+|+*m_)=I!t1@$=^O=J)yK z`{n!V_vhN>T4(?O0L4i}K~y-)#go|%f-n$8+1x5+FCao~2Ppji7a9|2F(so9Cf>*1 zlg{oqKa=Y{#469VeLoNz1wQWE#Oh0cT|)bZOqD$hEf!SI38t(hzhWYb~Bm zt&a$adQ1B7%&!$6(a(ulCe@DfUN4ek{EDnnmY!F zsck2L0dk%t2B?nn&Q8*_8aSmKtX6M^^#kdg0005TNklY5Qg8qoFEn{a*}|CNE(X}TZIr&5iHV+rG#v)5vHpW5=8&zhMx5C9&Bt%lp}0Dy3{JZv7<04~={CqkZ<>~OgP;8E(}Sjf|g zBzaTvIY$)0;5cZ3Di*Q)0<@(a6vj%{FdKBh5BV)D(#%R)$gL; z+HC>FK>I^W;L6!hPQkR<7<14((wfPxgR5@sQZY~rv`gCl*@H)NBIWOg%_9K7YbMfX zB28_whY$|nOQZX#xbs0gLUAtCc80Cj08LX zZRNG0FB2_%(am8CVDCR literal 0 HcmV?d00001 diff --git a/editor/resources/Icons/console_warn_button_icon.png b/editor/resources/Icons/console_warn_button_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..72eefdc4d16ae1be6923b6f596ead143cd9cadfa GIT binary patch literal 486 zcmV@P)kdg00054Nkl;G)H!iJ{spK5ThysTC19aLmM&l^O8WyM z1`q>FqtqEfLYl+SgxIlf6nE*u9nNsix$pa&b1h%E@1^vqx7+Fz#mmFq4kGiB+eK{wG{$qro_`p0q{RLT7hnbhbaPHY|r{$*L;a*W;curDC7mGK;j3+ z&)ip|6oQ050s%|MLF}KklomdlB(JFD7Tf}@#J>N*NeFx^FZ+XAI_Mqr+!8Apj9Y(c zWcn5RADx&$nm>dm&3rQ-o+LA3|ER`p;oUFsGQI}ahmWH(FtR=;qGBzmU3Hbo|4Qxf cfBjp30UKh1?DXhbzyJUM07*qoM6N<$f~(EgO8@`> literal 0 HcmV?d00001 diff --git a/editor/resources/Icons/console_warn_icon.png b/editor/resources/Icons/console_warn_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..12a6a735085454a3a7284c7219a37a5838edab27 GIT binary patch literal 624 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dy&jWlyT!HlISq!IUFr1psaC!#A zc`*Cr42CBQ8BWh+I60l+!fcQ%Py|T+KL8SWyZ|Kie=ozm?F^@YjQt?Ndpp2}fX(^8 z2BiK5ketr&e>KDPRbag|6!Cuc#qXJ;{-nZ>ZWfx+97!N-!}%uI%O zcLqlThB!BdXcvZbUyvasVGIok3=Re$2X|*MIOsFXC}LPr&0w$3u)c+1cQ1pT9>bm< zhIluI3v(Ej)-d>3Fvy-gJ|E~+y^o1Px znkJTS?A0vnjVx6x4KF^t`sn<|U7}uP6$1k!m#2$kh{fr%6C=Z$0z_IfH+i0Nn|v!+}eRr$F<$q@0r?pqR{*K~|xUa!rCYYjK ze_yY?Vcz;1j9j9-MJHqme%TwtXnN0Hs-?ElUgPk;lz*ER%6Avo_a!Zv%dF>#eCV08N8`a(z4u?u-_Cy`EQ3 literal 0 HcmV?d00001 diff --git a/editor/src/panels/ConsolePanel.cpp b/editor/src/panels/ConsolePanel.cpp index c19dc525..4e750381 100644 --- a/editor/src/panels/ConsolePanel.cpp +++ b/editor/src/panels/ConsolePanel.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -52,6 +53,65 @@ struct ConsoleRowInteraction { bool doubleClicked = false; }; +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(); + } +} + ConsoleSeverityVisual ResolveSeverity(LogLevel level) { switch (level) { case LogLevel::Warning: @@ -257,7 +317,22 @@ bool SelectRelativeRow(const std::vector& rows, uint64_t& select return true; } -void DrawSeverityIcon(ImDrawList* drawList, const ImVec2& center, float radius, ConsoleSeverityVisual severity) { +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; + } + const ImU32 color = ImGui::GetColorU32(SeverityColor(severity)); switch (severity) { case ConsoleSeverityVisual::Warning: @@ -283,22 +358,256 @@ void DrawSeverityIcon(ImDrawList* drawList, const ImVec2& center, float radius, } } +void DrawSearchGlyph(ImDrawList* drawList, const ImVec2& center, ImU32 color) { + drawList->AddCircle(center, 4.0f, color, 16, 1.5f); + drawList->AddLine( + ImVec2(center.x + 3.0f, center.y + 3.0f), + ImVec2(center.x + 7.0f, center.y + 7.0f), + color, + 1.5f); +} + +bool DrawToolbarDropdownButton( + const char* id, + const char* label, + float width, + const std::function& 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& 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; +} + +void DrawToolbarArrowDropdownButton( + const char* id, + float width, + const std::function& 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(); + } +} + +bool DrawConsoleSearchField(const char* id, char* buffer, size_t bufferSize) { + const float originalCursorY = ImGui::GetCursorPosY(); + ImGui::SetCursorPosY((std::max)(0.0f, originalCursorY - 1.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(22.0f, 1.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 2.0f); + ImGui::PushStyleColor(ImGuiCol_FrameBg, XCEngine::Editor::UI::ToolbarButtonColor(false)); + ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, XCEngine::Editor::UI::ToolbarButtonHoveredColor(false)); + ImGui::PushStyleColor(ImGuiCol_FrameBgActive, XCEngine::Editor::UI::ToolbarButtonActiveColor()); + ImGui::SetNextItemWidth((std::max)(0.0f, ImGui::GetContentRegionAvail().x)); + const bool changed = ImGui::InputTextWithHint(id, "Search", buffer, bufferSize); + const ImVec2 min = ImGui::GetItemRectMin(); + const ImVec2 max = ImGui::GetItemRectMax(); + ImDrawList* drawList = ImGui::GetWindowDrawList(); + DrawSearchGlyph( + drawList, + ImVec2(min.x + 11.0f, (min.y + max.y) * 0.5f), + ImGui::GetColorU32(XCEngine::Editor::UI::ConsoleSecondaryTextColor())); + ImGui::PopStyleColor(3); + ImGui::PopStyleVar(2); + return changed; +} + bool DrawSeverityToggleButton( const char* id, ConsoleSeverityVisual severity, size_t count, bool& active, const char* tooltip) { + const float originalCursorY = ImGui::GetCursorPosY(); + ImGui::SetCursorPosY((std::max)(0.0f, originalCursorY - 1.0f)); + 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 padding(6.0f, 3.0f); + const float iconRadius = 6.0f; + const float gap = 4.0f; + const float buttonHeight = kConsoleToolbarButtonHeight + 2.0f; const ImVec2 size( (std::max)( - XCEngine::Editor::UI::ConsoleSeverityButtonMinWidth(), + kConsoleCounterWidth, padding.x * 2.0f + iconRadius * 2.0f + gap + countSize.x), - ImGui::GetFrameHeight()); + buttonHeight); ImGui::InvisibleButton(id, size); const bool hovered = ImGui::IsItemHovered(); @@ -318,7 +627,7 @@ bool DrawSeverityToggleButton( 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); + DrawSeverityIcon(drawList, iconCenter, iconRadius, severity, true); drawList->AddText( ImVec2(iconCenter.x + iconRadius + gap, min.y + (size.y - countSize.y) * 0.5f), ImGui::GetColorU32(ImGuiCol_Text), @@ -331,10 +640,21 @@ bool DrawSeverityToggleButton( return pressed; } +float CalculateSeverityToggleButtonWidth(size_t count) { + const std::string countText = std::to_string(count); + const ImVec2 countSize = ImGui::CalcTextSize(countText.c_str()); + const ImVec2 padding(6.0f, 3.0f); + const float iconRadius = 6.0f; + const float gap = 4.0f; + return (std::max)( + kConsoleCounterWidth, + padding.x * 2.0f + iconRadius * 2.0f + gap + countSize.x); +} + ConsoleRowInteraction DrawConsoleRow(const ConsoleRowData& row, bool selected) { ConsoleRowInteraction interaction; - const float rowHeight = XCEngine::Editor::UI::ConsoleRowHeight(); + const float rowHeight = kConsoleRowHeight; const float availableWidth = (std::max)(ImGui::GetContentRegionAvail().x, 1.0f); ImGui::InvisibleButton("##ConsoleRow", ImVec2(availableWidth, rowHeight)); interaction.clicked = ImGui::IsItemClicked(ImGuiMouseButton_Left); @@ -350,48 +670,37 @@ ConsoleRowInteraction DrawConsoleRow(const ConsoleRowData& row, bool selected) { } else if (hovered) { drawList->AddRectFilled(min, max, ImGui::GetColorU32(XCEngine::Editor::UI::ConsoleRowHoverFillColor())); } + 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))); 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); + const ImVec2 iconCenter(min.x + 10.0f, min.y + rowHeight * 0.5f); + DrawSeverityIcon(drawList, iconCenter, 4.5f, severity); - float rightEdge = max.x - 8.0f; + float rightEdge = max.x - 6.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 badgeMin(rightEdge - countSize.x - 8.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()); + 2.0f); 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; + rightEdge = badgeMin.x - 6.0f; } const std::string summary = XCEngine::Editor::UI::BuildConsoleSummaryText(row.entry); - const ImVec2 textMin(min.x + 24.0f, min.y); + const ImVec2 textMin(min.x + 22.0f, min.y); const ImVec2 textMax((std::max)(textMin.x, rightEdge), max.y); drawList->PushClipRect(textMin, textMax, true); drawList->AddText( @@ -400,6 +709,32 @@ ConsoleRowInteraction DrawConsoleRow(const ConsoleRowData& row, bool selected) { summary.c_str()); drawList->PopClipRect(); + 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(); + } + return interaction; } @@ -475,6 +810,56 @@ void CopyToClipboard(const ConsoleRowData& row) { ImGui::SetClipboardText(copyText.c_str()); } +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() ? "" : 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; +} + } // namespace namespace XCEngine { @@ -640,14 +1025,20 @@ void ConsolePanel::Render() { } } + const float logFilterWidth = CalculateSeverityToggleButtonWidth(counts.logCount); + const float warningFilterWidth = CalculateSeverityToggleButtonWidth(counts.warningCount); + const float errorFilterWidth = CalculateSeverityToggleButtonWidth(counts.errorCount); + const float severityGroupWidth = logFilterWidth + warningFilterWidth + errorFilterWidth; + UI::PanelToolbarScope toolbar( "ConsoleToolbar", - UI::StandardPanelToolbarHeight(), + kConsoleToolbarHeight, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse, true, - UI::ToolbarPadding(), - UI::ToolbarItemSpacing(), - UI::ToolbarBackgroundColor()); + ImVec2(6.0f, kConsoleToolbarRowPaddingY), + ImVec2(kConsoleToolbarItemSpacing, 0.0f), + kConsoleToolbarBackgroundColor); + ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(0.0f, 0.0f)); if (toolbar.IsOpen() && ImGui::BeginTable( "##ConsoleToolbarLayout", @@ -655,45 +1046,49 @@ void ConsolePanel::Render() { ImGuiTableFlags_NoSavedSettings | ImGuiTableFlags_SizingStretchProp)) { ImGui::TableSetupColumn("##Primary", ImGuiTableColumnFlags_WidthStretch); - ImGui::TableSetupColumn("##Search", ImGuiTableColumnFlags_WidthFixed, 240.0f); - ImGui::TableSetupColumn("##Severity", ImGuiTableColumnFlags_WidthFixed, 250.0f); + ImGui::TableSetupColumn("##Search", ImGuiTableColumnFlags_WidthFixed, kConsoleSearchWidth); + ImGui::TableSetupColumn("##Severity", ImGuiTableColumnFlags_WidthFixed, severityGroupWidth); ImGui::TableNextRow(); ImGui::TableNextColumn(); - if (Actions::DrawToolbarAction(Actions::MakeClearConsoleAction())) { + if (DrawToolbarButton("##ConsoleClearButton", "Clear", 42.0f)) { 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::SameLine(0.0f, 1.0f); + DrawToolbarArrowDropdownButton("##ConsoleClearOptions", 16.0f, [&]() { + if (ImGui::MenuItem("Clear on Play", nullptr, m_filterState.ClearOnPlay())) { + 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); + }); ImGui::TableNextColumn(); if (m_requestSearchFocus) { ImGui::SetKeyboardFocusHere(); m_requestSearchFocus = false; } - UI::ToolbarSearchField("##ConsoleSearch", "Search", m_searchBuffer, sizeof(m_searchBuffer)); + DrawConsoleSearchField("##ConsoleSearch", m_searchBuffer, sizeof(m_searchBuffer)); ImGui::TableNextColumn(); DrawSeverityToggleButton("##ConsoleLogFilter", ConsoleSeverityVisual::Log, counts.logCount, m_filterState.ShowLog(), "Log"); - ImGui::SameLine(0.0f, 6.0f); + ImGui::SameLine(0.0f, 0.0f); DrawSeverityToggleButton("##ConsoleWarningFilter", ConsoleSeverityVisual::Warning, counts.warningCount, m_filterState.ShowWarning(), "Warnings"); - ImGui::SameLine(0.0f, 6.0f); + ImGui::SameLine(0.0f, 0.0f); DrawSeverityToggleButton("##ConsoleErrorFilter", ConsoleSeverityVisual::Error, counts.errorCount, m_filterState.ShowError(), "Errors"); ImGui::EndTable(); } + ImGui::PopStyleVar(); UI::PanelContentScope content("ConsoleRoot", ImVec2(0.0f, 0.0f)); if (!content.IsOpen()) { @@ -709,6 +1104,7 @@ void ConsolePanel::Render() { m_detailsHeight = std::clamp(m_detailsHeight, minDetailsHeight, maxDetailsHeight); const float listHeight = (std::max)(minListHeight, totalHeight - m_detailsHeight - splitterThickness); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0f, 0.0f)); ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ConsoleListBackgroundColor()); const bool listOpen = ImGui::BeginChild( "ConsoleLogList", @@ -720,7 +1116,7 @@ void ConsolePanel::Render() { const bool wasAtBottom = ImGui::GetScrollY() >= ImGui::GetScrollMaxY() - 4.0f; bool openSelectedSource = false; ImGuiListClipper clipper; - clipper.Begin(static_cast(rows.size()), UI::ConsoleRowHeight()); + clipper.Begin(static_cast(rows.size()), kConsoleRowHeight); while (clipper.Step()) { for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; ++i) { ConsoleRowData& row = rows[static_cast(i)]; @@ -757,7 +1153,7 @@ void ConsolePanel::Render() { m_selectedSerial = 0; m_selectedEntryKey.clear(); }); - UI::EndContextMenu(); + XCEngine::Editor::UI::EndContextMenu(); } ImGui::PopID(); @@ -781,7 +1177,7 @@ void ConsolePanel::Render() { m_selectedSerial = 0; m_selectedEntryKey.clear(); }); - UI::EndContextMenu(); + XCEngine::Editor::UI::EndContextMenu(); } if (hasNewRevision && wasAtBottom) { @@ -789,6 +1185,7 @@ void ConsolePanel::Render() { } } ImGui::EndChild(); + ImGui::PopStyleVar(); const UI::SplitterResult splitter = UI::DrawSplitter("##ConsoleDetailsSplitter", UI::SplitterAxis::Horizontal, splitterThickness); if (splitter.active) { @@ -807,90 +1204,84 @@ void ConsolePanel::Render() { ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ConsoleDetailsHeaderBackgroundColor()); const bool headerOpen = ImGui::BeginChild( "ConsoleDetailsHeader", - ImVec2(0.0f, 28.0f), + ImVec2(0.0f, kConsoleDetailsHeaderHeight), 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); + const bool canOpenSource = CanOpenSourceLocation(selectedRow->entry); + std::string headerText = UI::ConsoleSeverityLabel(selectedRow->entry.level); if (!sourceText.empty()) { - ImGui::SameLine(); - ImGui::TextColored(UI::ConsoleSecondaryTextColor(), "%s", sourceText.c_str()); + headerText += " "; + headerText += sourceText; } - 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); + + 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(); } } else { - ImGui::TextColored(UI::ConsoleSecondaryTextColor(), "Details"); + ImGui::TextColored(UI::ConsoleSecondaryTextColor(), "Stack Trace"); } } 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(); + const std::string traceText = BuildDetailsTraceText(*selectedRow); - 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); + const bool bodyOpen = ImGui::BeginChild( + "ConsoleDetailsBody", + ImVec2(0.0f, 0.0f), + false, + ImGuiWindowFlags_HorizontalScrollbar); + ImGui::PopStyleVar(); if (bodyOpen) { - ImGui::TextWrapped("%s", message.c_str()); + ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x); + ImGui::TextUnformatted(message.c_str()); + ImGui::PopTextWrapPos(); 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::PushStyleColor(ImGuiCol_Text, UI::ConsoleSecondaryTextColor()); + ImGui::TextUnformatted(traceText.c_str()); + ImGui::PopStyleColor(); } ImGui::EndChild(); } else { UI::PanelContentScope detailsContent("ConsoleDetailsEmpty", ImVec2(10.0f, 8.0f)); if (detailsContent.IsOpen()) { - UI::DrawEmptyState("No message selected"); + UI::DrawEmptyState("No message selected", "Select a console entry to inspect its stack trace"); } } }