Files
XCEngine/new_editor/src/Docking/DockHostInteractionHelpers.cpp

460 lines
16 KiB
C++

#include "Docking/DockHostInteractionInternal.h"
#include <algorithm>
#include <string_view>
#include <utility>
#include <vector>
namespace XCEngine::UI::Editor::Internal {
using ::XCEngine::UI::UIInputEvent;
using ::XCEngine::UI::UIInputEventType;
using ::XCEngine::UI::UIPoint;
using ::XCEngine::UI::UIRect;
using Widgets::HitTestUIEditorDockHost;
using Widgets::UIEditorDockHostHitTarget;
using Widgets::UIEditorDockHostHitTargetKind;
using Widgets::UIEditorDockHostTabItemLayout;
using Widgets::UIEditorDockHostTabStackLayout;
using Widgets::UIEditorTabStripHitTargetKind;
using Widgets::UIEditorTabStripItem;
bool ShouldUseDockHostPointerPosition(const UIInputEvent& event) {
switch (event.type) {
case UIInputEventType::PointerMove:
case UIInputEventType::PointerEnter:
case UIInputEventType::PointerButtonDown:
case UIInputEventType::PointerButtonUp:
return true;
default:
return false;
}
}
bool ShouldDispatchTabStripEvent(
const UIInputEvent& event,
bool splitterActive) {
if (splitterActive && event.type != UIInputEventType::FocusLost) {
return false;
}
switch (event.type) {
case UIInputEventType::FocusLost:
case UIInputEventType::PointerMove:
case UIInputEventType::PointerEnter:
case UIInputEventType::PointerLeave:
case UIInputEventType::PointerButtonDown:
case UIInputEventType::PointerButtonUp:
case UIInputEventType::KeyDown:
return true;
default:
return false;
}
}
UIEditorWorkspaceLayoutOperationResult ApplySplitRatio(
UIEditorWorkspaceController& controller,
std::string_view nodeId,
float splitRatio) {
return controller.SetSplitRatio(nodeId, splitRatio);
}
UIEditorWorkspaceCommandResult DispatchPanelCommand(
UIEditorWorkspaceController& controller,
UIEditorWorkspaceCommandKind kind,
std::string panelId) {
UIEditorWorkspaceCommand command = {};
command.kind = kind;
command.panelId = std::move(panelId);
return controller.Dispatch(command);
}
UIEditorDockHostTabStripInteractionEntry& FindOrCreateTabStripInteractionEntry(
UIEditorDockHostInteractionState& state,
std::string_view nodeId) {
for (UIEditorDockHostTabStripInteractionEntry& entry : state.tabStripInteractions) {
if (entry.nodeId == nodeId) {
return entry;
}
}
state.tabStripInteractions.push_back({});
UIEditorDockHostTabStripInteractionEntry& entry = state.tabStripInteractions.back();
entry.nodeId = std::string(nodeId);
return entry;
}
void PruneTabStripInteractionEntries(
UIEditorDockHostInteractionState& state,
const Widgets::UIEditorDockHostLayout& layout) {
const auto isVisibleNodeId = [&layout](std::string_view nodeId) {
return std::find_if(
layout.tabStacks.begin(),
layout.tabStacks.end(),
[nodeId](const UIEditorDockHostTabStackLayout& tabStack) {
return tabStack.nodeId == nodeId;
}) != layout.tabStacks.end();
};
state.tabStripInteractions.erase(
std::remove_if(
state.tabStripInteractions.begin(),
state.tabStripInteractions.end(),
[&isVisibleNodeId](const UIEditorDockHostTabStripInteractionEntry& entry) {
return !isVisibleNodeId(entry.nodeId);
}),
state.tabStripInteractions.end());
state.dockHostState.tabStripStates.erase(
std::remove_if(
state.dockHostState.tabStripStates.begin(),
state.dockHostState.tabStripStates.end(),
[&isVisibleNodeId](const Widgets::UIEditorDockHostTabStripVisualState& entry) {
return !isVisibleNodeId(entry.nodeId);
}),
state.dockHostState.tabStripStates.end());
if (!state.activeTabDragNodeId.empty() &&
!isVisibleNodeId(state.activeTabDragNodeId)) {
state.activeTabDragNodeId.clear();
state.activeTabDragPanelId.clear();
state.dockHostState.dropPreview = {};
}
}
void SyncDockHostTabStripVisualStates(UIEditorDockHostInteractionState& state) {
state.dockHostState.tabStripStates.clear();
state.dockHostState.tabStripStates.reserve(state.tabStripInteractions.size());
for (const UIEditorDockHostTabStripInteractionEntry& entry :
state.tabStripInteractions) {
Widgets::UIEditorDockHostTabStripVisualState visualState = {};
visualState.nodeId = entry.nodeId;
visualState.state = entry.state.tabStripState;
state.dockHostState.tabStripStates.push_back(std::move(visualState));
}
}
bool HasFocusedTabStrip(const UIEditorDockHostInteractionState& state) {
return std::find_if(
state.tabStripInteractions.begin(),
state.tabStripInteractions.end(),
[](const UIEditorDockHostTabStripInteractionEntry& entry) {
return entry.state.tabStripState.focused;
}) != state.tabStripInteractions.end();
}
const UIEditorDockHostTabStackLayout* FindTabStackLayoutByNodeId(
const Widgets::UIEditorDockHostLayout& layout,
std::string_view nodeId) {
for (const UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) {
if (tabStack.nodeId == nodeId) {
return &tabStack;
}
}
return nullptr;
}
bool IsPointInsideRect(
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;
}
void ClearAllTabStripTransientInteractions(UIEditorDockHostInteractionState& state) {
for (UIEditorDockHostTabStripInteractionEntry& entry : state.tabStripInteractions) {
ClearUIEditorTabStripTransientInteraction(entry.state);
}
SyncDockHostTabStripVisualStates(state);
}
void ClearTabDockDragState(UIEditorDockHostInteractionState& state) {
state.activeTabDragNodeId.clear();
state.activeTabDragPanelId.clear();
state.dockHostState.dropPreview = {};
}
std::vector<UIEditorTabStripItem> BuildTabStripItems(
const UIEditorDockHostTabStackLayout& tabStack) {
std::vector<UIEditorTabStripItem> items = {};
items.reserve(tabStack.items.size());
for (const UIEditorDockHostTabItemLayout& itemLayout : tabStack.items) {
UIEditorTabStripItem item = {};
item.tabId = itemLayout.panelId;
item.title = itemLayout.title;
item.closable = itemLayout.closable;
items.push_back(std::move(item));
}
return items;
}
UIEditorDockHostHitTarget MapTabStripHitTarget(
const UIEditorDockHostTabStackLayout& tabStack,
const UIEditorTabStripInteractionResult& result) {
UIEditorDockHostHitTarget target = {};
target.nodeId = tabStack.nodeId;
target.index = result.hitTarget.index;
switch (result.hitTarget.kind) {
case UIEditorTabStripHitTargetKind::HeaderBackground:
target.kind = UIEditorDockHostHitTargetKind::TabStripBackground;
break;
case UIEditorTabStripHitTargetKind::Tab:
target.kind = UIEditorDockHostHitTargetKind::Tab;
if (result.hitTarget.index < tabStack.items.size()) {
target.panelId = tabStack.items[result.hitTarget.index].panelId;
}
break;
case UIEditorTabStripHitTargetKind::CloseButton:
target.kind = UIEditorDockHostHitTargetKind::TabCloseButton;
if (result.hitTarget.index < tabStack.items.size()) {
target.panelId = tabStack.items[result.hitTarget.index].panelId;
}
break;
default:
break;
}
return target;
}
int ResolveTabStripPriority(const UIEditorTabStripInteractionResult& result) {
if (result.reorderRequested ||
result.dragStarted ||
result.dragEnded ||
result.dragCanceled) {
return 5;
}
if (result.closeRequested) {
return 4;
}
if (result.selectionChanged || result.keyboardNavigated) {
return 3;
}
if (result.consumed ||
result.hitTarget.kind != UIEditorTabStripHitTargetKind::None) {
return 2;
}
return 0;
}
DockHostTabStripEventResult ProcessTabStripEvent(
UIEditorDockHostInteractionState& state,
const Widgets::UIEditorDockHostLayout& layout,
const UIInputEvent& event,
const Widgets::UIEditorDockHostMetrics& metrics) {
DockHostTabStripEventResult resolved = {};
for (const UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) {
if (!state.activeTabDragNodeId.empty() &&
tabStack.nodeId != state.activeTabDragNodeId) {
continue;
}
UIEditorDockHostTabStripInteractionEntry& entry =
FindOrCreateTabStripInteractionEntry(state, tabStack.nodeId);
std::string selectedTabId = tabStack.selectedPanelId;
const std::vector<UIEditorTabStripItem> items = BuildTabStripItems(tabStack);
const UIEditorTabStripInteractionFrame frame = UpdateUIEditorTabStripInteraction(
entry.state,
selectedTabId,
tabStack.bounds,
items,
{ event },
metrics.tabStripMetrics);
const int priority = ResolveTabStripPriority(frame.result);
if (priority < resolved.priority) {
continue;
}
resolved.nodeId = tabStack.nodeId;
resolved.hitTarget = MapTabStripHitTarget(tabStack, frame.result);
resolved.requestPointerCapture = frame.result.requestPointerCapture;
resolved.releasePointerCapture = frame.result.releasePointerCapture;
resolved.dragStarted = frame.result.dragStarted;
resolved.dragEnded = frame.result.dragEnded;
resolved.dragCanceled = frame.result.dragCanceled;
resolved.dropInsertionIndex = frame.result.dropInsertionIndex;
resolved.draggedTabId = frame.result.draggedTabId;
if ((frame.result.closeRequested && !frame.result.closedTabId.empty()) ||
(event.type == UIInputEventType::PointerButtonUp &&
frame.result.consumed &&
resolved.hitTarget.kind == UIEditorDockHostHitTargetKind::TabCloseButton &&
!resolved.hitTarget.panelId.empty())) {
resolved.commandRequested = true;
resolved.commandKind = UIEditorWorkspaceCommandKind::ClosePanel;
resolved.panelId =
!frame.result.closedTabId.empty()
? frame.result.closedTabId
: resolved.hitTarget.panelId;
} else if (frame.result.reorderRequested &&
!frame.result.draggedTabId.empty() &&
frame.result.dropInsertionIndex !=
Widgets::UIEditorTabStripInvalidIndex) {
resolved.reorderRequested = true;
resolved.panelId = frame.result.draggedTabId;
resolved.dropInsertionIndex = frame.result.dropInsertionIndex;
} else if ((frame.result.selectionChanged ||
frame.result.keyboardNavigated ||
(event.type == UIInputEventType::PointerButtonUp &&
frame.result.consumed &&
resolved.hitTarget.kind == UIEditorDockHostHitTargetKind::Tab)) &&
(!frame.result.selectedTabId.empty() ||
!resolved.hitTarget.panelId.empty())) {
resolved.commandRequested = true;
resolved.commandKind = UIEditorWorkspaceCommandKind::ActivatePanel;
resolved.panelId =
!frame.result.selectedTabId.empty()
? frame.result.selectedTabId
: resolved.hitTarget.panelId;
} else if (priority == 0) {
continue;
} else {
resolved.commandRequested = false;
resolved.reorderRequested = false;
resolved.panelId.clear();
}
resolved.consumed = frame.result.consumed;
resolved.priority = priority;
}
SyncDockHostTabStripVisualStates(state);
return resolved;
}
std::size_t ResolveTabHeaderDropInsertionIndex(
const UIEditorDockHostTabStackLayout& tabStack,
const UIPoint& point) {
if (!IsPointInsideRect(tabStack.tabStripLayout.headerRect, point)) {
return Widgets::UIEditorTabStripInvalidIndex;
}
std::size_t insertionIndex = 0u;
for (const UIRect& rect : tabStack.tabStripLayout.tabHeaderRects) {
const float midpoint = rect.x + rect.width * 0.5f;
if (point.x > midpoint) {
++insertionIndex;
}
}
return insertionIndex;
}
UIEditorWorkspaceDockPlacement ResolveDockPlacement(
const UIEditorDockHostTabStackLayout& tabStack,
const UIPoint& point) {
if (IsPointInsideRect(tabStack.tabStripLayout.headerRect, point)) {
return UIEditorWorkspaceDockPlacement::Center;
}
const float leftDistance = point.x - tabStack.bounds.x;
const float rightDistance =
tabStack.bounds.x + tabStack.bounds.width - point.x;
const float topDistance = point.y - tabStack.bounds.y;
const float bottomDistance =
tabStack.bounds.y + tabStack.bounds.height - point.y;
const float minHorizontalThreshold = tabStack.bounds.width * 0.25f;
const float minVerticalThreshold = tabStack.bounds.height * 0.25f;
const float nearestEdge = (std::min)(
(std::min)(leftDistance, rightDistance),
(std::min)(topDistance, bottomDistance));
if (nearestEdge == leftDistance && leftDistance <= minHorizontalThreshold) {
return UIEditorWorkspaceDockPlacement::Left;
}
if (nearestEdge == rightDistance && rightDistance <= minHorizontalThreshold) {
return UIEditorWorkspaceDockPlacement::Right;
}
if (nearestEdge == topDistance && topDistance <= minVerticalThreshold) {
return UIEditorWorkspaceDockPlacement::Top;
}
if (nearestEdge == bottomDistance && bottomDistance <= minVerticalThreshold) {
return UIEditorWorkspaceDockPlacement::Bottom;
}
return UIEditorWorkspaceDockPlacement::Center;
}
void SyncDockPreview(
UIEditorDockHostInteractionState& state,
const Widgets::UIEditorDockHostLayout& layout) {
state.dockHostState.dropPreview = {};
if (state.activeTabDragNodeId.empty() ||
state.activeTabDragPanelId.empty() ||
!state.hasPointerPosition) {
return;
}
for (const UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) {
if (!IsPointInsideRect(tabStack.bounds, state.pointerPosition)) {
continue;
}
const UIEditorWorkspaceDockPlacement placement =
ResolveDockPlacement(tabStack, state.pointerPosition);
if (tabStack.nodeId == state.activeTabDragNodeId &&
placement == UIEditorWorkspaceDockPlacement::Center) {
return;
}
if (tabStack.nodeId == state.activeTabDragNodeId &&
tabStack.items.size() <= 1u) {
return;
}
Widgets::UIEditorDockHostDropPreviewState preview = {};
preview.visible = true;
preview.sourceNodeId = state.activeTabDragNodeId;
preview.sourcePanelId = state.activeTabDragPanelId;
preview.targetNodeId = tabStack.nodeId;
preview.placement = placement;
if (placement == UIEditorWorkspaceDockPlacement::Center) {
preview.insertionIndex =
ResolveTabHeaderDropInsertionIndex(tabStack, state.pointerPosition);
if (preview.insertionIndex == Widgets::UIEditorTabStripInvalidIndex) {
preview.insertionIndex = tabStack.items.size();
}
}
state.dockHostState.dropPreview = std::move(preview);
return;
}
}
void SyncHoverTarget(
UIEditorDockHostInteractionState& state,
const Widgets::UIEditorDockHostLayout& layout) {
if (state.splitterDragState.active) {
state.dockHostState.hoveredTarget = {
UIEditorDockHostHitTargetKind::SplitterHandle,
state.dockHostState.activeSplitterNodeId,
{},
Widgets::UIEditorTabStripInvalidIndex
};
return;
}
if (!state.activeTabDragNodeId.empty()) {
state.dockHostState.hoveredTarget = {};
return;
}
if (!state.hasPointerPosition) {
state.dockHostState.hoveredTarget = {};
return;
}
state.dockHostState.hoveredTarget =
HitTestUIEditorDockHost(layout, state.pointerPosition);
}
} // namespace XCEngine::UI::Editor::Internal