fix(editor_ui): resolve splitter and tab drag gesture conflict
This commit is contained in:
@@ -53,6 +53,10 @@ struct UIEditorTabStripInteractionFrame {
|
|||||||
bool focused = false;
|
bool focused = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Clears transient pointer-driven state while preserving selection/navigation.
|
||||||
|
void ClearUIEditorTabStripTransientInteraction(
|
||||||
|
UIEditorTabStripInteractionState& state);
|
||||||
|
|
||||||
UIEditorTabStripInteractionFrame UpdateUIEditorTabStripInteraction(
|
UIEditorTabStripInteractionFrame UpdateUIEditorTabStripInteraction(
|
||||||
UIEditorTabStripInteractionState& state,
|
UIEditorTabStripInteractionState& state,
|
||||||
std::string& selectedTabId,
|
std::string& selectedTabId,
|
||||||
|
|||||||
@@ -363,6 +363,14 @@ void CancelTabReorder(
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
void ClearUIEditorTabStripTransientInteraction(
|
||||||
|
UIEditorTabStripInteractionState& state) {
|
||||||
|
state.pressedTarget = {};
|
||||||
|
state.hasPointerPosition = false;
|
||||||
|
ClearHoverState(state);
|
||||||
|
ClearReorderState(state);
|
||||||
|
}
|
||||||
|
|
||||||
UIEditorTabStripInteractionFrame UpdateUIEditorTabStripInteraction(
|
UIEditorTabStripInteractionFrame UpdateUIEditorTabStripInteraction(
|
||||||
UIEditorTabStripInteractionState& state,
|
UIEditorTabStripInteractionState& state,
|
||||||
std::string& selectedTabId,
|
std::string& selectedTabId,
|
||||||
|
|||||||
@@ -192,6 +192,14 @@ bool IsPointInsideRect(
|
|||||||
point.y <= rect.y + rect.height;
|
point.y <= rect.y + rect.height;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ClearAllTabStripTransientInteractions(
|
||||||
|
UIEditorDockHostInteractionState& state) {
|
||||||
|
for (UIEditorDockHostTabStripInteractionEntry& entry : state.tabStripInteractions) {
|
||||||
|
ClearUIEditorTabStripTransientInteraction(entry.state);
|
||||||
|
}
|
||||||
|
SyncDockHostTabStripVisualStates(state);
|
||||||
|
}
|
||||||
|
|
||||||
void ClearTabDockDragState(UIEditorDockHostInteractionState& state) {
|
void ClearTabDockDragState(UIEditorDockHostInteractionState& state) {
|
||||||
state.activeTabDragNodeId.clear();
|
state.activeTabDragNodeId.clear();
|
||||||
state.activeTabDragPanelId.clear();
|
state.activeTabDragPanelId.clear();
|
||||||
@@ -510,9 +518,19 @@ UIEditorDockHostInteractionFrame UpdateUIEditorDockHostInteraction(
|
|||||||
state.hasPointerPosition = false;
|
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 = {};
|
UIEditorDockHostInteractionResult eventResult = {};
|
||||||
const DockHostTabStripEventResult tabStripResult =
|
const DockHostTabStripEventResult tabStripResult =
|
||||||
ShouldDispatchTabStripEvent(event, state.splitterDragState.active)
|
!splitterOwnsPointerDown &&
|
||||||
|
ShouldDispatchTabStripEvent(event, state.splitterDragState.active)
|
||||||
? ProcessTabStripEvent(state, layout, event, metrics)
|
? ProcessTabStripEvent(state, layout, event, metrics)
|
||||||
: DockHostTabStripEventResult {};
|
: DockHostTabStripEventResult {};
|
||||||
eventResult.requestPointerCapture = tabStripResult.requestPointerCapture;
|
eventResult.requestPointerCapture = tabStripResult.requestPointerCapture;
|
||||||
@@ -541,6 +559,7 @@ UIEditorDockHostInteractionFrame UpdateUIEditorDockHostInteraction(
|
|||||||
if (state.splitterDragState.active) {
|
if (state.splitterDragState.active) {
|
||||||
EndUISplitterDrag(state.splitterDragState);
|
EndUISplitterDrag(state.splitterDragState);
|
||||||
state.dockHostState.activeSplitterNodeId.clear();
|
state.dockHostState.activeSplitterNodeId.clear();
|
||||||
|
ClearAllTabStripTransientInteractions(state);
|
||||||
eventResult.consumed = true;
|
eventResult.consumed = true;
|
||||||
eventResult.releasePointerCapture = true;
|
eventResult.releasePointerCapture = true;
|
||||||
}
|
}
|
||||||
@@ -609,11 +628,11 @@ UIEditorDockHostInteractionFrame UpdateUIEditorDockHostInteraction(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.dockHostState.hoveredTarget.kind ==
|
if (pointerHitTarget.kind ==
|
||||||
UIEditorDockHostHitTargetKind::SplitterHandle) {
|
UIEditorDockHostHitTargetKind::SplitterHandle) {
|
||||||
const auto* splitter = FindUIEditorDockHostSplitterLayout(
|
const auto* splitter = FindUIEditorDockHostSplitterLayout(
|
||||||
layout,
|
layout,
|
||||||
state.dockHostState.hoveredTarget.nodeId);
|
pointerHitTarget.nodeId);
|
||||||
if (splitter != nullptr &&
|
if (splitter != nullptr &&
|
||||||
BeginUISplitterDrag(
|
BeginUISplitterDrag(
|
||||||
1u,
|
1u,
|
||||||
@@ -626,11 +645,12 @@ UIEditorDockHostInteractionFrame UpdateUIEditorDockHostInteraction(
|
|||||||
splitter->metrics,
|
splitter->metrics,
|
||||||
state.pointerPosition,
|
state.pointerPosition,
|
||||||
state.splitterDragState)) {
|
state.splitterDragState)) {
|
||||||
|
ClearAllTabStripTransientInteractions(state);
|
||||||
state.dockHostState.activeSplitterNodeId = splitter->nodeId;
|
state.dockHostState.activeSplitterNodeId = splitter->nodeId;
|
||||||
state.dockHostState.focused = true;
|
state.dockHostState.focused = true;
|
||||||
eventResult.consumed = true;
|
eventResult.consumed = true;
|
||||||
eventResult.requestPointerCapture = true;
|
eventResult.requestPointerCapture = true;
|
||||||
eventResult.hitTarget = state.dockHostState.hoveredTarget;
|
eventResult.hitTarget = pointerHitTarget;
|
||||||
eventResult.activeSplitterNodeId = splitter->nodeId;
|
eventResult.activeSplitterNodeId = splitter->nodeId;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -643,7 +663,10 @@ UIEditorDockHostInteractionFrame UpdateUIEditorDockHostInteraction(
|
|||||||
state.dockHostState.hoveredTarget.kind !=
|
state.dockHostState.hoveredTarget.kind !=
|
||||||
UIEditorDockHostHitTargetKind::None;
|
UIEditorDockHostHitTargetKind::None;
|
||||||
eventResult.consumed = state.dockHostState.focused;
|
eventResult.consumed = state.dockHostState.focused;
|
||||||
eventResult.hitTarget = state.dockHostState.hoveredTarget;
|
eventResult.hitTarget =
|
||||||
|
pointerHitTarget.kind != UIEditorDockHostHitTargetKind::None
|
||||||
|
? pointerHitTarget
|
||||||
|
: state.dockHostState.hoveredTarget;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -668,6 +691,7 @@ UIEditorDockHostInteractionFrame UpdateUIEditorDockHostInteraction(
|
|||||||
UIEditorWorkspaceLayoutOperationStatus::Changed;
|
UIEditorWorkspaceLayoutOperationStatus::Changed;
|
||||||
}
|
}
|
||||||
EndUISplitterDrag(state.splitterDragState);
|
EndUISplitterDrag(state.splitterDragState);
|
||||||
|
ClearAllTabStripTransientInteractions(state);
|
||||||
eventResult.consumed = true;
|
eventResult.consumed = true;
|
||||||
eventResult.releasePointerCapture = true;
|
eventResult.releasePointerCapture = true;
|
||||||
eventResult.activeSplitterNodeId = state.dockHostState.activeSplitterNodeId;
|
eventResult.activeSplitterNodeId = state.dockHostState.activeSplitterNodeId;
|
||||||
|
|||||||
@@ -143,6 +143,18 @@ const UIEditorDockHostTabStackLayout* FindTabStackByNodeId(
|
|||||||
return nullptr;
|
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
|
} // namespace
|
||||||
|
|
||||||
TEST(UIEditorDockHostInteractionTest, SplitterDragUpdatesWorkspaceSplitRatio) {
|
TEST(UIEditorDockHostInteractionTest, SplitterDragUpdatesWorkspaceSplitRatio) {
|
||||||
@@ -216,6 +228,87 @@ TEST(UIEditorDockHostInteractionTest, FocusLostWhileDraggingSplitterRequestsPoin
|
|||||||
EXPECT_TRUE(state.dockHostState.activeSplitterNodeId.empty());
|
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) {
|
TEST(UIEditorDockHostInteractionTest, ClickingTabActivatesTargetPanel) {
|
||||||
auto controller =
|
auto controller =
|
||||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||||
|
|||||||
Reference in New Issue
Block a user