diff --git a/editor/src/UI/StyleTokens.h b/editor/src/UI/StyleTokens.h index 8e67e78b..6b95473e 100644 --- a/editor/src/UI/StyleTokens.h +++ b/editor/src/UI/StyleTokens.h @@ -166,6 +166,20 @@ inline ImVec2 NavigationTreeNodeFramePadding() { return ImVec2(4.0f, 3.0f); } +inline float NavigationTreePrefixWidth() { + return 16.0f; +} + +inline ImVec4 NavigationTreePrefixColor(bool selected = false, bool hovered = false) { + if (selected) { + return ImVec4(0.86f, 0.86f, 0.86f, 1.0f); + } + if (hovered) { + return ImVec4(0.76f, 0.76f, 0.76f, 1.0f); + } + return ImVec4(0.62f, 0.62f, 0.62f, 1.0f); +} + inline ImVec4 ProjectNavigationPaneBackgroundColor() { return ImVec4(0.20f, 0.20f, 0.20f, 1.0f); } diff --git a/editor/src/UI/TreeView.h b/editor/src/UI/TreeView.h index 06b89838..ba6489e7 100644 --- a/editor/src/UI/TreeView.h +++ b/editor/src/UI/TreeView.h @@ -2,12 +2,50 @@ #include "StyleTokens.h" +#include #include +#include +#include +#include namespace XCEngine { namespace Editor { namespace UI { +class TreeViewState { +public: + bool TryGetExpanded(std::string_view key, bool& expanded) const { + const auto it = m_expanded.find(std::string(key)); + if (it == m_expanded.end()) { + return false; + } + + expanded = it->second; + return true; + } + + bool ResolveExpanded(std::string_view key, bool fallback) const { + bool expanded = fallback; + TryGetExpanded(key, expanded); + return expanded; + } + + void SetExpanded(std::string_view key, bool expanded) { + if (key.empty()) { + return; + } + + m_expanded[std::string(key)] = expanded; + } + + void Clear() { + m_expanded.clear(); + } + +private: + std::unordered_map m_expanded; +}; + struct TreeNodeOptions { bool selected = false; bool leaf = false; @@ -22,9 +60,72 @@ struct TreeNodeResult { bool open = false; bool clicked = false; bool doubleClicked = false; + bool secondaryClicked = false; + bool hovered = false; + bool toggledOpen = false; }; -inline TreeNodeResult DrawTreeNode(const void* id, const char* label, const TreeNodeOptions& options = {}) { +struct TreeNodePrefixContext { + ImDrawList* drawList = nullptr; + ImVec2 min = ImVec2(0.0f, 0.0f); + ImVec2 max = ImVec2(0.0f, 0.0f); + bool selected = false; + bool hovered = false; +}; + +struct TreeNodePrefixSlot { + float width = 0.0f; + std::function draw; + + bool IsVisible() const { + return width > 0.0f && static_cast(draw); + } +}; + +struct TreeNodeCallbacks { + std::function onInteraction; + std::function onRenderExtras; +}; + +struct TreeNodeDefinition { + TreeNodeOptions options; + std::string_view persistenceKey; + TreeNodePrefixSlot prefix; + TreeNodeCallbacks callbacks; +}; + +inline std::string MakeTreeNodeDisplayLabel(const char* label, float prefixWidth) { + std::string result; + if (!label) { + return result; + } + + if (prefixWidth <= 0.0f) { + result = label; + return result; + } + + const float spaceWidth = ImGui::CalcTextSize(" ").x; + int spaceCount = static_cast(prefixWidth / (spaceWidth > 0.0f ? spaceWidth : 1.0f)) + 1; + if (spaceCount < 1) { + spaceCount = 1; + } + + result.assign(static_cast(spaceCount), ' '); + result += label; + return result; +} + +inline TreeNodeResult DrawTreeNode( + TreeViewState* state, + const void* id, + const char* label, + const TreeNodeDefinition& definition = {}) { + TreeNodeOptions options = definition.options; + if (state && !definition.persistenceKey.empty()) { + ImGui::SetNextItemOpen(state->ResolveExpanded(definition.persistenceKey, options.defaultOpen), ImGuiCond_Always); + } + ImGuiTreeNodeFlags flags = 0; if (options.openOnArrow) { flags |= ImGuiTreeNodeFlags_OpenOnArrow; @@ -48,15 +149,54 @@ inline TreeNodeResult DrawTreeNode(const void* id, const char* label, const Tree flags |= ImGuiTreeNodeFlags_DefaultOpen; } + const std::string displayLabel = definition.prefix.IsVisible() + ? MakeTreeNodeDisplayLabel(label, definition.prefix.width) + : std::string(label ? label : ""); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, NavigationTreeNodeFramePadding()); - const bool open = ImGui::TreeNodeEx(id, flags, "%s", label); + const bool open = ImGui::TreeNodeEx(id, flags, "%s", displayLabel.c_str()); ImGui::PopStyleVar(); - return TreeNodeResult{ + TreeNodeResult result{ open, ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen(), - ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0) + ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0), + ImGui::IsItemClicked(ImGuiMouseButton_Right), + ImGui::IsItemHovered(), + ImGui::IsItemToggledOpen() }; + + if (state && !definition.persistenceKey.empty()) { + state->SetExpanded(definition.persistenceKey, result.open); + } + + if (definition.prefix.IsVisible()) { + const ImVec2 itemMin = ImGui::GetItemRectMin(); + const ImVec2 itemMax = ImGui::GetItemRectMax(); + const float prefixMinX = itemMin.x + ImGui::GetTreeNodeToLabelSpacing(); + definition.prefix.draw(TreeNodePrefixContext{ + ImGui::GetWindowDrawList(), + ImVec2(prefixMinX, itemMin.y), + ImVec2(prefixMinX + definition.prefix.width, itemMax.y), + options.selected, + result.hovered + }); + } + + if (definition.callbacks.onInteraction) { + definition.callbacks.onInteraction(result); + } + if (definition.callbacks.onRenderExtras) { + definition.callbacks.onRenderExtras(); + } + + return result; +} + +inline TreeNodeResult DrawTreeNode(const void* id, const char* label, const TreeNodeOptions& options = {}) { + TreeNodeDefinition definition; + definition.options = options; + return DrawTreeNode(nullptr, id, label, definition); } inline void EndTreeNode() { diff --git a/editor/src/panels/HierarchyPanel.cpp b/editor/src/panels/HierarchyPanel.cpp index 2c4084f5..d71ba60d 100644 --- a/editor/src/panels/HierarchyPanel.cpp +++ b/editor/src/panels/HierarchyPanel.cpp @@ -10,6 +10,25 @@ #include "UI/UI.h" #include +namespace { + +void DrawHierarchyTreePrefix(const XCEngine::Editor::UI::TreeNodePrefixContext& context) { + if (!context.drawList) { + return; + } + + const ImVec4 color = XCEngine::Editor::UI::NavigationTreePrefixColor(context.selected, context.hovered); + const float size = 9.0f; + const float centerX = context.min.x + (context.max.x - context.min.x) * 0.5f; + const float centerY = context.min.y + (context.max.y - context.min.y) * 0.5f; + const ImVec2 min(centerX - size * 0.5f, centerY - size * 0.5f); + const ImVec2 max(centerX + size * 0.5f, centerY + size * 0.5f); + context.drawList->AddRect(min, max, ImGui::GetColorU32(color), 1.5f); + context.drawList->AddLine(ImVec2(min.x, centerY), ImVec2(max.x, centerY), ImGui::GetColorU32(color), 1.0f); +} + +} // namespace + namespace XCEngine { namespace Editor { @@ -122,27 +141,33 @@ void HierarchyPanel::RenderEntity(::XCEngine::Components::GameObject* gameObject CommitRename(); } } else { - UI::TreeNodeOptions nodeOptions; - nodeOptions.selected = m_context->GetSelectionManager().IsSelected(gameObject->GetID()); - nodeOptions.leaf = gameObject->GetChildCount() == 0; + UI::TreeNodeDefinition nodeDefinition; + nodeDefinition.options.selected = m_context->GetSelectionManager().IsSelected(gameObject->GetID()); + nodeDefinition.options.leaf = gameObject->GetChildCount() == 0; + const std::string persistenceKey = std::to_string(gameObject->GetUUID()); + nodeDefinition.persistenceKey = persistenceKey; + nodeDefinition.prefix.width = UI::NavigationTreePrefixWidth(); + nodeDefinition.prefix.draw = DrawHierarchyTreePrefix; + nodeDefinition.callbacks.onInteraction = [this, gameObject](const UI::TreeNodeResult& node) { + if (node.clicked) { + Actions::HandleHierarchySelectionClick(*m_context, gameObject->GetID(), ImGui::GetIO().KeyCtrl); + } + + if (node.doubleClicked) { + BeginRename(gameObject); + } + }; + nodeDefinition.callbacks.onRenderExtras = [this, gameObject]() { + Actions::BeginHierarchyEntityDrag(gameObject); + Actions::AcceptHierarchyEntityDrop(*m_context, gameObject); + Actions::DrawHierarchyEntityContextPopup(*m_context, gameObject); + }; const UI::TreeNodeResult node = UI::DrawTreeNode( + &m_treeState, (void*)gameObject->GetUUID(), gameObject->GetName().c_str(), - nodeOptions); - - if (node.clicked) { - Actions::HandleHierarchySelectionClick(*m_context, gameObject->GetID(), ImGui::GetIO().KeyCtrl); - } - - if (node.doubleClicked) { - BeginRename(gameObject); - } - - Actions::BeginHierarchyEntityDrag(gameObject); - Actions::AcceptHierarchyEntityDrop(*m_context, gameObject); - - Actions::DrawHierarchyEntityContextPopup(*m_context, gameObject); + nodeDefinition); if (node.open) { for (size_t i = 0; i < gameObject->GetChildCount(); i++) { diff --git a/editor/src/panels/HierarchyPanel.h b/editor/src/panels/HierarchyPanel.h index c46db982..80118824 100644 --- a/editor/src/panels/HierarchyPanel.h +++ b/editor/src/panels/HierarchyPanel.h @@ -2,14 +2,13 @@ #include "Panel.h" #include "UI/PopupState.h" +#include "UI/TreeView.h" #include #include "Core/ISceneManager.h" namespace XCEngine { namespace Editor { -enum class SortMode { Name, ComponentCount, TransformFirst }; - class HierarchyPanel : public Panel { public: HierarchyPanel(); @@ -21,18 +20,13 @@ public: private: void OnSelectionChanged(const struct SelectionChangedEvent& event); void OnRenameRequested(const struct EntityRenameRequestedEvent& event); - void RenderSearchBar(); - void RenderEntity(::XCEngine::Components::GameObject* gameObject, const std::string& filter); + void RenderEntity(::XCEngine::Components::GameObject* gameObject); void BeginRename(::XCEngine::Components::GameObject* gameObject); void CommitRename(); void CancelRename(); - bool PassesFilter(::XCEngine::Components::GameObject* gameObject, const std::string& filter); - void SortEntities(std::vector<::XCEngine::Components::GameObject*>& entities); - - char m_searchBuffer[256] = ""; + UI::InlineTextEditState m_renameState; - UI::DeferredPopupState m_optionsPopup; - SortMode m_sortMode = SortMode::Name; + UI::TreeViewState m_treeState; uint64_t m_selectionHandlerId = 0; uint64_t m_renameRequestHandlerId = 0; }; diff --git a/editor/src/panels/ProjectPanel.cpp b/editor/src/panels/ProjectPanel.cpp index 972f6f95..f5e56631 100644 --- a/editor/src/panels/ProjectPanel.cpp +++ b/editor/src/panels/ProjectPanel.cpp @@ -14,6 +14,26 @@ namespace XCEngine { namespace Editor { +namespace { + +void DrawProjectFolderTreePrefix(const UI::TreeNodePrefixContext& context) { + if (!context.drawList) { + return; + } + + const float iconWidth = 14.0f; + const float iconHeight = 11.0f; + const float minX = context.min.x + ((context.max.x - context.min.x) - iconWidth) * 0.5f; + const float minY = context.min.y + ((context.max.y - context.min.y) - iconHeight) * 0.5f; + UI::DrawAssetIcon( + context.drawList, + ImVec2(minX, minY), + ImVec2(minX + iconWidth, minY + iconHeight), + UI::AssetIconKind::Folder); +} + +} // namespace + ProjectPanel::ProjectPanel() : Panel("Project") { } @@ -118,19 +138,24 @@ void ProjectPanel::RenderFolderTreeNode( } } - UI::TreeNodeOptions nodeOptions; - nodeOptions.selected = folder->fullPath == currentFolderPath; - nodeOptions.leaf = !hasChildFolders; - nodeOptions.defaultOpen = IsCurrentTreeBranch(currentFolderPath, folder->fullPath); + UI::TreeNodeDefinition nodeDefinition; + nodeDefinition.options.selected = folder->fullPath == currentFolderPath; + nodeDefinition.options.leaf = !hasChildFolders; + nodeDefinition.options.defaultOpen = IsCurrentTreeBranch(currentFolderPath, folder->fullPath); + nodeDefinition.persistenceKey = folder->fullPath; + nodeDefinition.prefix.width = UI::NavigationTreePrefixWidth(); + nodeDefinition.prefix.draw = DrawProjectFolderTreePrefix; + nodeDefinition.callbacks.onInteraction = [&manager, folder](const UI::TreeNodeResult& node) { + if (node.clicked) { + manager.NavigateToFolder(folder); + } + }; const UI::TreeNodeResult node = UI::DrawTreeNode( + &m_folderTreeState, (void*)folder.get(), folder->name.c_str(), - nodeOptions); - - if (node.clicked) { - manager.NavigateToFolder(folder); - } + nodeDefinition); if (node.open) { for (const auto& child : folder->children) { diff --git a/editor/src/panels/ProjectPanel.h b/editor/src/panels/ProjectPanel.h index f4ada144..3410801f 100644 --- a/editor/src/panels/ProjectPanel.h +++ b/editor/src/panels/ProjectPanel.h @@ -3,6 +3,7 @@ #include "Panel.h" #include "Core/AssetItem.h" #include "UI/PopupState.h" +#include "UI/TreeView.h" namespace XCEngine { namespace Editor { @@ -32,6 +33,7 @@ private: char m_searchBuffer[256] = ""; float m_navigationWidth = UI::ProjectNavigationDefaultWidth(); + UI::TreeViewState m_folderTreeState; UI::TextInputPopupState<256> m_createFolderDialog; UI::DeferredPopupState m_emptyContextMenu; UI::TargetedPopupState m_itemContextMenu;