#include "HierarchyPanelInternal.h" #include namespace XCEngine::UI::Editor::App { using namespace HierarchyPanelInternal; 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; } } // namespace void HierarchyPanel::Initialize() { m_model = HierarchyModel::BuildDefault(); RebuildItems(); } void HierarchyPanel::SetBuiltInIcons(const BuiltInIcons* icons) { m_icons = icons; RebuildItems(); } void HierarchyPanel::ResetInteractionState() { m_treeInteractionState = {}; m_treeFrame = {}; m_dragState = {}; 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(); m_dragState.requestPointerCapture = false; m_dragState.requestPointerRelease = false; } void HierarchyPanel::RebuildItems() { const auto icon = ResolveGameObjectIcon(m_icons); const std::string previousSelection = m_selection.HasSelection() ? m_selection.GetSelectedId() : std::string(); m_treeItems = m_model.BuildTreeItems(icon); m_expansion.Expand("player"); m_expansion.Expand("environment"); m_expansion.Expand("props"); if (!previousSelection.empty() && m_model.ContainsNode(previousSelection)) { m_selection.SetSelection(previousSelection); return; } if (!m_treeItems.empty()) { m_selection.SetSelection(m_treeItems.front().itemId); return; } m_selection.ClearSelection(); } const HierarchyNode* HierarchyPanel::GetSelectedNode() const { if (!m_selection.HasSelection()) { return nullptr; } return m_model.FindNode(m_selection.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)); } bool HierarchyPanel::WantsHostPointerCapture() const { return m_dragState.requestPointerCapture; } bool HierarchyPanel::WantsHostPointerRelease() const { return m_dragState.requestPointerRelease; } bool HierarchyPanel::HasActivePointerCapture() const { return m_dragState.dragging; } 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 (commandId == "edit.rename") { EmitRenameRequestedEvent(selectedNodeId); return BuildDispatchResult( true, "Hierarchy rename requested for '" + selectedNodeLabel + "'."); } if (commandId == "edit.delete") { if (!m_model.DeleteNode(selectedNodeId)) { return BuildDispatchResult(false, "Failed to delete the selected hierarchy object."); } RebuildItems(); EmitSelectionEvent(); return BuildDispatchResult( true, "Deleted hierarchy object '" + selectedNodeLabel + "'."); } if (commandId == "edit.duplicate") { const std::string duplicatedNodeId = m_model.DuplicateNode(selectedNodeId); if (duplicatedNodeId.empty()) { return BuildDispatchResult(false, "Failed to duplicate the selected hierarchy object."); } RebuildItems(); m_selection.SetSelection(duplicatedNodeId); 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."); } std::vector HierarchyPanel::BuildInteractionInputEvents( const std::vector& inputEvents, const UIRect& bounds, bool allowInteraction, bool panelActive) const { const std::vector rawEvents = FilterHierarchyInputEvents( bounds, inputEvents, allowInteraction, panelActive, HasActivePointerCapture()); struct DragPreviewState { std::string armedItemId = {}; UIPoint pressPosition = {}; bool armed = false; bool dragging = false; }; const Widgets::UIEditorTreeViewLayout layout = m_treeFrame.layout.bounds.width > 0.0f ? m_treeFrame.layout : Widgets::BuildUIEditorTreeViewLayout( bounds, m_treeItems, m_expansion, ResolveUIEditorTreeViewMetrics()); DragPreviewState preview = {}; preview.armed = m_dragState.armed; preview.armedItemId = m_dragState.armedItemId; preview.pressPosition = m_dragState.pressPosition; preview.dragging = m_dragState.dragging; std::vector filteredEvents = {}; filteredEvents.reserve(rawEvents.size()); for (const UIInputEvent& event : rawEvents) { bool suppress = false; switch (event.type) { case UIInputEventType::PointerButtonDown: if (event.pointerButton == UIPointerButton::Left) { UIEditorTreeViewHitTarget hitTarget = {}; const Widgets::UIEditorTreeViewItem* hitItem = ResolveHitItem(layout, m_treeItems, event.position, &hitTarget); if (hitItem != nullptr && hitTarget.kind == UIEditorTreeViewHitTargetKind::Row) { preview.armed = true; preview.armedItemId = hitItem->itemId; preview.pressPosition = event.position; } else { preview.armed = false; preview.armedItemId.clear(); } } if (preview.dragging) { suppress = true; } break; case UIInputEventType::PointerMove: if (preview.dragging) { suppress = true; break; } if (preview.armed && ComputeSquaredDistance(event.position, preview.pressPosition) >= kDragThreshold * kDragThreshold) { preview.dragging = true; suppress = true; } break; case UIInputEventType::PointerButtonUp: if (event.pointerButton == UIPointerButton::Left) { if (preview.dragging) { suppress = true; preview.dragging = false; } preview.armed = false; preview.armedItemId.clear(); } else if (preview.dragging) { suppress = true; } break; case UIInputEventType::PointerLeave: if (preview.dragging) { suppress = true; } break; case UIInputEventType::FocusLost: preview.armed = false; preview.dragging = false; preview.armedItemId.clear(); break; default: break; } if (!suppress) { filteredEvents.push_back(event); } } return filteredEvents; } 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) { EmitSelectionEvent(); } if (m_treeFrame.result.renameRequested && !m_treeFrame.result.renameItemId.empty()) { Event event = {}; event.kind = EventKind::RenameRequested; event.itemId = m_treeFrame.result.renameItemId; if (const HierarchyNode* node = m_model.FindNode(event.itemId); node != nullptr) { event.label = node->label; } m_frameEvents.push_back(std::move(event)); } for (const UIInputEvent& event : filteredEvents) { switch (event.type) { case UIInputEventType::PointerButtonDown: if (event.pointerButton == UIPointerButton::Left) { UIEditorTreeViewHitTarget hitTarget = {}; const Widgets::UIEditorTreeViewItem* hitItem = ResolveHitItem(m_treeFrame.layout, m_treeItems, event.position, &hitTarget); if (hitItem != nullptr && hitTarget.kind == UIEditorTreeViewHitTargetKind::Row) { m_dragState.armed = true; m_dragState.armedItemId = hitItem->itemId; m_dragState.pressPosition = event.position; } else { m_dragState.armed = false; m_dragState.armedItemId.clear(); } } break; case UIInputEventType::PointerMove: if (m_dragState.armed && !m_dragState.dragging && ComputeSquaredDistance(event.position, m_dragState.pressPosition) >= kDragThreshold * kDragThreshold) { m_dragState.dragging = !m_dragState.armedItemId.empty(); m_dragState.draggedItemId = m_dragState.armedItemId; m_dragState.dropTargetItemId.clear(); m_dragState.dropToRoot = false; m_dragState.validDropTarget = false; if (m_dragState.dragging) { m_dragState.requestPointerCapture = true; if (!m_selection.IsSelected(m_dragState.draggedItemId)) { m_selection.SetSelection(m_dragState.draggedItemId); EmitSelectionEvent(); } } } if (m_dragState.dragging) { UIEditorTreeViewHitTarget hitTarget = {}; const Widgets::UIEditorTreeViewItem* hitItem = ResolveHitItem(m_treeFrame.layout, m_treeItems, event.position, &hitTarget); m_dragState.dropTargetItemId.clear(); m_dragState.dropToRoot = false; m_dragState.validDropTarget = false; if (hitItem != nullptr && (hitTarget.kind == UIEditorTreeViewHitTargetKind::Row || hitTarget.kind == UIEditorTreeViewHitTargetKind::Disclosure)) { m_dragState.dropTargetItemId = hitItem->itemId; m_dragState.validDropTarget = m_model.CanReparent(m_dragState.draggedItemId, m_dragState.dropTargetItemId); } else if (ContainsPoint(bounds, event.position)) { m_dragState.dropToRoot = true; m_dragState.validDropTarget = m_model.GetParentId(m_dragState.draggedItemId).has_value(); } } break; case UIInputEventType::PointerButtonUp: if (event.pointerButton != UIPointerButton::Left) { break; } if (m_dragState.dragging) { if (m_dragState.validDropTarget) { const std::string draggedItemId = m_dragState.draggedItemId; const std::string dropTargetItemId = m_dragState.dropTargetItemId; const bool changed = m_dragState.dropToRoot ? m_model.MoveToRoot(draggedItemId) : m_model.Reparent(draggedItemId, dropTargetItemId); if (changed) { RebuildItems(); EmitReparentEvent( m_dragState.dropToRoot ? EventKind::MovedToRoot : EventKind::Reparented, draggedItemId, dropTargetItemId); } } m_dragState.armed = false; m_dragState.dragging = false; m_dragState.armedItemId.clear(); m_dragState.draggedItemId.clear(); m_dragState.dropTargetItemId.clear(); m_dragState.dropToRoot = false; m_dragState.validDropTarget = false; m_dragState.requestPointerRelease = true; } else { m_dragState.armed = false; m_dragState.armedItemId.clear(); } break; case UIInputEventType::FocusLost: if (m_dragState.dragging) { m_dragState.requestPointerRelease = true; } m_dragState = {}; break; default: break; } } } 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 = {}; return; } if (m_treeItems.empty()) { RebuildItems(); } m_visible = true; const std::vector interactionEvents = BuildInteractionInputEvents( inputEvents, panelState->bounds, allowInteraction, panelActive); m_treeFrame = UpdateUIEditorTreeViewInteraction( m_treeInteractionState, m_selection, m_expansion, panelState->bounds, m_treeItems, interactionEvents, ResolveUIEditorTreeViewMetrics()); 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_selection, m_treeInteractionState.treeViewState, palette, metrics); AppendUIEditorTreeViewForeground( drawList, m_treeFrame.layout, m_treeItems, palette, metrics); 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 = 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