refactor(new_editor): streamline internal layout and command routing
This commit is contained in:
459
new_editor/src/Docking/DockHostInteractionHelpers.cpp
Normal file
459
new_editor/src/Docking/DockHostInteractionHelpers.cpp
Normal file
@@ -0,0 +1,459 @@
|
||||
#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
|
||||
Reference in New Issue
Block a user