#include "Docking/UIEditorDockHostInteractionInternal.h" #include #include #include #include namespace XCEngine::UI::Editor::Detail { 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 ShouldUsePointerPosition(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 BuildTabStripItems( const UIEditorDockHostTabStackLayout& tabStack) { std::vector 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 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::Detail