#pragma once #include #include #include #include #include namespace XCEngine::UI::Editor::App::TreeItemDragDrop { using ::XCEngine::UI::UIInputEvent; using ::XCEngine::UI::UIInputEventType; using ::XCEngine::UI::UIPoint; using ::XCEngine::UI::UIRect; using ::XCEngine::UI::UIPointerButton; using Widgets::HitTestUIEditorTreeView; using Widgets::UIEditorTreeViewHitTarget; using Widgets::UIEditorTreeViewHitTargetKind; using Widgets::UIEditorTreeViewInvalidIndex; inline constexpr float kDefaultDragThreshold = 4.0f; struct State { std::string armedItemId = {}; std::string draggedItemId = {}; std::string dropTargetItemId = {}; UIPoint pressPosition = {}; bool armed = false; bool dragging = false; bool dropToRoot = false; bool validDropTarget = false; bool requestPointerCapture = false; bool requestPointerRelease = false; }; struct ProcessResult { bool selectionForced = false; bool dropCommitted = false; bool droppedToRoot = false; std::string draggedItemId = {}; std::string dropTargetItemId = {}; }; inline void ResetTransientRequests(State& state) { state.requestPointerCapture = false; state.requestPointerRelease = false; } inline bool HasActivePointerCapture(const State& state) { return state.dragging; } 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; } inline float ComputeSquaredDistance(const UIPoint& lhs, const UIPoint& rhs) { const float dx = lhs.x - rhs.x; const float dy = lhs.y - rhs.y; return dx * dx + dy * dy; } 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 std::vector BuildInteractionInputEvents( const State& state, const Widgets::UIEditorTreeViewLayout& layout, const std::vector& items, const std::vector& rawEvents, float dragThreshold = kDefaultDragThreshold) { struct PreviewState { std::string armedItemId = {}; UIPoint pressPosition = {}; bool armed = false; bool dragging = false; }; PreviewState preview = {}; preview.armed = state.armed; preview.armedItemId = state.armedItemId; preview.pressPosition = state.pressPosition; preview.dragging = state.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, items, 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) >= dragThreshold * dragThreshold) { 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: if (preview.dragging && (event.type == UIInputEventType::PointerWheel || event.type == UIInputEventType::PointerEnter)) { suppress = true; } break; } if (!suppress) { 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) { ProcessResult result = {}; for (const UIInputEvent& event : inputEvents) { switch (event.type) { case UIInputEventType::PointerButtonDown: if (event.pointerButton == UIPointerButton::Left) { UIEditorTreeViewHitTarget hitTarget = {}; const Widgets::UIEditorTreeViewItem* hitItem = ResolveHitItem(layout, items, event.position, &hitTarget); if (hitItem != nullptr && hitTarget.kind == UIEditorTreeViewHitTargetKind::Row) { state.armed = true; state.armedItemId = hitItem->itemId; state.pressPosition = event.position; } else { state.armed = false; state.armedItemId.clear(); } } break; case UIInputEventType::PointerMove: if (state.armed && !state.dragging && ComputeSquaredDistance(event.position, state.pressPosition) >= dragThreshold * dragThreshold) { state.dragging = !state.armedItemId.empty(); state.draggedItemId = state.armedItemId; state.dropTargetItemId.clear(); state.dropToRoot = false; state.validDropTarget = false; if (state.dragging) { state.requestPointerCapture = true; if (!callbacks.IsItemSelected(state.draggedItemId)) { result.selectionForced = callbacks.SelectDraggedItem(state.draggedItemId); } } } if (state.dragging) { UIEditorTreeViewHitTarget hitTarget = {}; const Widgets::UIEditorTreeViewItem* hitItem = ResolveHitItem(layout, items, event.position, &hitTarget); state.dropTargetItemId.clear(); state.dropToRoot = false; state.validDropTarget = false; if (hitItem != nullptr && (hitTarget.kind == UIEditorTreeViewHitTargetKind::Row || hitTarget.kind == UIEditorTreeViewHitTargetKind::Disclosure)) { state.dropTargetItemId = hitItem->itemId; state.validDropTarget = callbacks.CanDropOnItem(state.draggedItemId, state.dropTargetItemId); } else if (ContainsPoint(bounds, event.position)) { state.dropToRoot = true; state.validDropTarget = callbacks.CanDropToRoot(state.draggedItemId); } } break; case UIInputEventType::PointerButtonUp: if (event.pointerButton != UIPointerButton::Left) { break; } if (state.dragging) { if (state.validDropTarget) { result.draggedItemId = state.draggedItemId; result.dropTargetItemId = state.dropTargetItemId; result.droppedToRoot = state.dropToRoot; result.dropCommitted = state.dropToRoot ? callbacks.CommitDropToRoot(state.draggedItemId) : callbacks.CommitDropOnItem( state.draggedItemId, state.dropTargetItemId); } state.armed = false; state.dragging = false; state.armedItemId.clear(); state.draggedItemId.clear(); state.dropTargetItemId.clear(); state.dropToRoot = false; state.validDropTarget = false; state.requestPointerRelease = true; } else { state.armed = false; state.armedItemId.clear(); } break; case UIInputEventType::PointerLeave: if (state.dragging) { state.dropTargetItemId.clear(); state.dropToRoot = false; state.validDropTarget = false; } break; case UIInputEventType::FocusLost: { const bool requestPointerRelease = state.dragging; state = {}; state.requestPointerRelease = requestPointerRelease; } break; default: break; } } return result; } } // namespace XCEngine::UI::Editor::App::TreeItemDragDrop