#pragma once #include #include #include #include namespace XCEngine::UI::Editor::App::GridItemDragDrop { using ::XCEngine::UI::UIInputEvent; using ::XCEngine::UI::UIInputEventType; using ::XCEngine::UI::UIPoint; using ::XCEngine::UI::UIPointerButton; 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 validDropTarget = false; bool requestPointerCapture = false; bool requestPointerRelease = false; }; struct ProcessResult { bool selectionForced = false; bool dropCommitted = 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 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; } template ProcessResult ProcessInputEvents( State& state, const std::vector& inputEvents, Callbacks& callbacks, float dragThreshold = kDefaultDragThreshold) { ProcessResult result = {}; for (const UIInputEvent& event : inputEvents) { switch (event.type) { case UIInputEventType::PointerButtonDown: if (event.pointerButton == UIPointerButton::Left) { state.armedItemId = callbacks.ResolveDraggableItem(event.position); state.pressPosition = event.position; state.armed = !state.armedItemId.empty(); if (!state.armed) { state.draggedItemId.clear(); state.dropTargetItemId.clear(); state.validDropTarget = false; } } 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.validDropTarget = false; if (state.dragging) { state.requestPointerCapture = true; if (!callbacks.IsItemSelected(state.draggedItemId)) { result.selectionForced = callbacks.SelectDraggedItem(state.draggedItemId); } } } if (state.dragging) { state.dropTargetItemId = callbacks.ResolveDropTargetItem( state.draggedItemId, event.position); state.validDropTarget = !state.dropTargetItemId.empty() && callbacks.CanDropOnItem( state.draggedItemId, state.dropTargetItemId); } 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.dropCommitted = callbacks.CommitDropOnItem( state.draggedItemId, state.dropTargetItemId); } state.armed = false; state.dragging = false; state.armedItemId.clear(); state.draggedItemId.clear(); state.dropTargetItemId.clear(); state.validDropTarget = false; state.requestPointerRelease = true; } else { state.armed = false; state.armedItemId.clear(); } break; case UIInputEventType::PointerLeave: if (state.dragging) { state.dropTargetItemId.clear(); 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::GridItemDragDrop