Unify panel search behavior and polish console UI

This commit is contained in:
2026-04-01 16:40:54 +08:00
parent e03f17146a
commit 4e8ad9a706
11 changed files with 998 additions and 172 deletions

View File

@@ -11,7 +11,6 @@
#include <imgui.h>
#include <shellapi.h>
#include <algorithm>
#include <cctype>
#include <cmath>
#include <ctime>
@@ -40,6 +39,8 @@ struct ConsoleSeverityCounts {
size_t errorCount = 0;
};
bool CanOpenSourceLocation(const LogEntry& entry);
struct ConsoleRowData {
uint64_t serial = 0;
LogEntry entry = {};
@@ -143,13 +144,6 @@ 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<char>(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());
@@ -173,15 +167,11 @@ std::string BuildSearchHaystack(const LogEntry& entry) {
haystack += entry.file.CStr();
haystack.push_back('\n');
haystack += entry.function.CStr();
return ToLowerCopy(std::move(haystack));
return haystack;
}
bool MatchesSearch(const LogEntry& entry, const std::string& searchText) {
if (searchText.empty()) {
return true;
}
return BuildSearchHaystack(entry).find(searchText) != std::string::npos;
bool MatchesSearch(const LogEntry& entry, const XCEngine::Editor::UI::SearchQuery& searchQuery) {
return searchQuery.Matches(BuildSearchHaystack(entry));
}
void CountSeverity(ConsoleSeverityCounts& counts, LogLevel level) {
@@ -210,7 +200,7 @@ uint64_t FindLatestSerial(const std::vector<EditorConsoleRecord>& records) {
std::vector<ConsoleRowData> BuildVisibleRows(
const std::vector<EditorConsoleRecord>& records,
const XCEngine::Editor::UI::ConsoleFilterState& filterState,
const std::string& searchText,
const XCEngine::Editor::UI::SearchQuery& searchQuery,
ConsoleSeverityCounts& counts) {
std::vector<ConsoleRowData> rows;
rows.reserve(records.size());
@@ -218,7 +208,7 @@ std::vector<ConsoleRowData> BuildVisibleRows(
if (!filterState.Collapse()) {
for (const EditorConsoleRecord& record : records) {
CountSeverity(counts, record.entry.level);
if (!filterState.Allows(record.entry.level) || !MatchesSearch(record.entry, searchText)) {
if (!filterState.Allows(record.entry.level) || !MatchesSearch(record.entry, searchQuery)) {
continue;
}
@@ -236,7 +226,7 @@ std::vector<ConsoleRowData> BuildVisibleRows(
rowIndicesByKey.reserve(records.size());
for (const EditorConsoleRecord& record : records) {
CountSeverity(counts, record.entry.level);
if (!filterState.Allows(record.entry.level) || !MatchesSearch(record.entry, searchText)) {
if (!filterState.Allows(record.entry.level) || !MatchesSearch(record.entry, searchQuery)) {
continue;
}
@@ -358,15 +348,6 @@ void DrawSeverityIcon(
}
}
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,
@@ -525,6 +506,37 @@ bool DrawToolbarButton(
return pressed;
}
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;
}
void DrawToolbarArrowDropdownButton(
const char* id,
float width,
@@ -566,28 +578,6 @@ void DrawToolbarArrowDropdownButton(
}
}
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,
@@ -595,14 +585,14 @@ bool DrawSeverityToggleButton(
bool& active,
const char* tooltip) {
const float originalCursorY = ImGui::GetCursorPosY();
ImGui::SetCursorPosY((std::max)(0.0f, originalCursorY - 1.0f));
ImGui::SetCursorPosY((std::max)(0.0f, originalCursorY - kConsoleToolbarRowPaddingY));
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;
const float buttonHeight = kConsoleToolbarButtonHeight + 2.0f;
const ImVec2 padding(7.0f, 3.0f);
const float iconRadius = 10.0f;
const float gap = 5.0f;
const float buttonHeight = kConsoleToolbarHeight;
const ImVec2 size(
(std::max)(
kConsoleCounterWidth,
@@ -643,9 +633,9 @@ bool DrawSeverityToggleButton(
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;
const ImVec2 padding(7.0f, 3.0f);
const float iconRadius = 10.0f;
const float gap = 5.0f;
return (std::max)(
kConsoleCounterWidth,
padding.x * 2.0f + iconRadius * 2.0f + gap + countSize.x);
@@ -991,9 +981,9 @@ void ConsolePanel::Render() {
m_lastErrorPauseScanSerial = FindLatestSerial(records);
}
const std::string searchText = ToLowerCopy(std::string(m_searchBuffer));
const UI::SearchQuery searchQuery(m_searchBuffer);
ConsoleSeverityCounts counts;
std::vector<ConsoleRowData> rows = BuildVisibleRows(records, m_filterState, searchText, counts);
std::vector<ConsoleRowData> rows = BuildVisibleRows(records, m_filterState, searchQuery, counts);
const ConsoleRowData* selectedRow = ResolveSelectedRow(rows, m_selectedSerial, m_selectedEntryKey);
if (selectedRow) {
m_selectedEntryKey = selectedRow->entryKey;
@@ -1058,7 +1048,7 @@ void ConsolePanel::Render() {
}
ImGui::SameLine(0.0f, 1.0f);
DrawToolbarArrowDropdownButton("##ConsoleClearOptions", 16.0f, [&]() {
if (ImGui::MenuItem("Clear on Play", nullptr, m_filterState.ClearOnPlay())) {
if (DrawCompactCheckedMenuItem("Clear on Play", m_filterState.ClearOnPlay())) {
m_filterState.ClearOnPlay() = !m_filterState.ClearOnPlay();
}
});
@@ -1077,7 +1067,7 @@ void ConsolePanel::Render() {
ImGui::SetKeyboardFocusHere();
m_requestSearchFocus = false;
}
DrawConsoleSearchField("##ConsoleSearch", m_searchBuffer, sizeof(m_searchBuffer));
UI::ToolbarSearchField("##ConsoleSearch", "Search", m_searchBuffer, sizeof(m_searchBuffer));
ImGui::TableNextColumn();
DrawSeverityToggleButton("##ConsoleLogFilter", ConsoleSeverityVisual::Log, counts.logCount, m_filterState.ShowLog(), "Log");
@@ -1162,8 +1152,8 @@ void ConsolePanel::Render() {
if (rows.empty()) {
UI::DrawEmptyState(
searchText.empty() ? "No messages" : "No search results",
searchText.empty() ? nullptr : "No console entries match the current search",
searchQuery.Empty() ? "No messages" : "No search results",
searchQuery.Empty() ? nullptr : "No console entries match the current search",
ImVec2(12.0f, 12.0f));
}

View File

@@ -8,10 +8,37 @@
#include "Core/EditorEvents.h"
#include "Core/EventBus.h"
#include "UI/UI.h"
#include <string>
#include <imgui.h>
namespace {
constexpr float kHierarchyToolbarHeight = 26.0f;
constexpr float kHierarchyToolbarPaddingY = 3.0f;
constexpr float kHierarchySearchWidth = 220.0f;
bool HierarchyNodeMatchesSearch(
XCEngine::Components::GameObject* gameObject,
const XCEngine::Editor::UI::SearchQuery& searchQuery) {
if (!gameObject) {
return false;
}
if (searchQuery.Matches(gameObject->GetName().c_str())) {
return true;
}
for (size_t i = 0; i < gameObject->GetChildCount(); ++i) {
if (HierarchyNodeMatchesSearch(gameObject->GetChild(i), searchQuery)) {
return true;
}
}
return false;
}
void DrawHierarchyTreePrefix(const XCEngine::Editor::UI::TreeNodePrefixContext& context) {
if (!context.drawList) {
return;
@@ -157,6 +184,33 @@ void HierarchyPanel::Render() {
Actions::ObserveFocusedActionRoute(*m_context, EditorActionRoute::Hierarchy);
{
UI::PanelToolbarScope toolbar(
"HierarchyToolbar",
kHierarchyToolbarHeight,
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse,
true,
ImVec2(UI::ToolbarPadding().x, kHierarchyToolbarPaddingY),
UI::ToolbarItemSpacing(),
UI::HierarchyInspectorPanelBackgroundColor());
if (toolbar.IsOpen()) {
ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(0.0f, 0.0f));
if (ImGui::BeginTable("##HierarchyToolbarLayout", 2, ImGuiTableFlags_NoSavedSettings | ImGuiTableFlags_SizingStretchProp)) {
ImGui::TableSetupColumn("##Spacer", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("##Search", ImGuiTableColumnFlags_WidthFixed, kHierarchySearchWidth);
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::TableNextColumn();
UI::ToolbarSearchField("##HierarchySearch", "Search", m_searchBuffer, sizeof(m_searchBuffer));
ImGui::EndTable();
}
ImGui::PopStyleVar();
}
}
UI::PanelContentScope content("EntityList", UI::HierarchyPanelContentPadding());
if (!content.IsOpen()) {
ImGui::PopStyleColor(2);
@@ -165,10 +219,18 @@ void HierarchyPanel::Render() {
auto& sceneManager = m_context->GetSceneManager();
auto rootEntities = sceneManager.GetRootEntities();
const UI::SearchQuery searchQuery(m_searchBuffer);
size_t visibleEntityCount = 0;
UI::ResetTreeLayout();
for (auto* gameObject : rootEntities) {
RenderEntity(gameObject);
if (RenderEntity(gameObject, searchQuery)) {
++visibleEntityCount;
}
}
if (!searchQuery.Empty() && visibleEntityCount == 0) {
UI::DrawEmptyState("No matching entities", "Try a different search term");
}
Actions::DrawHierarchyBackgroundInteraction(*m_context, m_renameState);
@@ -179,7 +241,7 @@ void HierarchyPanel::Render() {
Actions::TraceHierarchyPopup("Hierarchy background popup opened via background surface");
s_backgroundContextOpen = true;
}
Actions::DrawHierarchyCreateActions(*m_context, nullptr);
Actions::DrawHierarchyContextActions(*m_context, nullptr, true);
UI::EndContextMenu();
} else if (s_backgroundContextOpen) {
Actions::TraceHierarchyPopup("Hierarchy background popup closed");
@@ -189,18 +251,32 @@ void HierarchyPanel::Render() {
ImGui::PopStyleColor(2);
}
void HierarchyPanel::RenderEntity(::XCEngine::Components::GameObject* gameObject) {
if (!gameObject) return;
bool HierarchyPanel::RenderEntity(::XCEngine::Components::GameObject* gameObject, const UI::SearchQuery& searchQuery) {
if (!gameObject) {
return false;
}
const bool visibleInSearch = HierarchyNodeMatchesSearch(gameObject, searchQuery);
if (!visibleInSearch) {
return false;
}
const bool searching = !searchQuery.Empty();
ImGui::PushID(static_cast<int>(gameObject->GetID()));
UI::TreeNodeDefinition nodeDefinition =
BuildHierarchyNodeDefinition(*m_context, gameObject, m_itemContextMenu);
const std::string persistenceKey = std::to_string(gameObject->GetUUID());
nodeDefinition.persistenceKey = persistenceKey;
const std::string persistenceKey = searching ? std::string() : std::to_string(gameObject->GetUUID());
if (searching) {
nodeDefinition.options.defaultOpen = gameObject->GetChildCount() > 0;
nodeDefinition.persistenceKey = {};
} else {
nodeDefinition.persistenceKey = persistenceKey;
}
const bool editing = m_renameState.IsEditing(gameObject->GetID());
const UI::TreeNodeResult node = UI::DrawTreeNode(
&m_treeState,
searching ? nullptr : &m_treeState,
(void*)gameObject->GetUUID(),
editing ? "" : gameObject->GetName().c_str(),
nodeDefinition);
@@ -220,12 +296,13 @@ void HierarchyPanel::RenderEntity(::XCEngine::Components::GameObject* gameObject
if (node.open) {
for (size_t i = 0; i < gameObject->GetChildCount(); i++) {
RenderEntity(gameObject->GetChild(i));
RenderEntity(gameObject->GetChild(i), searchQuery);
}
UI::EndTreeNode();
}
ImGui::PopID();
return true;
}
void HierarchyPanel::BeginRename(::XCEngine::Components::GameObject* gameObject) {

View File

@@ -2,9 +2,11 @@
#include "Panel.h"
#include "UI/PopupState.h"
#include "UI/SearchText.h"
#include "UI/TreeView.h"
#include <XCEngine/Components/GameObject.h>
#include "Core/ISceneManager.h"
#include <string>
namespace XCEngine {
namespace Editor {
@@ -20,7 +22,7 @@ public:
private:
void OnSelectionChanged(const struct SelectionChangedEvent& event);
void OnRenameRequested(const struct EntityRenameRequestedEvent& event);
void RenderEntity(::XCEngine::Components::GameObject* gameObject);
bool RenderEntity(::XCEngine::Components::GameObject* gameObject, const UI::SearchQuery& searchQuery = UI::SearchQuery());
void BeginRename(::XCEngine::Components::GameObject* gameObject);
void CommitRename();
void CancelRename();
@@ -31,6 +33,7 @@ private:
UI::TargetedPopupState<::XCEngine::Components::GameObject*> m_itemContextMenu;
uint64_t m_selectionHandlerId = 0;
uint64_t m_renameRequestHandlerId = 0;
char m_searchBuffer[256] = "";
};
}

View File

@@ -16,6 +16,9 @@ namespace Editor {
namespace {
constexpr float kProjectToolbarHeight = 26.0f;
constexpr float kProjectToolbarPaddingY = 3.0f;
template <typename Fn>
void QueueDeferredAction(std::function<void()>& pendingAction, Fn&& fn) {
if (!pendingAction) {
@@ -234,16 +237,17 @@ void ProjectPanel::Render() {
void ProjectPanel::RenderToolbar() {
UI::PanelToolbarScope toolbar(
"ProjectToolbar",
UI::ProjectPanelToolbarHeight(),
kProjectToolbarHeight,
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse,
true,
UI::ToolbarPadding(),
ImVec2(UI::ToolbarPadding().x, kProjectToolbarPaddingY),
UI::ToolbarItemSpacing(),
UI::ProjectPanelToolbarBackgroundColor());
if (!toolbar.IsOpen()) {
return;
}
ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(0.0f, 0.0f));
if (ImGui::BeginTable("##ProjectToolbarLayout", 2, ImGuiTableFlags_NoSavedSettings | ImGuiTableFlags_SizingStretchProp)) {
ImGui::TableSetupColumn("##Spacer", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("##Search", ImGuiTableColumnFlags_WidthFixed, 220.0f);
@@ -257,6 +261,7 @@ void ProjectPanel::RenderToolbar() {
ImGui::EndTable();
}
ImGui::PopStyleVar();
}
void ProjectPanel::RenderFolderTreePane(IProjectManager& manager) {
@@ -359,13 +364,13 @@ void ProjectPanel::RenderBrowserPane(IProjectManager& manager) {
std::vector<AssetItemPtr> visibleItems;
const auto& items = manager.GetCurrentItems();
const std::string search = m_searchBuffer;
const UI::SearchQuery searchQuery(m_searchBuffer);
if (m_renameState.IsActive() && manager.FindCurrentItemIndex(m_renameState.Item()) < 0) {
CancelRename();
}
visibleItems.reserve(items.size());
for (const auto& item : items) {
if ((m_renameState.IsActive() && item && item->fullPath == m_renameState.Item()) || MatchesSearch(item, search)) {
if ((m_renameState.IsActive() && item && item->fullPath == m_renameState.Item()) || MatchesSearch(item, searchQuery)) {
visibleItems.push_back(item);
}
}
@@ -375,7 +380,10 @@ void ProjectPanel::RenderBrowserPane(IProjectManager& manager) {
ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ProjectBrowserPaneBackgroundColor());
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, UI::ProjectBrowserPanePadding());
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, UI::AssetGridSpacing());
const bool bodyOpen = ImGui::BeginChild("ProjectBrowserBody", ImVec2(0.0f, 0.0f), false);
const bool bodyOpen = ImGui::BeginChild(
"ProjectBrowserBody",
ImVec2(0.0f, 0.0f),
ImGuiChildFlags_AlwaysUseWindowPadding);
ImGui::PopStyleVar(2);
ImGui::PopStyleColor();
if (!bodyOpen) {
@@ -424,7 +432,7 @@ void ProjectPanel::RenderBrowserPane(IProjectManager& manager) {
ImGui::SetCursorPosY(gridOrigin.y + rowCount * tileHeight + (rowCount - 1) * rowSpacing);
}
if (visibleItems.empty() && !search.empty()) {
if (visibleItems.empty() && !searchQuery.Empty()) {
UI::DrawEmptyState(
"No Search Results",
"No assets match the current search");
@@ -521,7 +529,12 @@ ProjectPanel::AssetItemInteraction ProjectPanel::RenderAssetItem(const AssetItem
isDraggingThisItem,
[&](ImDrawList* drawList, const ImVec2& iconMin, const ImVec2& iconMax) {
if (item && item->canUseImagePreview &&
UI::DrawTextureAssetPreview(drawList, iconMin, iconMax, item->fullPath)) {
UI::DrawTextureAssetPreview(
drawList,
iconMin,
iconMax,
item->fullPath,
m_context ? m_context->GetProjectPath() : std::string())) {
return;
}
UI::DrawAssetIcon(drawList, iconMin, iconMax, iconKind);
@@ -584,23 +597,15 @@ ProjectPanel::AssetItemInteraction ProjectPanel::RenderAssetItem(const AssetItem
return interaction;
}
bool ProjectPanel::MatchesSearch(const AssetItemPtr& item, const std::string& search) {
bool ProjectPanel::MatchesSearch(const AssetItemPtr& item, const UI::SearchQuery& searchQuery) {
if (!item) {
return false;
}
if (search.empty()) {
if (searchQuery.Empty()) {
return true;
}
auto toLower = [](unsigned char c) {
return static_cast<char>(std::tolower(c));
};
std::string itemName = item->name;
std::string searchText = search;
std::transform(itemName.begin(), itemName.end(), itemName.begin(), toLower);
std::transform(searchText.begin(), searchText.end(), searchText.begin(), toLower);
return itemName.find(searchText) != std::string::npos;
return searchQuery.Matches(item->name);
}
bool ProjectPanel::IsCurrentTreeBranch(const std::string& currentFolderPath, const std::string& folderPath) {

View File

@@ -3,6 +3,7 @@
#include "Panel.h"
#include "Core/AssetItem.h"
#include "UI/PopupState.h"
#include "UI/SearchText.h"
#include "UI/TreeView.h"
#include <functional>
@@ -52,7 +53,7 @@ private:
void RenderBrowserPane(IProjectManager& manager);
void RenderBrowserHeader(IProjectManager& manager);
AssetItemInteraction RenderAssetItem(const AssetItemPtr& item, bool isSelected);
static bool MatchesSearch(const AssetItemPtr& item, const std::string& search);
static bool MatchesSearch(const AssetItemPtr& item, const UI::SearchQuery& searchQuery);
static bool IsCurrentTreeBranch(const std::string& currentFolderPath, const std::string& folderPath);
char m_searchBuffer[256] = "";