#include "Actions/ActionRouting.h" #include "Core/IEditorContext.h" #include "Core/ISceneManager.h" #include "Core/ISelectionManager.h" #include "SceneViewPanel.h" #include "Viewport/SceneViewportEditorOverlayData.h" #include "Viewport/SceneViewportHudOverlay.h" #include "Viewport/SceneViewportInteractionActions.h" #include "Viewport/SceneViewportInteractionResolver.h" #include "Viewport/SceneViewportMath.h" #include "Viewport/SceneViewportNavigation.h" #include "Viewport/SceneViewportTransformGizmoCoordinator.h" #include "ViewportPanelContent.h" #include "Platform/Win32Utf8.h" #include "UI/UI.h" #include #include #include #include namespace XCEngine { namespace Editor { namespace { struct SceneViewportToolOverlayResult { bool hovered = false; bool clicked = false; SceneViewportToolMode clickedTool = SceneViewportToolMode::Move; }; const char* GetSceneViewportPivotModeLabel(SceneViewportPivotMode mode) { return mode == SceneViewportPivotMode::Pivot ? "Pivot" : "Center"; } const char* GetSceneViewportTransformSpaceModeLabel(SceneViewportTransformSpaceMode mode) { return mode == SceneViewportTransformSpaceMode::Global ? "Global" : "Local"; } float GetSceneToolbarToggleWidth(const char* firstLabel, const char* secondLabel) { constexpr float kHorizontalPadding = 10.0f; constexpr float kMinWidth = 68.0f; const float maxLabelWidth = (std::max)( ImGui::CalcTextSize(firstLabel).x, ImGui::CalcTextSize(secondLabel).x); return (std::max)(kMinWidth, maxLabelWidth + kHorizontalPadding * 2.0f); } void RenderSceneViewportTopBar( SceneViewportPivotMode& pivotMode, SceneViewportTransformSpaceMode& transformSpaceMode) { constexpr float kSceneToolbarHeight = 24.0f; constexpr float kSceneToolbarPaddingY = 0.0f; constexpr float kSceneToolbarButtonHeight = kSceneToolbarHeight - kSceneToolbarPaddingY * 2.0f; constexpr ImVec2 kSceneToolbarButtonFramePadding(8.0f, 1.0f); UI::PanelToolbarScope toolbar( "SceneToolbar", kSceneToolbarHeight, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse, true, ImVec2(UI::ToolbarPadding().x, kSceneToolbarPaddingY), ImVec2(0.0f, UI::ToolbarItemSpacing().y), ImVec4(0.23f, 0.23f, 0.23f, 1.0f)); if (!toolbar.IsOpen()) { return; } ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, kSceneToolbarButtonFramePadding); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 0.0f); if (UI::ToolbarButton( GetSceneViewportPivotModeLabel(pivotMode), true, ImVec2(GetSceneToolbarToggleWidth("Pivot", "Center"), kSceneToolbarButtonHeight))) { pivotMode = pivotMode == SceneViewportPivotMode::Pivot ? SceneViewportPivotMode::Center : SceneViewportPivotMode::Pivot; } ImGui::SameLine(0.0f, 0.0f); if (UI::ToolbarButton( GetSceneViewportTransformSpaceModeLabel(transformSpaceMode), true, ImVec2(GetSceneToolbarToggleWidth("Global", "Local"), kSceneToolbarButtonHeight))) { transformSpaceMode = transformSpaceMode == SceneViewportTransformSpaceMode::Global ? SceneViewportTransformSpaceMode::Local : SceneViewportTransformSpaceMode::Global; } ImGui::PopStyleVar(2); } const char* GetSceneViewportToolTooltip(SceneViewportToolMode toolMode) { switch (toolMode) { case SceneViewportToolMode::ViewMove: return "View Move"; case SceneViewportToolMode::Move: return "Move"; case SceneViewportToolMode::Rotate: return "Rotate"; case SceneViewportToolMode::Scale: return "Scale"; case SceneViewportToolMode::Transform: return "Transform"; default: return ""; } } const char* GetSceneViewportToolIconBaseName(SceneViewportToolMode toolMode) { switch (toolMode) { case SceneViewportToolMode::ViewMove: return "view_move_tool"; case SceneViewportToolMode::Move: return "move_tool"; case SceneViewportToolMode::Rotate: return "rotate_tool"; case SceneViewportToolMode::Scale: return "scale_tool"; case SceneViewportToolMode::Transform: return "transform_tool"; default: return ""; } } std::string BuildSceneViewportIconPath(const char* iconBaseName, bool active = false) { const std::filesystem::path exeDir( XCEngine::Editor::Platform::Utf8ToWide(XCEngine::Editor::Platform::GetExecutableDirectoryUtf8())); std::filesystem::path iconPath = exeDir / L".." / L".." / L"resources" / L"Icons" / std::filesystem::path(XCEngine::Editor::Platform::Utf8ToWide(iconBaseName)); if (active) { iconPath += L"_on"; } iconPath += L".png"; return XCEngine::Editor::Platform::WideToUtf8(iconPath.lexically_normal().wstring()); } const std::string& GetSceneViewportToolIconPath(SceneViewportToolMode toolMode, bool active) { static std::string cachedPaths[5][2] = {}; const size_t toolIndex = static_cast(toolMode); const size_t stateIndex = active ? 1u : 0u; std::string& cachedPath = cachedPaths[toolIndex][stateIndex]; if (!cachedPath.empty()) { return cachedPath; } cachedPath = BuildSceneViewportIconPath(GetSceneViewportToolIconBaseName(toolMode), active); return cachedPath; } SceneViewportToolOverlayResult RenderSceneViewportToolOverlay( const ViewportPanelContentResult& content, SceneViewportToolMode activeTool) { SceneViewportToolOverlayResult result = {}; if (!content.hasViewportArea) { return result; } constexpr float kButtonExtent = 30.0f; constexpr float kButtonSpacing = 6.0f; constexpr float kPanelPadding = 6.0f; constexpr float kViewportInset = 10.0f; constexpr float kIconInset = 4.0f; constexpr size_t kToolCount = 5; ImDrawList* drawList = ImGui::GetWindowDrawList(); if (drawList == nullptr) { return result; } const ImVec2 panelMin( content.itemMin.x + kViewportInset, content.itemMin.y + kViewportInset); const ImVec2 panelMax( panelMin.x + kPanelPadding * 2.0f + kButtonExtent, panelMin.y + kPanelPadding * 2.0f + kToolCount * kButtonExtent + (kToolCount - 1) * kButtonSpacing); drawList->AddRectFilled(panelMin, panelMax, IM_COL32(24, 26, 29, 220), 7.0f); drawList->AddRect(panelMin, panelMax, IM_COL32(255, 255, 255, 28), 7.0f, 0, 1.0f); const SceneViewportToolMode toolModes[kToolCount] = { SceneViewportToolMode::ViewMove, SceneViewportToolMode::Move, SceneViewportToolMode::Rotate, SceneViewportToolMode::Scale, SceneViewportToolMode::Transform }; for (size_t index = 0; index < kToolCount; ++index) { const SceneViewportToolMode toolMode = toolModes[index]; const bool active = toolMode == activeTool; const ImVec2 buttonMin( panelMin.x + kPanelPadding, panelMin.y + kPanelPadding + index * (kButtonExtent + kButtonSpacing)); const ImVec2 buttonMax(buttonMin.x + kButtonExtent, buttonMin.y + kButtonExtent); ImGui::SetCursorScreenPos(buttonMin); const std::string buttonId = std::string("##SceneToolButton") + std::to_string(static_cast(toolMode)); const bool clicked = ImGui::InvisibleButton( buttonId.c_str(), ImVec2(kButtonExtent, kButtonExtent), ImGuiButtonFlags_MouseButtonLeft | ImGuiButtonFlags_PressedOnClick | ImGuiButtonFlags_AllowOverlap); const bool hovered = ImGui::IsItemHovered(); const bool held = ImGui::IsItemActive(); result.hovered = result.hovered || hovered; result.clicked = result.clicked || clicked; if (clicked) { result.clickedTool = toolMode; } const ImU32 backgroundColor = ImGui::GetColorU32( held ? UI::ToolbarButtonActiveColor() : hovered ? UI::ToolbarButtonHoveredColor(active) : UI::ToolbarButtonColor(active)); drawList->AddRectFilled(buttonMin, buttonMax, backgroundColor, 5.0f); drawList->AddRect(buttonMin, buttonMax, IM_COL32(255, 255, 255, active ? 48 : 24), 5.0f, 0, 1.0f); const ImVec2 iconMin(buttonMin.x + kIconInset, buttonMin.y + kIconInset); const ImVec2 iconMax(buttonMax.x - kIconInset, buttonMax.y - kIconInset); if (!UI::DrawTextureAssetPreview( drawList, iconMin, iconMax, GetSceneViewportToolIconPath(toolMode, active))) { drawList->AddText( ImVec2(buttonMin.x + 8.0f, buttonMin.y + 7.0f), IM_COL32(230, 230, 230, 255), GetSceneViewportToolTooltip(toolMode)); } if (hovered) { ImGui::SetTooltip("%s", GetSceneViewportToolTooltip(toolMode)); } } return result; } } // namespace SceneViewPanel::SceneViewPanel() : Panel("Scene") {} void SceneViewPanel::Render() { ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); UI::PanelWindowScope panel(m_name.c_str()); ImGui::PopStyleVar(); if (!panel.IsOpen()) { return; } RenderSceneViewportTopBar(m_pivotMode, m_transformSpaceMode); const ViewportPanelContentResult content = RenderViewportPanelContent(*m_context, EditorViewportKind::Scene); if (IViewportHostService* viewportHostService = m_context->GetViewportHostService()) { const ImGuiIO& io = ImGui::GetIO(); const bool hasInteractiveViewport = content.hasViewportArea && content.frame.hasTexture; const SceneViewportToolOverlayResult toolOverlay = RenderSceneViewportToolOverlay(content, m_toolMode); const bool viewportContentHovered = content.hovered && !toolOverlay.hovered; if (toolOverlay.clicked) { CancelSceneViewportTransformGizmoFrame(*m_context, m_moveGizmo, m_rotateGizmo, m_scaleGizmo); m_toolMode = toolOverlay.clickedTool; } SceneViewportToolShortcutRequest toolShortcutRequest = {}; toolShortcutRequest.wantTextInput = io.WantTextInput; toolShortcutRequest.lookDragging = m_navigationState.lookDragging; toolShortcutRequest.panDragging = m_navigationState.panDragging; toolShortcutRequest.pressedViewMove = ImGui::IsKeyPressed(ImGuiKey_Q, false); toolShortcutRequest.pressedMove = ImGui::IsKeyPressed(ImGuiKey_W, false); toolShortcutRequest.pressedRotate = ImGui::IsKeyPressed(ImGuiKey_E, false); toolShortcutRequest.pressedScale = ImGui::IsKeyPressed(ImGuiKey_R, false); const SceneViewportToolShortcutAction toolShortcutAction = BuildSceneViewportToolShortcutAction(toolShortcutRequest); if (toolShortcutAction.HasAction()) { if (toolShortcutAction.cancelMoveGizmo && m_moveGizmo.IsActive()) { m_moveGizmo.CancelDrag(&m_context->GetUndoManager()); } if (toolShortcutAction.cancelRotateGizmo && m_rotateGizmo.IsActive()) { m_rotateGizmo.CancelDrag(&m_context->GetUndoManager()); } if (toolShortcutAction.cancelScaleGizmo && m_scaleGizmo.IsActive()) { m_scaleGizmo.CancelDrag(&m_context->GetUndoManager()); } m_toolMode = toolShortcutAction.targetTool; } const bool usingViewMoveTool = m_toolMode == SceneViewportToolMode::ViewMove; const bool usingTransformTool = m_toolMode == SceneViewportToolMode::Transform; const bool showingMoveGizmo = m_toolMode == SceneViewportToolMode::Move || usingTransformTool; const bool showingRotateGizmo = m_toolMode == SceneViewportToolMode::Rotate || usingTransformTool; const bool showingScaleGizmo = m_toolMode == SceneViewportToolMode::Scale || usingTransformTool; const bool useCenterPivot = m_pivotMode == SceneViewportPivotMode::Center; const bool localSpace = m_transformSpaceMode == SceneViewportTransformSpaceMode::Local; const SceneViewportTransformGizmoFrameOptions gizmoFrameOptions = BuildSceneViewportTransformGizmoFrameOptions( useCenterPivot, localSpace, usingTransformTool, showingMoveGizmo, showingRotateGizmo, showingScaleGizmo); const Math::Vector2 viewportSize(content.availableSize.x, content.availableSize.y); const Math::Vector2 localMousePosition( io.MousePos.x - content.itemMin.x, io.MousePos.y - content.itemMin.y); SceneViewportOverlayData overlay = {}; SceneViewportTransformGizmoFrameUpdate interactionGizmoFrame = {}; SceneViewportTransformGizmoFrameState gizmoFrameState = {}; SceneViewportOverlayFrameData emptySceneOverlayFrameData = {}; if (hasInteractiveViewport) { overlay = viewportHostService->GetSceneViewOverlayData(); interactionGizmoFrame = RefreshAndSubmitSceneViewportTransformGizmoFrame( *viewportHostService, BuildSceneViewportTransformGizmoRefreshRequest( *m_context, overlay, viewportSize, localMousePosition, gizmoFrameOptions), m_moveGizmo, m_rotateGizmo, m_scaleGizmo); gizmoFrameState = interactionGizmoFrame.frameState; } else { CancelSceneViewportTransformGizmoFrame( *m_context, m_moveGizmo, m_rotateGizmo, m_scaleGizmo); } const SceneViewportTransformGizmoOverlaySubmission interactionGizmoSubmission = hasInteractiveViewport ? interactionGizmoFrame.overlaySubmission : SceneViewportTransformGizmoOverlaySubmission{}; const SceneViewportOverlayFrameData& interactionOverlayFrameData = hasInteractiveViewport ? viewportHostService->GetSceneViewEditorOverlayFrameData(*m_context) : emptySceneOverlayFrameData; const SceneViewportActiveGizmoKind activeGizmoKind = interactionGizmoSubmission.activeGizmoKind; const bool gizmoActive = interactionGizmoSubmission.GizmoActive(); const SceneViewportHudOverlayData interactionHudOverlay = BuildSceneViewportHudOverlayData(overlay); SceneViewportInteractionResult hoveredInteraction = {}; const bool canResolveViewportInteraction = CanResolveSceneViewportInteraction( hasInteractiveViewport, viewportContentHovered, usingViewMoveTool, m_navigationState, gizmoActive); if (canResolveViewportInteraction) { SceneViewportInteractionResolveRequest interactionRequest = {}; interactionRequest.overlayFrameData = &interactionOverlayFrameData; interactionRequest.viewportSize = viewportSize; interactionRequest.localMousePosition = localMousePosition; interactionRequest.hudOverlay = &interactionHudOverlay; interactionRequest.viewportMin = content.itemMin; interactionRequest.viewportMax = content.itemMax; interactionRequest.absoluteMousePosition = io.MousePos; hoveredInteraction = ResolveSceneViewportInteraction(interactionRequest); } ApplySceneViewportHoveredHandleState( BuildSceneViewportHoveredHandleState(hoveredInteraction), gizmoActive, showingMoveGizmo, m_moveGizmo, showingRotateGizmo, m_rotateGizmo, showingScaleGizmo, m_scaleGizmo); const SceneViewportInteractionActions interactionActions = BuildSceneViewportInteractionActions( hoveredInteraction, hasInteractiveViewport, content.clickedLeft, canResolveViewportInteraction); SceneViewportNavigationRequest navigationRequest = {}; navigationRequest.state = m_navigationState; navigationRequest.hasInteractiveViewport = hasInteractiveViewport; navigationRequest.viewportHovered = viewportContentHovered; navigationRequest.usingViewMoveTool = usingViewMoveTool; navigationRequest.gizmoActive = gizmoActive; navigationRequest.clickedLeft = content.clickedLeft; navigationRequest.clickedRight = content.clickedRight; navigationRequest.clickedMiddle = content.clickedMiddle; navigationRequest.leftMouseDown = ImGui::IsMouseDown(ImGuiMouseButton_Left); navigationRequest.rightMouseDown = ImGui::IsMouseDown(ImGuiMouseButton_Right); navigationRequest.middleMouseDown = ImGui::IsMouseDown(ImGuiMouseButton_Middle); const SceneViewportNavigationUpdate navigationUpdate = UpdateSceneViewportNavigationState(navigationRequest); m_navigationState = navigationUpdate.state; if (toolOverlay.clicked || interactionActions.HasClickAction() || navigationUpdate.beginLookDrag || navigationUpdate.beginPanDrag) { ImGui::SetWindowFocus(); } ExecuteSceneViewportTransformGizmoLifecycleCommand( BuildBeginSceneViewportTransformGizmoLifecycleCommand(interactionActions), m_context->GetUndoManager(), gizmoFrameState, m_moveGizmo, m_rotateGizmo, m_scaleGizmo); DispatchSceneViewportInteractionActions( interactionActions, *m_context, *viewportHostService, content.availableSize, localMousePosition); ExecuteSceneViewportTransformGizmoLifecycleCommand( BuildFrameSceneViewportTransformGizmoLifecycleCommand( activeGizmoKind, ImGui::IsMouseDown(ImGuiMouseButton_Left)), m_context->GetUndoManager(), gizmoFrameState, m_moveGizmo, m_rotateGizmo, m_scaleGizmo); const SceneViewportCaptureFlags captureFlags = BuildSceneViewportCaptureFlags( SceneViewportCaptureRequest{ m_navigationState, m_moveGizmo.IsActive(), m_rotateGizmo.IsActive(), m_scaleGizmo.IsActive()}); if (captureFlags.captureMouse) { ImGui::SetNextFrameWantCaptureMouse(true); } if (captureFlags.captureKeyboard) { ImGui::SetNextFrameWantCaptureKeyboard(true); } SceneViewportInputBuildRequest inputRequest = {}; inputRequest.state = m_navigationState; inputRequest.viewportSize = content.availableSize; inputRequest.mouseDelta = io.MouseDelta; inputRequest.mouseWheel = io.MouseWheel; inputRequest.deltaTime = io.DeltaTime; inputRequest.viewportHovered = viewportContentHovered; inputRequest.viewportFocused = content.focused; inputRequest.wantTextInput = io.WantTextInput; inputRequest.fastMove = io.KeyShift; inputRequest.focusSelectionKeyPressed = ImGui::IsKeyPressed(ImGuiKey_F, false); inputRequest.moveForwardKeyDown = ImGui::IsKeyDown(ImGuiKey_W); inputRequest.moveBackwardKeyDown = ImGui::IsKeyDown(ImGuiKey_S); inputRequest.moveRightKeyDown = ImGui::IsKeyDown(ImGuiKey_D); inputRequest.moveLeftKeyDown = ImGui::IsKeyDown(ImGuiKey_A); inputRequest.moveUpKeyDown = ImGui::IsKeyDown(ImGuiKey_E); inputRequest.moveDownKeyDown = ImGui::IsKeyDown(ImGuiKey_Q); const SceneViewportInput input = BuildSceneViewportInput(inputRequest); viewportHostService->UpdateSceneViewInput(*m_context, input); if (content.hasViewportArea && content.frame.hasTexture) { overlay = viewportHostService->GetSceneViewOverlayData(); RefreshAndSubmitSceneViewportTransformGizmoFrame( *viewportHostService, BuildSceneViewportTransformGizmoRefreshRequest( *m_context, overlay, viewportSize, localMousePosition, gizmoFrameOptions), m_moveGizmo, m_rotateGizmo, m_scaleGizmo); DrawSceneViewportHudOverlay( ImGui::GetWindowDrawList(), BuildSceneViewportHudOverlayData(overlay), content.itemMin, content.itemMax); } } Actions::ObserveInactiveActionRoute(*m_context); } } }