#pragma once #include #include #include #include #include #include #include #include namespace XCEngine::UI::Editor::Collections::TreeDragDrop { using ::XCEngine::UI::UIPoint; using ::XCEngine::UI::UIRect; using DragDropInteraction::kDefaultDragThreshold; using Widgets::HitTestUIEditorTreeView; using Widgets::UIEditorTreeViewHitTarget; using Widgets::UIEditorTreeViewHitTargetKind; using Widgets::UIEditorTreeViewInvalidIndex; using ::XCEngine::UI::Editor::UIEditorTreeViewDropPreviewPlacement; struct State : DragDropInteraction::State { bool dropToRoot = false; UIEditorTreeViewDropPreviewPlacement dropPlacement = UIEditorTreeViewDropPreviewPlacement::None; }; struct ProcessResult : DragDropInteraction::ProcessResult { bool droppedToRoot = false; }; inline void ResetTransientRequests(State& state) { DragDropInteraction::ResetTransientRequests(state); } inline bool HasActivePointerCapture(const State& state) { return DragDropInteraction::HasActivePointerCapture(state); } inline bool ContainsPoint(const UIRect& rect, const UIPoint& point) { return point.x >= rect.x && point.x <= rect.x + rect.width && point.y >= rect.y && point.y <= rect.y + rect.height; } template inline constexpr bool SupportsSiblingInsertion() { return requires( Callbacks& callbacks, std::string_view draggedItemId, std::string_view targetItemId) { { callbacks.CanInsertBeforeItem(draggedItemId, targetItemId) } -> std::convertible_to; { callbacks.CanInsertAfterItem(draggedItemId, targetItemId) } -> std::convertible_to; { callbacks.CommitInsertBeforeItem(draggedItemId, targetItemId) } -> std::convertible_to; { callbacks.CommitInsertAfterItem(draggedItemId, targetItemId) } -> std::convertible_to; }; } inline UIEditorTreeViewDropPreviewPlacement ResolveRowDropPlacement( const Widgets::UIEditorTreeViewLayout& layout, const UIEditorTreeViewHitTarget& hitTarget, const UIPoint& point) { if (hitTarget.visibleIndex >= layout.rowRects.size()) { return UIEditorTreeViewDropPreviewPlacement::None; } const UIRect& rowRect = layout.rowRects[hitTarget.visibleIndex]; const float edgeBand = (std::min)((std::max)(rowRect.height * 0.25f, 4.0f), 8.0f); if (point.y <= rowRect.y + edgeBand) { return UIEditorTreeViewDropPreviewPlacement::BeforeItem; } if (point.y >= rowRect.y + rowRect.height - edgeBand) { return UIEditorTreeViewDropPreviewPlacement::AfterItem; } return UIEditorTreeViewDropPreviewPlacement::OnItem; } inline const Widgets::UIEditorTreeViewItem* ResolveGapInsertionItem( const Widgets::UIEditorTreeViewLayout& layout, const std::vector& items, const UIPoint& point, UIEditorTreeViewDropPreviewPlacement& outPlacement) { outPlacement = UIEditorTreeViewDropPreviewPlacement::None; if (layout.rowRects.empty()) { return nullptr; } if (point.y < layout.rowRects.front().y) { outPlacement = UIEditorTreeViewDropPreviewPlacement::BeforeItem; const std::size_t itemIndex = layout.visibleItemIndices.front(); return itemIndex < items.size() ? &items[itemIndex] : nullptr; } for (std::size_t visibleIndex = 0u; visibleIndex + 1u < layout.rowRects.size(); ++visibleIndex) { const UIRect& currentRow = layout.rowRects[visibleIndex]; const UIRect& nextRow = layout.rowRects[visibleIndex + 1u]; if (point.y > currentRow.y + currentRow.height && point.y < nextRow.y) { outPlacement = UIEditorTreeViewDropPreviewPlacement::BeforeItem; const std::size_t itemIndex = layout.visibleItemIndices[visibleIndex + 1u]; return itemIndex < items.size() ? &items[itemIndex] : nullptr; } } const UIRect& lastRow = layout.rowRects.back(); if (point.y > lastRow.y + lastRow.height) { outPlacement = UIEditorTreeViewDropPreviewPlacement::AfterItem; const std::size_t itemIndex = layout.visibleItemIndices.back(); return itemIndex < items.size() ? &items[itemIndex] : nullptr; } return nullptr; } inline const Widgets::UIEditorTreeViewItem* ResolveHitItem( const Widgets::UIEditorTreeViewLayout& layout, const std::vector& items, const UIPoint& point, UIEditorTreeViewHitTarget* hitTargetOutput = nullptr) { const UIEditorTreeViewHitTarget hitTarget = HitTestUIEditorTreeView(layout, point); if (hitTargetOutput != nullptr) { *hitTargetOutput = hitTarget; } if (hitTarget.itemIndex >= items.size()) { return nullptr; } return &items[hitTarget.itemIndex]; } inline std::size_t FindVisibleIndexForItemId( const Widgets::UIEditorTreeViewLayout& layout, const std::vector& items, std::string_view itemId) { for (std::size_t visibleIndex = 0u; visibleIndex < layout.visibleItemIndices.size(); ++visibleIndex) { const std::size_t itemIndex = layout.visibleItemIndices[visibleIndex]; if (itemIndex < items.size() && items[itemIndex].itemId == itemId) { return visibleIndex; } } return UIEditorTreeViewInvalidIndex; } inline bool IsSuppressedPointerInputEvent( const DragDropInteraction::UIInputEvent& event) { switch (event.type) { case ::XCEngine::UI::UIInputEventType::PointerMove: case ::XCEngine::UI::UIInputEventType::PointerButtonDown: case ::XCEngine::UI::UIInputEventType::PointerButtonUp: case ::XCEngine::UI::UIInputEventType::PointerWheel: case ::XCEngine::UI::UIInputEventType::PointerEnter: return true; default: return false; } } inline std::vector FilterPointerInputEvents( const std::vector& inputEvents, bool suppressPointerInput) { if (!suppressPointerInput) { return inputEvents; } std::vector filteredEvents = {}; filteredEvents.reserve(inputEvents.size()); for (const DragDropInteraction::UIInputEvent& event : inputEvents) { if (!IsSuppressedPointerInputEvent(event)) { filteredEvents.push_back(event); } } return filteredEvents; } inline std::vector BuildInteractionInputEvents( const State& state, const Widgets::UIEditorTreeViewLayout& layout, const std::vector& items, const std::vector& rawEvents, float dragThreshold = kDefaultDragThreshold, bool suppressPointerInput = false) { auto resolveDraggableItem = [&layout, &items](const UIPoint& point) -> std::string { UIEditorTreeViewHitTarget hitTarget = {}; const Widgets::UIEditorTreeViewItem* hitItem = ResolveHitItem(layout, items, point, &hitTarget); if (hitItem != nullptr && hitTarget.kind == UIEditorTreeViewHitTargetKind::Row) { return hitItem->itemId; } return {}; }; DragDropInteraction::PreviewState preview = DragDropInteraction::BuildPreviewState(state); const std::vector inputEvents = FilterPointerInputEvents(rawEvents, suppressPointerInput); std::vector filteredEvents = {}; filteredEvents.reserve(inputEvents.size()); for (const DragDropInteraction::UIInputEvent& event : inputEvents) { if (!DragDropInteraction::ProcessPreviewEvent( preview, event, resolveDraggableItem, dragThreshold)) { filteredEvents.push_back(event); } } return filteredEvents; } template ProcessResult ProcessInputEvents( State& state, const Widgets::UIEditorTreeViewLayout& layout, const std::vector& items, const std::vector& inputEvents, const UIRect& bounds, Callbacks& callbacks, float dragThreshold = kDefaultDragThreshold, bool suppressPointerInput = false) { struct AdaptedCallbacks { const Widgets::UIEditorTreeViewLayout& layout; const std::vector& items; const UIRect& bounds; Callbacks& callbacks; bool droppedToRoot = false; std::string ResolveDraggableItem(const UIPoint& point) const { UIEditorTreeViewHitTarget hitTarget = {}; const Widgets::UIEditorTreeViewItem* hitItem = ResolveHitItem(layout, items, point, &hitTarget); if (hitItem != nullptr && hitTarget.kind == UIEditorTreeViewHitTargetKind::Row) { return hitItem->itemId; } return {}; } void ResetDropTarget(State& state) const { state.dropTargetItemId.clear(); state.dropToRoot = false; state.dropPlacement = UIEditorTreeViewDropPreviewPlacement::None; state.validDropTarget = false; } void UpdateDropTarget( State& state, std::string_view draggedItemId, const UIPoint& point) const { UIEditorTreeViewHitTarget hitTarget = {}; const Widgets::UIEditorTreeViewItem* hitItem = ResolveHitItem(layout, items, point, &hitTarget); if (hitItem != nullptr && (hitTarget.kind == UIEditorTreeViewHitTargetKind::Row || hitTarget.kind == UIEditorTreeViewHitTargetKind::Disclosure)) { UIEditorTreeViewDropPreviewPlacement placement = UIEditorTreeViewDropPreviewPlacement::OnItem; if constexpr (SupportsSiblingInsertion()) { placement = hitTarget.kind == UIEditorTreeViewHitTargetKind::Disclosure ? UIEditorTreeViewDropPreviewPlacement::OnItem : ResolveRowDropPlacement(layout, hitTarget, point); } state.dropTargetItemId = hitItem->itemId; state.dropPlacement = placement; switch (placement) { case UIEditorTreeViewDropPreviewPlacement::BeforeItem: if constexpr (SupportsSiblingInsertion()) { state.validDropTarget = callbacks.CanInsertBeforeItem( draggedItemId, state.dropTargetItemId); } break; case UIEditorTreeViewDropPreviewPlacement::AfterItem: if constexpr (SupportsSiblingInsertion()) { state.validDropTarget = callbacks.CanInsertAfterItem( draggedItemId, state.dropTargetItemId); } break; case UIEditorTreeViewDropPreviewPlacement::OnItem: state.validDropTarget = callbacks.CanDropOnItem(draggedItemId, state.dropTargetItemId); break; default: break; } return; } if constexpr (SupportsSiblingInsertion()) { UIEditorTreeViewDropPreviewPlacement gapPlacement = UIEditorTreeViewDropPreviewPlacement::None; const Widgets::UIEditorTreeViewItem* gapItem = ResolveGapInsertionItem(layout, items, point, gapPlacement); if (gapItem != nullptr && gapPlacement != UIEditorTreeViewDropPreviewPlacement::None) { state.dropTargetItemId = gapItem->itemId; state.dropPlacement = gapPlacement; state.validDropTarget = gapPlacement == UIEditorTreeViewDropPreviewPlacement::BeforeItem ? callbacks.CanInsertBeforeItem( draggedItemId, state.dropTargetItemId) : callbacks.CanInsertAfterItem( draggedItemId, state.dropTargetItemId); return; } } if (ContainsPoint(bounds, point)) { state.dropToRoot = true; state.dropPlacement = UIEditorTreeViewDropPreviewPlacement::Root; state.validDropTarget = callbacks.CanDropToRoot(draggedItemId); } } bool IsItemSelected(std::string_view itemId) const { return callbacks.IsItemSelected(itemId); } bool SelectDraggedItem(std::string_view itemId) const { return callbacks.SelectDraggedItem(itemId); } bool CommitDrop(State& state, DragDropInteraction::ProcessResult&) { droppedToRoot = state.dropToRoot; switch (state.dropPlacement) { case UIEditorTreeViewDropPreviewPlacement::BeforeItem: if constexpr (SupportsSiblingInsertion()) { return callbacks.CommitInsertBeforeItem( state.draggedItemId, state.dropTargetItemId); } return false; case UIEditorTreeViewDropPreviewPlacement::AfterItem: if constexpr (SupportsSiblingInsertion()) { return callbacks.CommitInsertAfterItem( state.draggedItemId, state.dropTargetItemId); } return false; case UIEditorTreeViewDropPreviewPlacement::Root: return callbacks.CommitDropToRoot(state.draggedItemId); case UIEditorTreeViewDropPreviewPlacement::OnItem: return callbacks.CommitDropOnItem( state.draggedItemId, state.dropTargetItemId); case UIEditorTreeViewDropPreviewPlacement::None: default: return false; } } } adaptedCallbacks{ layout, items, bounds, callbacks }; ProcessResult result = {}; const std::vector filteredInputEvents = FilterPointerInputEvents(inputEvents, suppressPointerInput); const DragDropInteraction::ProcessResult interactionResult = DragDropInteraction::ProcessInputEvents( state, filteredInputEvents, adaptedCallbacks, dragThreshold); result.selectionForced = interactionResult.selectionForced; result.dropCommitted = interactionResult.dropCommitted; result.draggedItemId = interactionResult.draggedItemId; result.dropTargetItemId = interactionResult.dropTargetItemId; result.droppedToRoot = adaptedCallbacks.droppedToRoot; return result; } } // namespace XCEngine::UI::Editor::Collections::TreeDragDrop