fix(editor_ui): resolve splitter and tab drag gesture conflict
This commit is contained in:
@@ -53,6 +53,10 @@ struct UIEditorTabStripInteractionFrame {
|
||||
bool focused = false;
|
||||
};
|
||||
|
||||
// Clears transient pointer-driven state while preserving selection/navigation.
|
||||
void ClearUIEditorTabStripTransientInteraction(
|
||||
UIEditorTabStripInteractionState& state);
|
||||
|
||||
UIEditorTabStripInteractionFrame UpdateUIEditorTabStripInteraction(
|
||||
UIEditorTabStripInteractionState& state,
|
||||
std::string& selectedTabId,
|
||||
|
||||
@@ -363,6 +363,14 @@ void CancelTabReorder(
|
||||
|
||||
} // namespace
|
||||
|
||||
void ClearUIEditorTabStripTransientInteraction(
|
||||
UIEditorTabStripInteractionState& state) {
|
||||
state.pressedTarget = {};
|
||||
state.hasPointerPosition = false;
|
||||
ClearHoverState(state);
|
||||
ClearReorderState(state);
|
||||
}
|
||||
|
||||
UIEditorTabStripInteractionFrame UpdateUIEditorTabStripInteraction(
|
||||
UIEditorTabStripInteractionState& state,
|
||||
std::string& selectedTabId,
|
||||
|
||||
@@ -192,6 +192,14 @@ bool IsPointInsideRect(
|
||||
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();
|
||||
@@ -510,9 +518,19 @@ UIEditorDockHostInteractionFrame UpdateUIEditorDockHostInteraction(
|
||||
state.hasPointerPosition = false;
|
||||
}
|
||||
|
||||
const UIEditorDockHostHitTarget pointerHitTarget =
|
||||
state.hasPointerPosition
|
||||
? HitTestUIEditorDockHost(layout, state.pointerPosition)
|
||||
: UIEditorDockHostHitTarget {};
|
||||
const bool splitterOwnsPointerDown =
|
||||
event.type == UIInputEventType::PointerButtonDown &&
|
||||
event.pointerButton == ::XCEngine::UI::UIPointerButton::Left &&
|
||||
pointerHitTarget.kind == UIEditorDockHostHitTargetKind::SplitterHandle;
|
||||
|
||||
UIEditorDockHostInteractionResult eventResult = {};
|
||||
const DockHostTabStripEventResult tabStripResult =
|
||||
ShouldDispatchTabStripEvent(event, state.splitterDragState.active)
|
||||
!splitterOwnsPointerDown &&
|
||||
ShouldDispatchTabStripEvent(event, state.splitterDragState.active)
|
||||
? ProcessTabStripEvent(state, layout, event, metrics)
|
||||
: DockHostTabStripEventResult {};
|
||||
eventResult.requestPointerCapture = tabStripResult.requestPointerCapture;
|
||||
@@ -541,6 +559,7 @@ UIEditorDockHostInteractionFrame UpdateUIEditorDockHostInteraction(
|
||||
if (state.splitterDragState.active) {
|
||||
EndUISplitterDrag(state.splitterDragState);
|
||||
state.dockHostState.activeSplitterNodeId.clear();
|
||||
ClearAllTabStripTransientInteractions(state);
|
||||
eventResult.consumed = true;
|
||||
eventResult.releasePointerCapture = true;
|
||||
}
|
||||
@@ -609,11 +628,11 @@ UIEditorDockHostInteractionFrame UpdateUIEditorDockHostInteraction(
|
||||
break;
|
||||
}
|
||||
|
||||
if (state.dockHostState.hoveredTarget.kind ==
|
||||
if (pointerHitTarget.kind ==
|
||||
UIEditorDockHostHitTargetKind::SplitterHandle) {
|
||||
const auto* splitter = FindUIEditorDockHostSplitterLayout(
|
||||
layout,
|
||||
state.dockHostState.hoveredTarget.nodeId);
|
||||
pointerHitTarget.nodeId);
|
||||
if (splitter != nullptr &&
|
||||
BeginUISplitterDrag(
|
||||
1u,
|
||||
@@ -626,11 +645,12 @@ UIEditorDockHostInteractionFrame UpdateUIEditorDockHostInteraction(
|
||||
splitter->metrics,
|
||||
state.pointerPosition,
|
||||
state.splitterDragState)) {
|
||||
ClearAllTabStripTransientInteractions(state);
|
||||
state.dockHostState.activeSplitterNodeId = splitter->nodeId;
|
||||
state.dockHostState.focused = true;
|
||||
eventResult.consumed = true;
|
||||
eventResult.requestPointerCapture = true;
|
||||
eventResult.hitTarget = state.dockHostState.hoveredTarget;
|
||||
eventResult.hitTarget = pointerHitTarget;
|
||||
eventResult.activeSplitterNodeId = splitter->nodeId;
|
||||
}
|
||||
} else {
|
||||
@@ -643,7 +663,10 @@ UIEditorDockHostInteractionFrame UpdateUIEditorDockHostInteraction(
|
||||
state.dockHostState.hoveredTarget.kind !=
|
||||
UIEditorDockHostHitTargetKind::None;
|
||||
eventResult.consumed = state.dockHostState.focused;
|
||||
eventResult.hitTarget = state.dockHostState.hoveredTarget;
|
||||
eventResult.hitTarget =
|
||||
pointerHitTarget.kind != UIEditorDockHostHitTargetKind::None
|
||||
? pointerHitTarget
|
||||
: state.dockHostState.hoveredTarget;
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -668,6 +691,7 @@ UIEditorDockHostInteractionFrame UpdateUIEditorDockHostInteraction(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Changed;
|
||||
}
|
||||
EndUISplitterDrag(state.splitterDragState);
|
||||
ClearAllTabStripTransientInteractions(state);
|
||||
eventResult.consumed = true;
|
||||
eventResult.releasePointerCapture = true;
|
||||
eventResult.activeSplitterNodeId = state.dockHostState.activeSplitterNodeId;
|
||||
|
||||
@@ -143,6 +143,18 @@ const UIEditorDockHostTabStackLayout* FindTabStackByNodeId(
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const XCEngine::UI::Editor::Widgets::UIEditorDockHostSplitterLayout* FindSplitterByNodeId(
|
||||
const UIEditorDockHostLayout& layout,
|
||||
std::string_view nodeId) {
|
||||
for (const auto& splitter : layout.splitters) {
|
||||
if (splitter.nodeId == nodeId) {
|
||||
return &splitter;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIEditorDockHostInteractionTest, SplitterDragUpdatesWorkspaceSplitRatio) {
|
||||
@@ -216,6 +228,87 @@ TEST(UIEditorDockHostInteractionTest, FocusLostWhileDraggingSplitterRequestsPoin
|
||||
EXPECT_TRUE(state.dockHostState.activeSplitterNodeId.empty());
|
||||
}
|
||||
|
||||
TEST(UIEditorDockHostInteractionTest, SplitterGestureDoesNotLeaveGhostTabDragWhenHitZoneOverlapsTabHeader) {
|
||||
auto controller =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
UIEditorDockHostInteractionState state = {};
|
||||
|
||||
auto frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{});
|
||||
const auto* consoleStack = FindTabStackByNodeId(frame.layout, "console-node");
|
||||
const auto* rightSplitter = FindSplitterByNodeId(frame.layout, "right-split");
|
||||
ASSERT_NE(consoleStack, nullptr);
|
||||
ASSERT_NE(rightSplitter, nullptr);
|
||||
|
||||
const UIRect overlapRect(
|
||||
(std::max)(consoleStack->tabStripLayout.headerRect.x, rightSplitter->handleHitRect.x),
|
||||
(std::max)(consoleStack->tabStripLayout.headerRect.y, rightSplitter->handleHitRect.y),
|
||||
(std::min)(
|
||||
consoleStack->tabStripLayout.headerRect.x + consoleStack->tabStripLayout.headerRect.width,
|
||||
rightSplitter->handleHitRect.x + rightSplitter->handleHitRect.width) -
|
||||
(std::max)(consoleStack->tabStripLayout.headerRect.x, rightSplitter->handleHitRect.x),
|
||||
(std::min)(
|
||||
consoleStack->tabStripLayout.headerRect.y + consoleStack->tabStripLayout.headerRect.height,
|
||||
rightSplitter->handleHitRect.y + rightSplitter->handleHitRect.height) -
|
||||
(std::max)(consoleStack->tabStripLayout.headerRect.y, rightSplitter->handleHitRect.y));
|
||||
ASSERT_GT(overlapRect.width, 0.0f);
|
||||
ASSERT_GT(overlapRect.height, 0.0f);
|
||||
const UIPoint overlapPoint = RectCenter(overlapRect);
|
||||
const UIPoint splitterDragPoint(overlapPoint.x, overlapPoint.y + 24.0f);
|
||||
const UIPoint probeMovePoint(
|
||||
consoleStack->tabStripLayout.headerRect.x + 8.0f,
|
||||
consoleStack->tabStripLayout.headerRect.y + consoleStack->tabStripLayout.headerRect.height * 0.5f);
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerMove(overlapPoint.x, overlapPoint.y) });
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerDown(overlapPoint.x, overlapPoint.y) });
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
EXPECT_TRUE(frame.result.requestPointerCapture);
|
||||
EXPECT_EQ(frame.result.activeSplitterNodeId, "right-split");
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerMove(splitterDragPoint.x, splitterDragPoint.y) });
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
EXPECT_TRUE(frame.result.layoutChanged);
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerUp(splitterDragPoint.x, splitterDragPoint.y) });
|
||||
EXPECT_TRUE(frame.result.releasePointerCapture);
|
||||
EXPECT_TRUE(state.dockHostState.activeSplitterNodeId.empty());
|
||||
EXPECT_TRUE(state.activeTabDragNodeId.empty());
|
||||
EXPECT_TRUE(state.activeTabDragPanelId.empty());
|
||||
EXPECT_FALSE(state.dockHostState.dropPreview.visible);
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerMove(probeMovePoint.x, probeMovePoint.y) });
|
||||
EXPECT_FALSE(frame.result.requestPointerCapture);
|
||||
EXPECT_FALSE(frame.result.releasePointerCapture);
|
||||
EXPECT_FALSE(frame.result.layoutChanged);
|
||||
EXPECT_TRUE(state.activeTabDragNodeId.empty());
|
||||
EXPECT_TRUE(state.activeTabDragPanelId.empty());
|
||||
EXPECT_FALSE(state.dockHostState.dropPreview.visible);
|
||||
}
|
||||
|
||||
TEST(UIEditorDockHostInteractionTest, ClickingTabActivatesTargetPanel) {
|
||||
auto controller =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
|
||||
Reference in New Issue
Block a user