#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 #include 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); } 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::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()); if (!content.IsOpen()) { ImGui::PopStyleColor(2); return; } auto& sceneManager = m_context->GetSceneManager(); auto rootEntities = sceneManager.GetRootEntities(); const UI::SearchQuery searchQuery(m_searchBuffer); size_t visibleEntityCount = 0; UI::ResetTreeLayout(); for (auto* gameObject : rootEntities) { if (RenderEntity(gameObject, searchQuery)) { ++visibleEntityCount; } } if (!searchQuery.Empty() && visibleEntityCount == 0) { UI::DrawEmptyState("No matching entities", "Try a different search term"); } 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; } } 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(); ImGui::PushID(static_cast(gameObject->GetID())); 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; } const bool editing = m_renameState.IsEditing(gameObject->GetID()); const UI::TreeNodeResult node = UI::DrawTreeNode( searching ? nullptr : &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), searchQuery); } UI::EndTreeNode(); } ImGui::PopID(); return true; } 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(); } } }