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"
|
2026-03-20 17:08:06 +08:00
|
|
|
#include "HierarchyPanel.h"
|
2026-03-25 15:51:27 +08:00
|
|
|
#include "Core/IEditorContext.h"
|
2026-03-25 16:39:15 +08:00
|
|
|
#include "Core/ISceneManager.h"
|
2026-03-25 16:41:21 +08:00
|
|
|
#include "Core/ISelectionManager.h"
|
|
|
|
|
#include "Core/EditorEvents.h"
|
|
|
|
|
#include "Core/EventBus.h"
|
2026-03-26 21:18:33 +08:00
|
|
|
#include "UI/UI.h"
|
2026-03-20 17:08:06 +08:00
|
|
|
#include <imgui.h>
|
|
|
|
|
|
2026-03-27 23:21:43 +08:00
|
|
|
namespace {
|
|
|
|
|
|
|
|
|
|
void DrawHierarchyTreePrefix(const XCEngine::Editor::UI::TreeNodePrefixContext& context) {
|
|
|
|
|
if (!context.drawList) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-28 15:07:19 +08:00
|
|
|
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-27 23:21:43 +08:00
|
|
|
}
|
|
|
|
|
|
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;
|
2026-03-31 21:26:40 +08:00
|
|
|
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());
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-27 23:21:43 +08:00
|
|
|
} // namespace
|
|
|
|
|
|
2026-03-24 20:02:38 +08:00
|
|
|
namespace XCEngine {
|
|
|
|
|
namespace Editor {
|
2026-03-20 17:08:06 +08:00
|
|
|
|
|
|
|
|
HierarchyPanel::HierarchyPanel() : Panel("Hierarchy") {
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-27 00:08:46 +08:00
|
|
|
void HierarchyPanel::OnAttach() {
|
|
|
|
|
if (!m_context || m_selectionHandlerId || m_renameRequestHandlerId) {
|
|
|
|
|
return;
|
2026-03-25 16:41:21 +08:00
|
|
|
}
|
2026-03-25 15:51:27 +08:00
|
|
|
|
2026-03-25 16:41:21 +08:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
);
|
2026-03-25 16:41:21 +08:00
|
|
|
}
|
|
|
|
|
|
2026-03-27 00:08:46 +08:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-25 16:41:21 +08:00
|
|
|
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-20 17:08:06 +08:00
|
|
|
}
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 17:08:06 +08:00
|
|
|
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-26 16:43:06 +08:00
|
|
|
|
2026-03-27 22:05:05 +08:00
|
|
|
Actions::ObserveFocusedActionRoute(*m_context, EditorActionRoute::Hierarchy);
|
2026-03-26 16:43:06 +08:00
|
|
|
|
2026-03-28 15:07:19 +08:00
|
|
|
UI::PanelContentScope content("EntityList", UI::HierarchyPanelContentPadding());
|
2026-03-27 22:05:05 +08:00
|
|
|
if (!content.IsOpen()) {
|
|
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-03-27 12:06:24 +08:00
|
|
|
|
2026-03-27 22:05:05 +08:00
|
|
|
auto& sceneManager = m_context->GetSceneManager();
|
|
|
|
|
auto rootEntities = sceneManager.GetRootEntities();
|
2026-03-28 15:07:19 +08:00
|
|
|
UI::ResetTreeLayout();
|
2026-03-27 12:06:24 +08:00
|
|
|
|
2026-03-27 22:05:05 +08:00
|
|
|
for (auto* gameObject : rootEntities) {
|
|
|
|
|
RenderEntity(gameObject);
|
|
|
|
|
}
|
2026-03-20 17:08:06 +08:00
|
|
|
|
2026-03-29 15:12:38 +08:00
|
|
|
Actions::DrawHierarchyBackgroundInteraction(*m_context, m_renameState);
|
2026-03-28 00:03:20 +08:00
|
|
|
Actions::DrawHierarchyEntityContextPopup(*m_context, m_itemContextMenu);
|
2026-03-29 15:12:38 +08:00
|
|
|
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::DrawHierarchyCreateActions(*m_context, nullptr);
|
|
|
|
|
UI::EndContextMenu();
|
|
|
|
|
} else if (s_backgroundContextOpen) {
|
|
|
|
|
Actions::TraceHierarchyPopup("Hierarchy background popup closed");
|
|
|
|
|
s_backgroundContextOpen = false;
|
|
|
|
|
}
|
2026-03-26 16:43:06 +08:00
|
|
|
}
|
2026-03-27 22:05:05 +08:00
|
|
|
ImGui::PopStyleColor(2);
|
2026-03-20 17:08:06 +08:00
|
|
|
}
|
|
|
|
|
|
2026-03-27 22:05:05 +08:00
|
|
|
void HierarchyPanel::RenderEntity(::XCEngine::Components::GameObject* gameObject) {
|
2026-03-25 01:23:08 +08:00
|
|
|
if (!gameObject) return;
|
2026-03-27 22:05:05 +08:00
|
|
|
|
2026-03-25 01:23:08 +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 = std::to_string(gameObject->GetUUID());
|
|
|
|
|
nodeDefinition.persistenceKey = persistenceKey;
|
|
|
|
|
const bool editing = m_renameState.IsEditing(gameObject->GetID());
|
|
|
|
|
const UI::TreeNodeResult node = UI::DrawTreeNode(
|
|
|
|
|
&m_treeState,
|
|
|
|
|
(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-20 17:08:06 +08:00
|
|
|
}
|
2026-03-30 00:49:10 +08:00
|
|
|
}
|
2026-03-27 23:21:43 +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));
|
2026-03-20 17:08:06 +08:00
|
|
|
}
|
2026-03-30 00:49:10 +08:00
|
|
|
UI::EndTreeNode();
|
2026-03-20 17:08:06 +08:00
|
|
|
}
|
2026-03-30 00:49:10 +08:00
|
|
|
|
2026-03-20 17:08:06 +08:00
|
|
|
ImGui::PopID();
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 21:18:33 +08:00
|
|
|
void HierarchyPanel::BeginRename(::XCEngine::Components::GameObject* gameObject) {
|
|
|
|
|
if (!gameObject) {
|
|
|
|
|
CancelRename();
|
|
|
|
|
return;
|
2026-03-20 17:08:06 +08:00
|
|
|
}
|
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();
|
2026-03-27 12:06:24 +08:00
|
|
|
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();
|
2026-03-20 17:08:06 +08:00
|
|
|
}
|
|
|
|
|
|
2026-03-24 18:38:01 +08:00
|
|
|
}
|
2026-03-24 20:02:38 +08:00
|
|
|
}
|