Files
XCEngine/editor/src/panels/HierarchyPanel.cpp

333 lines
12 KiB
C++
Raw Normal View History

2026-03-26 23:52:05 +08:00
#include "Actions/HierarchyActionRouter.h"
2026-03-26 22:10:43 +08:00
#include "Actions/ActionRouting.h"
2026-03-26 21:18:33 +08:00
#include "Commands/EntityCommands.h"
#include "HierarchyPanel.h"
#include "Core/IEditorContext.h"
#include "Core/ISceneManager.h"
#include "Core/ISelectionManager.h"
#include "Core/EditorEvents.h"
#include "Core/EventBus.h"
2026-03-26 21:18:33 +08:00
#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;
}
const float width = context.max.x - context.min.x;
const float height = context.max.y - context.min.y;
const float iconExtent = XCEngine::Editor::UI::NavigationTreeIconSize();
const float minX = context.min.x + (width - iconExtent) * 0.5f;
const float minY = context.min.y + (height - iconExtent) * 0.5f;
XCEngine::Editor::UI::DrawAssetIcon(
context.drawList,
ImVec2(minX, minY),
ImVec2(minX + iconExtent, minY + iconExtent),
XCEngine::Editor::UI::AssetIconKind::GameObject);
}
2026-03-30 00:49:10 +08:00
XCEngine::Editor::UI::TreeNodeDefinition BuildHierarchyNodeDefinition(
XCEngine::Editor::IEditorContext& context,
XCEngine::Components::GameObject* gameObject,
XCEngine::Editor::UI::TargetedPopupState<XCEngine::Components::GameObject*>& itemContextMenu) {
XCEngine::Editor::UI::TreeNodeDefinition nodeDefinition;
nodeDefinition.options.selected = context.GetSelectionManager().IsSelected(gameObject->GetID());
nodeDefinition.options.leaf = gameObject->GetChildCount() == 0;
nodeDefinition.options.openOnDoubleClick = false;
2026-03-30 00:49:10 +08:00
nodeDefinition.style = XCEngine::Editor::UI::HierarchyTreeStyle();
nodeDefinition.prefix.width = XCEngine::Editor::UI::NavigationTreePrefixWidth();
nodeDefinition.prefix.draw = DrawHierarchyTreePrefix;
nodeDefinition.callbacks.onInteraction = [&context, &itemContextMenu, gameObject](const XCEngine::Editor::UI::TreeNodeResult& node) {
if (node.clicked) {
XCEngine::Editor::Actions::HandleHierarchySelectionClick(
context,
gameObject->GetID(),
ImGui::GetIO().KeyCtrl);
}
if (node.secondaryClicked) {
XCEngine::Editor::Actions::HandleHierarchyItemContextRequest(
context,
gameObject,
itemContextMenu);
}
};
nodeDefinition.callbacks.onRenderExtras = [&context, gameObject]() {
XCEngine::Editor::Actions::BeginHierarchyEntityDrag(gameObject);
XCEngine::Editor::Actions::AcceptHierarchyEntityDrop(context, gameObject);
};
return nodeDefinition;
}
void DrawHierarchyRenameFieldAtCurrentRow(
XCEngine::Editor::UI::InlineTextEditState<uint64_t, 256>& renameState,
const XCEngine::Editor::UI::TreeViewStyle& style) {
const ImVec2 itemMin = ImGui::GetItemRectMin();
const ImVec2 itemMax = ImGui::GetItemRectMax();
const float arrowSlotWidth = ImGui::GetTreeNodeToLabelSpacing();
const float prefixWidth = XCEngine::Editor::UI::NavigationTreePrefixWidth();
const float prefixGap = XCEngine::Editor::UI::NavigationTreePrefixLabelGap();
const float labelMinX =
itemMin.x +
arrowSlotWidth +
style.prefixStartOffset +
prefixWidth +
prefixGap;
const float renameWidth = (std::max)(0.0f, itemMax.x - labelMinX);
const float renameY = itemMin.y +
(std::max)(0.0f, (itemMax.y - itemMin.y - XCEngine::Editor::UI::InlineRenameFieldHeight()) * 0.5f);
XCEngine::Editor::UI::DrawInlineRenameFieldAt(
"##Rename",
ImVec2(labelMinX, renameY),
renameState.Buffer(),
renameState.BufferSize(),
renameWidth,
renameState.ConsumeFocusRequest());
}
} // namespace
namespace XCEngine {
namespace Editor {
HierarchyPanel::HierarchyPanel() : Panel("Hierarchy") {
}
void HierarchyPanel::OnAttach() {
if (!m_context || m_selectionHandlerId || m_renameRequestHandlerId) {
return;
}
m_selectionHandlerId = m_context->GetEventBus().Subscribe<SelectionChangedEvent>(
[this](const SelectionChangedEvent& event) {
OnSelectionChanged(event);
}
);
2026-03-26 22:10:43 +08:00
m_renameRequestHandlerId = m_context->GetEventBus().Subscribe<EntityRenameRequestedEvent>(
[this](const EntityRenameRequestedEvent& event) {
OnRenameRequested(event);
}
);
}
void HierarchyPanel::OnDetach() {
if (!m_context) {
return;
}
if (m_selectionHandlerId) {
m_context->GetEventBus().Unsubscribe<SelectionChangedEvent>(m_selectionHandlerId);
m_selectionHandlerId = 0;
}
if (m_renameRequestHandlerId) {
m_context->GetEventBus().Unsubscribe<EntityRenameRequestedEvent>(m_renameRequestHandlerId);
m_renameRequestHandlerId = 0;
}
}
void HierarchyPanel::OnSelectionChanged(const SelectionChangedEvent& event) {
2026-03-26 21:30:46 +08:00
if (m_renameState.IsActive() && event.primarySelection != m_renameState.Item()) {
CancelRename();
}
}
2026-03-26 22:10:43 +08:00
void HierarchyPanel::OnRenameRequested(const EntityRenameRequestedEvent& event) {
if (!m_context || event.entityId == 0) {
return;
}
if (auto* gameObject = m_context->GetSceneManager().GetEntity(event.entityId)) {
BeginRename(gameObject);
}
}
void HierarchyPanel::Render() {
2026-03-27 22:05:05 +08:00
ImGui::PushStyleColor(ImGuiCol_WindowBg, UI::HierarchyInspectorPanelBackgroundColor());
ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::HierarchyInspectorPanelBackgroundColor());
{
UI::PanelWindowScope panel(m_name.c_str());
if (!panel.IsOpen()) {
ImGui::PopStyleColor(2);
return;
}
2026-03-27 22:05:05 +08:00
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());
2026-03-27 22:05:05 +08:00
if (!content.IsOpen()) {
ImGui::PopStyleColor(2);
return;
}
2026-03-27 22:05:05 +08:00
auto& sceneManager = m_context->GetSceneManager();
auto rootEntities = sceneManager.GetRootEntities();
const UI::SearchQuery searchQuery(m_searchBuffer);
size_t visibleEntityCount = 0;
UI::ResetTreeLayout();
2026-03-27 22:05:05 +08:00
for (auto* gameObject : rootEntities) {
if (RenderEntity(gameObject, searchQuery)) {
++visibleEntityCount;
}
}
if (!searchQuery.Empty() && visibleEntityCount == 0) {
UI::DrawEmptyState("No matching entities", "Try a different search term");
2026-03-27 22:05:05 +08:00
}
Actions::DrawHierarchyBackgroundInteraction(*m_context, m_renameState);
Actions::DrawHierarchyEntityContextPopup(*m_context, m_itemContextMenu);
static bool s_backgroundContextOpen = false;
if (UI::BeginContextMenuForLastItem("##HierarchyBackgroundContext")) {
if (!s_backgroundContextOpen) {
Actions::TraceHierarchyPopup("Hierarchy background popup opened via background surface");
s_backgroundContextOpen = true;
}
Actions::DrawHierarchyContextActions(*m_context, nullptr, true);
UI::EndContextMenu();
} else if (s_backgroundContextOpen) {
Actions::TraceHierarchyPopup("Hierarchy background popup closed");
s_backgroundContextOpen = false;
}
}
2026-03-27 22:05:05 +08:00
ImGui::PopStyleColor(2);
}
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();
2026-03-27 22:05:05 +08:00
ImGui::PushID(static_cast<int>(gameObject->GetID()));
2026-03-26 21:18:33 +08:00
2026-03-30 00:49:10 +08:00
UI::TreeNodeDefinition nodeDefinition =
BuildHierarchyNodeDefinition(*m_context, gameObject, m_itemContextMenu);
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;
}
2026-03-30 00:49:10 +08:00
const bool editing = m_renameState.IsEditing(gameObject->GetID());
const UI::TreeNodeResult node = UI::DrawTreeNode(
searching ? nullptr : &m_treeState,
2026-03-30 00:49:10 +08:00
(void*)gameObject->GetUUID(),
editing ? "" : gameObject->GetName().c_str(),
nodeDefinition);
if (editing) {
DrawHierarchyRenameFieldAtCurrentRow(m_renameState, nodeDefinition.style);
const bool active = ImGui::IsItemActive();
const bool deactivated = ImGui::IsItemDeactivated();
const bool cancelRequested = active && ImGui::IsKeyPressed(ImGuiKey_Escape);
if (cancelRequested) {
2026-03-26 21:18:33 +08:00
CancelRename();
2026-03-30 00:49:10 +08:00
} else if (deactivated || ImGui::IsKeyPressed(ImGuiKey_Enter) || ImGui::IsKeyPressed(ImGuiKey_KeypadEnter)) {
2026-03-26 21:18:33 +08:00
CommitRename();
}
2026-03-30 00:49:10 +08:00
}
2026-03-30 00:49:10 +08:00
if (node.open) {
for (size_t i = 0; i < gameObject->GetChildCount(); i++) {
RenderEntity(gameObject->GetChild(i), searchQuery);
}
2026-03-30 00:49:10 +08:00
UI::EndTreeNode();
}
2026-03-30 00:49:10 +08:00
ImGui::PopID();
return true;
}
2026-03-26 21:18:33 +08:00
void HierarchyPanel::BeginRename(::XCEngine::Components::GameObject* gameObject) {
if (!gameObject) {
CancelRename();
return;
}
2026-03-26 21:18:33 +08:00
2026-03-26 21:30:46 +08:00
m_renameState.Begin(gameObject->GetID(), gameObject->GetName().c_str());
2026-03-26 21:18:33 +08:00
}
void HierarchyPanel::CommitRename() {
2026-03-26 21:30:46 +08:00
if (!m_renameState.IsActive()) {
return;
}
const uint64_t entityId = m_renameState.Item();
Actions::CommitEntityRename(*m_context, entityId, m_renameState.Buffer());
2026-03-26 21:18:33 +08:00
CancelRename();
}
void HierarchyPanel::CancelRename() {
2026-03-26 21:30:46 +08:00
m_renameState.Cancel();
}
}
}