#include "Actions/HierarchyActionRouter.h" #include "Actions/ActionRouting.h" #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" #include "UI/UI.h" #include namespace { 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); } XCEngine::Editor::UI::TreeNodeDefinition BuildHierarchyNodeDefinition( XCEngine::Editor::IEditorContext& context, XCEngine::Components::GameObject* gameObject, XCEngine::Editor::UI::TargetedPopupState& itemContextMenu) { XCEngine::Editor::UI::TreeNodeDefinition nodeDefinition; nodeDefinition.options.selected = context.GetSelectionManager().IsSelected(gameObject->GetID()); nodeDefinition.options.leaf = gameObject->GetChildCount() == 0; nodeDefinition.options.openOnDoubleClick = false; 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& 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( [this](const SelectionChangedEvent& event) { OnSelectionChanged(event); } ); m_renameRequestHandlerId = m_context->GetEventBus().Subscribe( [this](const EntityRenameRequestedEvent& event) { OnRenameRequested(event); } ); } void HierarchyPanel::OnDetach() { if (!m_context) { return; } if (m_selectionHandlerId) { m_context->GetEventBus().Unsubscribe(m_selectionHandlerId); m_selectionHandlerId = 0; } if (m_renameRequestHandlerId) { m_context->GetEventBus().Unsubscribe(m_renameRequestHandlerId); m_renameRequestHandlerId = 0; } } void HierarchyPanel::OnSelectionChanged(const SelectionChangedEvent& event) { if (m_renameState.IsActive() && event.primarySelection != m_renameState.Item()) { CancelRename(); } } 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() { 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; } Actions::ObserveFocusedActionRoute(*m_context, EditorActionRoute::Hierarchy); UI::PanelContentScope content("EntityList", UI::HierarchyPanelContentPadding()); if (!content.IsOpen()) { ImGui::PopStyleColor(2); return; } auto& sceneManager = m_context->GetSceneManager(); auto rootEntities = sceneManager.GetRootEntities(); UI::ResetTreeLayout(); for (auto* gameObject : rootEntities) { RenderEntity(gameObject); } 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::DrawHierarchyCreateActions(*m_context, nullptr); UI::EndContextMenu(); } else if (s_backgroundContextOpen) { Actions::TraceHierarchyPopup("Hierarchy background popup closed"); s_backgroundContextOpen = false; } } ImGui::PopStyleColor(2); } void HierarchyPanel::RenderEntity(::XCEngine::Components::GameObject* gameObject) { if (!gameObject) return; ImGui::PushID(static_cast(gameObject->GetID())); 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) { CancelRename(); } else if (deactivated || ImGui::IsKeyPressed(ImGuiKey_Enter) || ImGui::IsKeyPressed(ImGuiKey_KeypadEnter)) { CommitRename(); } } if (node.open) { for (size_t i = 0; i < gameObject->GetChildCount(); i++) { RenderEntity(gameObject->GetChild(i)); } UI::EndTreeNode(); } ImGui::PopID(); } void HierarchyPanel::BeginRename(::XCEngine::Components::GameObject* gameObject) { if (!gameObject) { CancelRename(); return; } m_renameState.Begin(gameObject->GetID(), gameObject->GetName().c_str()); } void HierarchyPanel::CommitRename() { if (!m_renameState.IsActive()) { return; } const uint64_t entityId = m_renameState.Item(); Actions::CommitEntityRename(*m_context, entityId, m_renameState.Buffer()); CancelRename(); } void HierarchyPanel::CancelRename() { m_renameState.Cancel(); } } }