#include "HierarchyPanelInternal.h" #include "Scene/EditorSceneRuntime.h" #include #include #include namespace XCEngine::UI::Editor::App { using namespace HierarchyPanelInternal; namespace TreeDrag = XCEngine::UI::Editor::Collections::TreeDragDrop; namespace { UIEditorHostCommandEvaluationResult BuildEvaluationResult( bool executable, std::string message) { UIEditorHostCommandEvaluationResult result = {}; result.executable = executable; result.message = std::move(message); return result; } UIEditorHostCommandDispatchResult BuildDispatchResult( bool commandExecuted, std::string message) { UIEditorHostCommandDispatchResult result = {}; result.commandExecuted = commandExecuted; result.message = std::move(message); return result; } bool HasValidBounds(const UIRect& bounds) { return bounds.width > 0.0f && bounds.height > 0.0f; } } // namespace void HierarchyPanel::Initialize() { SyncModelFromScene(); } void HierarchyPanel::SetSceneRuntime(EditorSceneRuntime* sceneRuntime) { if (m_sceneRuntime == sceneRuntime) { return; } m_sceneRuntime = sceneRuntime; SyncModelFromScene(); } void HierarchyPanel::SetBuiltInIcons(const BuiltInIcons* icons) { m_icons = icons; RebuildItems(); } void HierarchyPanel::ResetInteractionState() { m_treeInteractionState = {}; m_treeFrame = {}; m_dragState = {}; ClearRenameState(); ResetTransientState(); } const UIEditorPanelContentHostPanelState* HierarchyPanel::FindMountedHierarchyPanel( const UIEditorPanelContentHostFrame& contentHostFrame) const { for (const UIEditorPanelContentHostPanelState& panelState : contentHostFrame.panelStates) { if (panelState.panelId == kHierarchyPanelId && panelState.mounted) { return &panelState; } } return nullptr; } void HierarchyPanel::ResetTransientState() { m_frameEvents.clear(); TreeDrag::ResetTransientRequests(m_dragState); } void HierarchyPanel::SyncModelFromScene() { if (m_sceneRuntime != nullptr) { m_sceneRuntime->RefreshScene(); } const HierarchyModel sceneModel = HierarchyModel::BuildFromScene( m_sceneRuntime != nullptr ? m_sceneRuntime->GetActiveScene() : nullptr); if (!m_model.HasSameTree(sceneModel) || m_treeItems.empty()) { m_model = sceneModel; if (m_sceneRuntime != nullptr && !m_model.Empty()) { m_sceneRuntime->EnsureSceneSelection(); } RebuildItems(); return; } if (m_sceneRuntime != nullptr && !m_model.Empty()) { m_sceneRuntime->EnsureSceneSelection(); } SyncTreeSelectionFromSceneRuntime(); } void HierarchyPanel::RebuildItems() { const auto icon = ResolveGameObjectIcon(m_icons); m_treeItems = m_model.BuildTreeItems(icon); SyncTreeSelectionFromSceneRuntime(); } void HierarchyPanel::SyncTreeSelectionFromSceneRuntime() { if (m_sceneRuntime == nullptr) { m_treeSelection.ClearSelection(); return; } const std::string selectedItemId = m_sceneRuntime->GetSelectedItemId(); if (!selectedItemId.empty() && m_model.ContainsNode(selectedItemId)) { m_treeSelection.SetSelection(selectedItemId); return; } m_treeSelection.ClearSelection(); } void HierarchyPanel::SyncSceneRuntimeSelectionFromTree() { if (m_sceneRuntime == nullptr) { return; } if (m_treeSelection.HasSelection()) { if (!m_sceneRuntime->SetSelection(m_treeSelection.GetSelectedId())) { m_sceneRuntime->RefreshScene(); } } else { m_sceneRuntime->ClearSelection(); } if (!m_model.Empty()) { m_sceneRuntime->EnsureSceneSelection(); } SyncTreeSelectionFromSceneRuntime(); } const HierarchyNode* HierarchyPanel::GetSelectedNode() const { if (m_sceneRuntime != nullptr) { const std::string selectedItemId = m_sceneRuntime->GetSelectedItemId(); if (!selectedItemId.empty()) { return m_model.FindNode(selectedItemId); } } if (!m_treeSelection.HasSelection()) { return nullptr; } return m_model.FindNode(m_treeSelection.GetSelectedId()); } void HierarchyPanel::EmitSelectionEvent() { Event event = {}; event.kind = EventKind::SelectionChanged; if (const HierarchyNode* node = GetSelectedNode(); node != nullptr) { event.itemId = node->nodeId; event.label = node->label; } m_frameEvents.push_back(std::move(event)); } void HierarchyPanel::EmitReparentEvent( EventKind kind, std::string itemId, std::string targetItemId) { Event event = {}; event.kind = kind; event.itemId = std::move(itemId); event.targetItemId = std::move(targetItemId); if (const HierarchyNode* node = m_model.FindNode(event.itemId); node != nullptr) { event.label = node->label; } m_frameEvents.push_back(std::move(event)); } void HierarchyPanel::EmitRenameRequestedEvent(std::string_view itemId) { Event event = {}; event.kind = EventKind::RenameRequested; event.itemId = std::string(itemId); if (const HierarchyNode* node = m_model.FindNode(itemId); node != nullptr) { event.label = node->label; } m_frameEvents.push_back(std::move(event)); } void HierarchyPanel::ClearRenameState() { m_renameState = {}; m_renameFrame = {}; m_pendingRenameItemId.clear(); } void HierarchyPanel::QueueRenameSession(std::string_view itemId) { if (itemId.empty() || !m_model.ContainsNode(itemId)) { return; } if (m_renameState.active && m_renameState.itemId == itemId) { return; } m_pendingRenameItemId = std::string(itemId); } bool HierarchyPanel::TryStartQueuedRenameSession( const Widgets::UIEditorTreeViewLayout& layout) { if (m_pendingRenameItemId.empty()) { return false; } const HierarchyNode* node = m_model.FindNode(m_pendingRenameItemId); if (node == nullptr) { m_pendingRenameItemId.clear(); return false; } const UIRect bounds = BuildRenameBounds(m_pendingRenameItemId, layout); if (!HasValidBounds(bounds)) { return false; } const Widgets::UIEditorTextFieldMetrics textFieldMetrics = BuildUIEditorInlineRenameTextFieldMetrics( bounds, BuildUIEditorPropertyGridTextFieldMetrics( ResolveUIEditorPropertyGridMetrics(), ResolveUIEditorTextFieldMetrics())); UIEditorInlineRenameSessionRequest request = {}; request.beginSession = true; request.itemId = m_pendingRenameItemId; request.initialText = node->label; request.bounds = bounds; m_renameFrame = UpdateUIEditorInlineRenameSession( m_renameState, request, {}, textFieldMetrics); if (m_renameFrame.result.sessionStarted) { m_pendingRenameItemId.clear(); return true; } return false; } void HierarchyPanel::UpdateRenameSession( const std::vector& inputEvents, const Widgets::UIEditorTreeViewLayout& layout) { if (!m_renameState.active) { return; } if (!m_model.ContainsNode(m_renameState.itemId)) { ClearRenameState(); return; } const UIRect bounds = BuildRenameBounds(m_renameState.itemId, layout); if (!HasValidBounds(bounds)) { ClearRenameState(); return; } const Widgets::UIEditorTextFieldMetrics textFieldMetrics = BuildUIEditorInlineRenameTextFieldMetrics( bounds, BuildUIEditorPropertyGridTextFieldMetrics( ResolveUIEditorPropertyGridMetrics(), ResolveUIEditorTextFieldMetrics())); UIEditorInlineRenameSessionRequest request = {}; request.itemId = m_renameState.itemId; request.initialText = m_renameState.textFieldSpec.value; request.bounds = bounds; m_renameFrame = UpdateUIEditorInlineRenameSession( m_renameState, request, inputEvents, textFieldMetrics); if (!m_renameFrame.result.sessionCommitted) { return; } if (m_renameFrame.result.valueChanged && m_sceneRuntime != nullptr) { m_sceneRuntime->RenameGameObject( m_renameFrame.result.itemId, m_renameFrame.result.valueAfter); } SyncModelFromScene(); EmitSelectionEvent(); } void HierarchyPanel::SyncTreeFocusState( const std::vector& inputEvents) { for (const UIInputEvent& event : inputEvents) { if (event.type == ::XCEngine::UI::UIInputEventType::FocusGained) { m_treeInteractionState.treeViewState.focused = true; } else if (event.type == ::XCEngine::UI::UIInputEventType::FocusLost) { m_treeInteractionState.treeViewState.focused = false; } } } UIRect HierarchyPanel::BuildRenameBounds( std::string_view itemId, const Widgets::UIEditorTreeViewLayout& layout) const { if (itemId.empty()) { return {}; } const std::size_t visibleIndex = FindVisibleIndexForItemId(layout, m_treeItems, itemId); if (visibleIndex == UIEditorTreeViewInvalidIndex || visibleIndex >= layout.rowRects.size() || visibleIndex >= layout.labelRects.size()) { return {}; } const Widgets::UIEditorTextFieldMetrics hostedMetrics = BuildUIEditorPropertyGridTextFieldMetrics( ResolveUIEditorPropertyGridMetrics(), ResolveUIEditorTextFieldMetrics()); const UIRect& rowRect = layout.rowRects[visibleIndex]; const UIRect& labelRect = layout.labelRects[visibleIndex]; const float x = (std::max)(rowRect.x, labelRect.x - hostedMetrics.valueTextInsetX); const float right = rowRect.x + rowRect.width - 8.0f; const float width = (std::max)(120.0f, right - x); return UIRect(x, rowRect.y, width, rowRect.height); } bool HierarchyPanel::WantsHostPointerCapture() const { return m_dragState.requestPointerCapture; } bool HierarchyPanel::WantsHostPointerRelease() const { return m_dragState.requestPointerRelease; } bool HierarchyPanel::HasActivePointerCapture() const { return TreeDrag::HasActivePointerCapture(m_dragState); } const std::vector& HierarchyPanel::GetFrameEvents() const { return m_frameEvents; } UIEditorHostCommandEvaluationResult HierarchyPanel::EvaluateEditCommand( std::string_view commandId) const { const HierarchyNode* selectedNode = GetSelectedNode(); if (selectedNode == nullptr) { return BuildEvaluationResult(false, "Select a hierarchy object first."); } if (commandId == "edit.rename") { return BuildEvaluationResult( true, "Rename hierarchy object '" + selectedNode->label + "'."); } if (commandId == "edit.delete") { return BuildEvaluationResult( true, "Delete hierarchy object '" + selectedNode->label + "'."); } if (commandId == "edit.duplicate") { return BuildEvaluationResult( true, "Duplicate hierarchy object '" + selectedNode->label + "'."); } if (commandId == "edit.cut" || commandId == "edit.copy" || commandId == "edit.paste") { return BuildEvaluationResult( false, "Hierarchy clipboard has no bound transfer owner in the current shell."); } return BuildEvaluationResult(false, "Hierarchy does not expose this edit command."); } UIEditorHostCommandDispatchResult HierarchyPanel::DispatchEditCommand( std::string_view commandId) { const UIEditorHostCommandEvaluationResult evaluation = EvaluateEditCommand(commandId); if (!evaluation.executable) { return BuildDispatchResult(false, evaluation.message); } const HierarchyNode* selectedNode = GetSelectedNode(); if (selectedNode == nullptr) { return BuildDispatchResult(false, "Select a hierarchy object first."); } const std::string selectedNodeId = selectedNode->nodeId; const std::string selectedNodeLabel = selectedNode->label; if (m_sceneRuntime == nullptr) { return BuildDispatchResult(false, "Hierarchy scene runtime is unavailable."); } if (commandId == "edit.rename") { QueueRenameSession(selectedNodeId); EmitRenameRequestedEvent(selectedNodeId); if (m_visible) { TryStartQueuedRenameSession(m_treeFrame.layout); } return BuildDispatchResult( true, "Hierarchy rename requested for '" + selectedNodeLabel + "'."); } if (commandId == "edit.delete") { if (!m_sceneRuntime->DeleteGameObject(selectedNodeId)) { return BuildDispatchResult(false, "Failed to delete the selected hierarchy object."); } SyncModelFromScene(); EmitSelectionEvent(); return BuildDispatchResult( true, "Deleted hierarchy object '" + selectedNodeLabel + "'."); } if (commandId == "edit.duplicate") { const std::string duplicatedNodeId = m_sceneRuntime->DuplicateGameObject(selectedNodeId); if (duplicatedNodeId.empty()) { return BuildDispatchResult(false, "Failed to duplicate the selected hierarchy object."); } SyncModelFromScene(); EmitSelectionEvent(); const HierarchyNode* duplicatedNode = m_model.FindNode(duplicatedNodeId); const std::string duplicatedLabel = duplicatedNode != nullptr ? duplicatedNode->label : selectedNodeLabel; return BuildDispatchResult( true, "Duplicated hierarchy object '" + duplicatedLabel + "'."); } return BuildDispatchResult(false, "Hierarchy does not expose this edit command."); } void HierarchyPanel::ProcessDragAndFrameEvents( const std::vector& inputEvents, const UIRect& bounds, bool allowInteraction, bool panelActive) { const std::vector filteredEvents = FilterHierarchyInputEvents( bounds, inputEvents, allowInteraction, panelActive, HasActivePointerCapture()); if (m_treeFrame.result.selectionChanged) { SyncSceneRuntimeSelectionFromTree(); EmitSelectionEvent(); } struct HierarchyTreeDragCallbacks { ::XCEngine::UI::Widgets::UISelectionModel& selection; HierarchyModel& model; EditorSceneRuntime* sceneRuntime = nullptr; bool IsItemSelected(std::string_view itemId) const { return selection.IsSelected(itemId); } bool SelectDraggedItem(std::string_view itemId) { return selection.SetSelection(std::string(itemId)); } bool CanDropOnItem( std::string_view draggedItemId, std::string_view targetItemId) const { return model.CanReparent(draggedItemId, targetItemId); } bool CanDropToRoot(std::string_view draggedItemId) const { return model.GetParentId(draggedItemId).has_value(); } bool CommitDropOnItem( std::string_view draggedItemId, std::string_view targetItemId) { return sceneRuntime != nullptr && sceneRuntime->ReparentGameObject(draggedItemId, targetItemId); } bool CommitDropToRoot(std::string_view draggedItemId) { return sceneRuntime != nullptr && sceneRuntime->MoveGameObjectToRoot(draggedItemId); } } callbacks{ m_treeSelection, m_model, m_sceneRuntime }; const TreeDrag::ProcessResult dragResult = TreeDrag::ProcessInputEvents( m_dragState, m_treeFrame.layout, m_treeItems, filteredEvents, bounds, callbacks, kDragThreshold); if (dragResult.selectionForced) { SyncSceneRuntimeSelectionFromTree(); EmitSelectionEvent(); } if (dragResult.dropCommitted) { SyncModelFromScene(); m_treeFrame.layout = Widgets::BuildUIEditorTreeViewLayout( bounds, m_treeItems, m_expansion, ResolveUIEditorTreeViewMetrics()); EmitReparentEvent( dragResult.droppedToRoot ? EventKind::MovedToRoot : EventKind::Reparented, dragResult.draggedItemId, dragResult.dropTargetItemId); } } void HierarchyPanel::Update( const UIEditorPanelContentHostFrame& contentHostFrame, const std::vector& inputEvents, bool allowInteraction, bool panelActive) { ResetTransientState(); const UIEditorPanelContentHostPanelState* panelState = FindMountedHierarchyPanel(contentHostFrame); if (panelState == nullptr) { m_visible = false; m_treeFrame = {}; m_dragState = {}; ClearRenameState(); return; } SyncModelFromScene(); if (m_renameState.active && !m_model.ContainsNode(m_renameState.itemId)) { ClearRenameState(); } else if (!m_pendingRenameItemId.empty() && !m_model.ContainsNode(m_pendingRenameItemId)) { m_pendingRenameItemId.clear(); } m_visible = true; const std::vector filteredEvents = FilterHierarchyInputEvents( panelState->bounds, inputEvents, allowInteraction, panelActive, HasActivePointerCapture()); SyncTreeFocusState(filteredEvents); const Widgets::UIEditorTreeViewMetrics treeMetrics = ResolveUIEditorTreeViewMetrics(); const Widgets::UIEditorTreeViewLayout layout = Widgets::BuildUIEditorTreeViewLayout( panelState->bounds, m_treeItems, m_expansion, treeMetrics); if (m_renameState.active || !m_pendingRenameItemId.empty()) { m_treeFrame.layout = layout; m_treeFrame.result = {}; TryStartQueuedRenameSession(layout); UpdateRenameSession(filteredEvents, layout); return; } const std::vector interactionEvents = TreeDrag::BuildInteractionInputEvents( m_dragState, layout, m_treeItems, filteredEvents, kDragThreshold); m_treeFrame = UpdateUIEditorTreeViewInteraction( m_treeInteractionState, m_treeSelection, m_expansion, panelState->bounds, m_treeItems, interactionEvents, treeMetrics); if (m_treeFrame.result.renameRequested && !m_treeFrame.result.renameItemId.empty()) { QueueRenameSession(m_treeFrame.result.renameItemId); EmitRenameRequestedEvent(m_treeFrame.result.renameItemId); TryStartQueuedRenameSession(m_treeFrame.layout); return; } ProcessDragAndFrameEvents( inputEvents, panelState->bounds, allowInteraction, panelActive); } void HierarchyPanel::Append(UIDrawList& drawList) const { if (!m_visible || m_treeFrame.layout.bounds.width <= 0.0f || m_treeFrame.layout.bounds.height <= 0.0f) { return; } const Widgets::UIEditorTreeViewPalette palette = ResolveUIEditorTreeViewPalette(); const Widgets::UIEditorTreeViewMetrics metrics = ResolveUIEditorTreeViewMetrics(); AppendUIEditorTreeViewBackground( drawList, m_treeFrame.layout, m_treeItems, m_treeSelection, m_treeInteractionState.treeViewState, palette, metrics); AppendUIEditorTreeViewForeground( drawList, m_treeFrame.layout, m_treeItems, palette, metrics); if (m_renameState.active) { const Widgets::UIEditorTextFieldPalette textFieldPalette = BuildUIEditorPropertyGridTextFieldPalette( ResolveUIEditorPropertyGridPalette(), ResolveUIEditorTextFieldPalette()); const Widgets::UIEditorTextFieldMetrics textFieldMetrics = BuildUIEditorInlineRenameTextFieldMetrics( BuildRenameBounds(m_renameState.itemId, m_treeFrame.layout), BuildUIEditorPropertyGridTextFieldMetrics( ResolveUIEditorPropertyGridMetrics(), ResolveUIEditorTextFieldMetrics())); Widgets::AppendUIEditorTextFieldBackground( drawList, m_renameFrame.layout, m_renameState.textFieldSpec, m_renameState.textFieldInteraction.textFieldState, textFieldPalette, textFieldMetrics); Widgets::AppendUIEditorTextFieldForeground( drawList, m_renameFrame.layout, m_renameState.textFieldSpec, m_renameState.textFieldInteraction.textFieldState, textFieldPalette, textFieldMetrics); } if (!m_dragState.dragging || !m_dragState.validDropTarget) { return; } if (m_dragState.dropToRoot) { drawList.AddRectOutline( m_treeFrame.layout.bounds, kDragPreviewColor, 1.0f, 0.0f); return; } const std::size_t visibleIndex = TreeDrag::FindVisibleIndexForItemId( m_treeFrame.layout, m_treeItems, m_dragState.dropTargetItemId); if (visibleIndex == UIEditorTreeViewInvalidIndex || visibleIndex >= m_treeFrame.layout.rowRects.size()) { return; } drawList.AddRectOutline( m_treeFrame.layout.rowRects[visibleIndex], kDragPreviewColor, 1.0f, 0.0f); } } // namespace XCEngine::UI::Editor::App