Add persistent shared editor tree interactions
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -2,12 +2,50 @@
|
||||
|
||||
#include "StyleTokens.h"
|
||||
|
||||
#include <functional>
|
||||
#include <imgui.h>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
|
||||
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<std::string, bool> 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<void(const TreeNodePrefixContext&)> draw;
|
||||
|
||||
bool IsVisible() const {
|
||||
return width > 0.0f && static_cast<bool>(draw);
|
||||
}
|
||||
};
|
||||
|
||||
struct TreeNodeCallbacks {
|
||||
std::function<void(const TreeNodeResult&)> onInteraction;
|
||||
std::function<void()> 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<int>(prefixWidth / (spaceWidth > 0.0f ? spaceWidth : 1.0f)) + 1;
|
||||
if (spaceCount < 1) {
|
||||
spaceCount = 1;
|
||||
}
|
||||
|
||||
result.assign(static_cast<size_t>(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() {
|
||||
|
||||
@@ -10,6 +10,25 @@
|
||||
#include "UI/UI.h"
|
||||
#include <imgui.h>
|
||||
|
||||
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++) {
|
||||
|
||||
@@ -2,14 +2,13 @@
|
||||
|
||||
#include "Panel.h"
|
||||
#include "UI/PopupState.h"
|
||||
#include "UI/TreeView.h"
|
||||
#include <XCEngine/Components/GameObject.h>
|
||||
#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<uint64_t, 256> 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;
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<AssetItemPtr> m_itemContextMenu;
|
||||
|
||||
Reference in New Issue
Block a user