Extract shared editor tree view

This commit is contained in:
2026-03-27 22:05:05 +08:00
parent a51e0f6f88
commit 37a90b39e2
6 changed files with 120 additions and 169 deletions

View File

@@ -162,7 +162,7 @@ inline ImVec2 ProjectBrowserPanePadding() {
return ImVec2(10.0f, 8.0f); return ImVec2(10.0f, 8.0f);
} }
inline ImVec2 ProjectTreeNodeFramePadding() { inline ImVec2 NavigationTreeNodeFramePadding() {
return ImVec2(4.0f, 3.0f); return ImVec2(4.0f, 3.0f);
} }
@@ -226,10 +226,6 @@ inline ImVec2 PopupWindowPadding() {
return ImVec2(12.0f, 10.0f); return ImVec2(12.0f, 10.0f);
} }
inline ImVec2 HierarchyNodeFramePadding() {
return ImVec2(4.0f, 3.0f);
}
inline ImVec2 AssetTileSize() { inline ImVec2 AssetTileSize() {
return ImVec2(104.0f, 82.0f); return ImVec2(104.0f, 82.0f);
} }

68
editor/src/UI/TreeView.h Normal file
View File

@@ -0,0 +1,68 @@
#pragma once
#include "StyleTokens.h"
#include <imgui.h>
namespace XCEngine {
namespace Editor {
namespace UI {
struct TreeNodeOptions {
bool selected = false;
bool leaf = false;
bool defaultOpen = false;
bool openOnArrow = true;
bool openOnDoubleClick = true;
bool spanAvailWidth = true;
bool framePadding = true;
};
struct TreeNodeResult {
bool open = false;
bool clicked = false;
bool doubleClicked = false;
};
inline TreeNodeResult DrawTreeNode(const void* id, const char* label, const TreeNodeOptions& options = {}) {
ImGuiTreeNodeFlags flags = 0;
if (options.openOnArrow) {
flags |= ImGuiTreeNodeFlags_OpenOnArrow;
}
if (options.openOnDoubleClick) {
flags |= ImGuiTreeNodeFlags_OpenOnDoubleClick;
}
if (options.spanAvailWidth) {
flags |= ImGuiTreeNodeFlags_SpanAvailWidth;
}
if (options.framePadding) {
flags |= ImGuiTreeNodeFlags_FramePadding;
}
if (options.leaf) {
flags |= ImGuiTreeNodeFlags_Leaf;
}
if (options.selected) {
flags |= ImGuiTreeNodeFlags_Selected;
}
if (options.defaultOpen) {
flags |= ImGuiTreeNodeFlags_DefaultOpen;
}
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, NavigationTreeNodeFramePadding());
const bool open = ImGui::TreeNodeEx(id, flags, "%s", label);
ImGui::PopStyleVar();
return TreeNodeResult{
open,
ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen(),
ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)
};
}
inline void EndTreeNode() {
ImGui::TreePop();
}
} // namespace UI
} // namespace Editor
} // namespace XCEngine

View File

@@ -15,6 +15,7 @@
#include "ScalarControls.h" #include "ScalarControls.h"
#include "SceneStatusWidget.h" #include "SceneStatusWidget.h"
#include "StyleTokens.h" #include "StyleTokens.h"
#include "TreeView.h"
#include "VectorControls.h" #include "VectorControls.h"
#include "Widgets.h" #include "Widgets.h"

View File

@@ -14,12 +14,6 @@ struct ComponentSectionResult {
bool open = false; bool open = false;
}; };
struct HierarchyNodeResult {
bool open = false;
bool clicked = false;
bool doubleClicked = false;
};
struct AssetTileResult { struct AssetTileResult {
bool clicked = false; bool clicked = false;
bool contextRequested = false; bool contextRequested = false;
@@ -183,37 +177,6 @@ inline void DrawToolbarBreadcrumbs(
ImGui::PopStyleColor(2); ImGui::PopStyleColor(2);
} }
inline HierarchyNodeResult DrawHierarchyNode(
const void* id,
const char* label,
bool selected,
bool leaf) {
ImGuiTreeNodeFlags flags =
ImGuiTreeNodeFlags_OpenOnArrow |
ImGuiTreeNodeFlags_SpanAvailWidth |
ImGuiTreeNodeFlags_FramePadding;
if (leaf) {
flags |= ImGuiTreeNodeFlags_Leaf;
}
if (selected) {
flags |= ImGuiTreeNodeFlags_Selected;
}
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, HierarchyNodeFramePadding());
const bool open = ImGui::TreeNodeEx(id, flags, "%s", label);
ImGui::PopStyleVar();
return HierarchyNodeResult{
open,
ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen(),
ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)
};
}
inline void EndHierarchyNode() {
ImGui::TreePop();
}
template <typename DrawIconFn> template <typename DrawIconFn>
inline AssetTileResult DrawAssetTile( inline AssetTileResult DrawAssetTile(
const char* label, const char* label,

View File

@@ -65,71 +65,41 @@ void HierarchyPanel::OnRenameRequested(const EntityRenameRequestedEvent& event)
} }
void HierarchyPanel::Render() { void HierarchyPanel::Render() {
UI::PanelWindowScope panel(m_name.c_str()); ImGui::PushStyleColor(ImGuiCol_WindowBg, UI::HierarchyInspectorPanelBackgroundColor());
if (!panel.IsOpen()) { ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::HierarchyInspectorPanelBackgroundColor());
return; {
UI::PanelWindowScope panel(m_name.c_str());
if (!panel.IsOpen()) {
ImGui::PopStyleColor(2);
return;
}
Actions::ObserveFocusedActionRoute(*m_context, EditorActionRoute::Hierarchy);
UI::PanelContentScope content("EntityList");
if (!content.IsOpen()) {
ImGui::PopStyleColor(2);
return;
}
auto& sceneManager = m_context->GetSceneManager();
auto rootEntities = sceneManager.GetRootEntities();
for (auto* gameObject : rootEntities) {
RenderEntity(gameObject);
}
Actions::HandleHierarchyBackgroundPrimaryClick(*m_context, m_renameState);
Actions::DrawHierarchyBackgroundContextPopup(*m_context);
Actions::DrawHierarchyRootDropTarget(*m_context);
} }
ImGui::PopStyleColor(2);
Actions::ObserveFocusedActionRoute(*m_context, EditorActionRoute::Hierarchy);
RenderSearchBar();
std::string filter = m_searchBuffer;
UI::PanelContentScope content("EntityList");
if (!content.IsOpen()) {
return;
}
auto& sceneManager = m_context->GetSceneManager();
auto rootEntities = sceneManager.GetRootEntities();
SortEntities(const_cast<std::vector<::XCEngine::Components::GameObject*>&>(rootEntities));
for (auto* gameObject : rootEntities) {
RenderEntity(gameObject, filter);
}
Actions::HandleHierarchyBackgroundPrimaryClick(*m_context, m_renameState);
Actions::DrawHierarchyBackgroundContextPopup(*m_context);
Actions::DrawHierarchyRootDropTarget(*m_context);
} }
void HierarchyPanel::RenderSearchBar() { void HierarchyPanel::RenderEntity(::XCEngine::Components::GameObject* gameObject) {
UI::PanelToolbarScope toolbar("HierarchyToolbar", UI::StandardPanelToolbarHeight());
if (!toolbar.IsOpen()) {
return;
}
const float buttonWidth = UI::HierarchyOverflowButtonWidth();
UI::ToolbarSearchField(
"##Search",
"Search hierarchy",
m_searchBuffer,
sizeof(m_searchBuffer),
buttonWidth + UI::ToolbarSearchTrailingSpacing());
ImGui::SameLine();
if (UI::ToolbarButton("...", false, ImVec2(buttonWidth, 0.0f))) {
Actions::RequestHierarchyOptionsPopup(m_optionsPopup);
}
Actions::DrawHierarchySortOptionsPopup(
m_optionsPopup,
m_sortMode,
SortMode::Name,
SortMode::ComponentCount,
SortMode::TransformFirst,
[this](SortMode mode) {
m_sortMode = mode;
});
}
void HierarchyPanel::RenderEntity(::XCEngine::Components::GameObject* gameObject, const std::string& filter) {
if (!gameObject) return; if (!gameObject) return;
if (!filter.empty() && !PassesFilter(gameObject, filter)) {
return;
}
ImGui::PushID(static_cast<int>(gameObject->GetID())); ImGui::PushID(static_cast<int>(gameObject->GetID()));
if (m_renameState.IsEditing(gameObject->GetID())) { if (m_renameState.IsEditing(gameObject->GetID())) {
@@ -152,11 +122,14 @@ void HierarchyPanel::RenderEntity(::XCEngine::Components::GameObject* gameObject
CommitRename(); CommitRename();
} }
} else { } else {
const UI::HierarchyNodeResult node = UI::DrawHierarchyNode( UI::TreeNodeOptions nodeOptions;
nodeOptions.selected = m_context->GetSelectionManager().IsSelected(gameObject->GetID());
nodeOptions.leaf = gameObject->GetChildCount() == 0;
const UI::TreeNodeResult node = UI::DrawTreeNode(
(void*)gameObject->GetUUID(), (void*)gameObject->GetUUID(),
gameObject->GetName().c_str(), gameObject->GetName().c_str(),
m_context->GetSelectionManager().IsSelected(gameObject->GetID()), nodeOptions);
gameObject->GetChildCount() == 0);
if (node.clicked) { if (node.clicked) {
Actions::HandleHierarchySelectionClick(*m_context, gameObject->GetID(), ImGui::GetIO().KeyCtrl); Actions::HandleHierarchySelectionClick(*m_context, gameObject->GetID(), ImGui::GetIO().KeyCtrl);
@@ -173,9 +146,9 @@ void HierarchyPanel::RenderEntity(::XCEngine::Components::GameObject* gameObject
if (node.open) { if (node.open) {
for (size_t i = 0; i < gameObject->GetChildCount(); i++) { for (size_t i = 0; i < gameObject->GetChildCount(); i++) {
RenderEntity(gameObject->GetChild(i), filter); RenderEntity(gameObject->GetChild(i));
} }
UI::EndHierarchyNode(); UI::EndTreeNode();
} }
} }
@@ -205,46 +178,5 @@ void HierarchyPanel::CancelRename() {
m_renameState.Cancel(); m_renameState.Cancel();
} }
bool HierarchyPanel::PassesFilter(::XCEngine::Components::GameObject* gameObject, const std::string& filter) {
if (!gameObject) return false;
if (gameObject->GetName().find(filter) != std::string::npos) {
return true;
}
for (size_t i = 0; i < gameObject->GetChildCount(); i++) {
if (PassesFilter(gameObject->GetChild(i), filter)) {
return true;
}
}
return false;
}
void HierarchyPanel::SortEntities(std::vector<::XCEngine::Components::GameObject*>& entities) {
switch (m_sortMode) {
case SortMode::Name:
std::sort(entities.begin(), entities.end(), [](::XCEngine::Components::GameObject* a, ::XCEngine::Components::GameObject* b) {
return a->GetName() < b->GetName();
});
break;
case SortMode::ComponentCount:
std::sort(entities.begin(), entities.end(), [](::XCEngine::Components::GameObject* a, ::XCEngine::Components::GameObject* b) {
return a->GetComponents<::XCEngine::Components::Component>().size() > b->GetComponents<::XCEngine::Components::Component>().size();
});
break;
case SortMode::TransformFirst:
std::sort(entities.begin(), entities.end(), [](::XCEngine::Components::GameObject* a, ::XCEngine::Components::GameObject* b) {
bool aHasTransform = a->GetComponent<::XCEngine::Components::TransformComponent>() != nullptr;
bool bHasTransform = b->GetComponent<::XCEngine::Components::TransformComponent>() != nullptr;
if (aHasTransform != bHasTransform) {
return aHasTransform;
}
return a->GetName() < b->GetName();
});
break;
}
}
} }
} }

View File

@@ -118,37 +118,28 @@ void ProjectPanel::RenderFolderTreeNode(
} }
} }
ImGuiTreeNodeFlags flags = UI::TreeNodeOptions nodeOptions;
ImGuiTreeNodeFlags_OpenOnArrow | nodeOptions.selected = folder->fullPath == currentFolderPath;
ImGuiTreeNodeFlags_OpenOnDoubleClick | nodeOptions.leaf = !hasChildFolders;
ImGuiTreeNodeFlags_SpanAvailWidth | nodeOptions.defaultOpen = IsCurrentTreeBranch(currentFolderPath, folder->fullPath);
ImGuiTreeNodeFlags_FramePadding;
if (!hasChildFolders) {
flags |= ImGuiTreeNodeFlags_Leaf;
}
if (folder->fullPath == currentFolderPath) {
flags |= ImGuiTreeNodeFlags_Selected;
}
if (IsCurrentTreeBranch(currentFolderPath, folder->fullPath)) {
flags |= ImGuiTreeNodeFlags_DefaultOpen;
}
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, UI::ProjectTreeNodeFramePadding()); const UI::TreeNodeResult node = UI::DrawTreeNode(
const bool open = ImGui::TreeNodeEx((void*)folder.get(), flags, "%s", folder->name.c_str()); (void*)folder.get(),
ImGui::PopStyleVar(); folder->name.c_str(),
nodeOptions);
if (ImGui::IsItemClicked()) { if (node.clicked) {
manager.NavigateToFolder(folder); manager.NavigateToFolder(folder);
} }
if (open) { if (node.open) {
for (const auto& child : folder->children) { for (const auto& child : folder->children) {
if (!child || !child->isFolder) { if (!child || !child->isFolder) {
continue; continue;
} }
RenderFolderTreeNode(manager, child, currentFolderPath); RenderFolderTreeNode(manager, child, currentFolderPath);
} }
ImGui::TreePop(); UI::EndTreeNode();
} }
} }