325 lines
11 KiB
C++
325 lines
11 KiB
C++
#pragma once
|
|
|
|
#include <XCEditor/Collections/UIEditorTreeView.h>
|
|
|
|
#include <XCEngine/UI/Types.h>
|
|
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <vector>
|
|
|
|
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<Widgets::UIEditorTreeViewItem>& 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<Widgets::UIEditorTreeViewItem>& 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<UIInputEvent> BuildInteractionInputEvents(
|
|
const State& state,
|
|
const Widgets::UIEditorTreeViewLayout& layout,
|
|
const std::vector<Widgets::UIEditorTreeViewItem>& items,
|
|
const std::vector<UIInputEvent>& 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<UIInputEvent> 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 <typename Callbacks>
|
|
ProcessResult ProcessInputEvents(
|
|
State& state,
|
|
const Widgets::UIEditorTreeViewLayout& layout,
|
|
const std::vector<Widgets::UIEditorTreeViewItem>& items,
|
|
const std::vector<UIInputEvent>& 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
|