diff --git a/docs/plan/SceneViewport_Overlay_Gizmo_Rework_Checkpoint_2026-04-02.md b/docs/plan/SceneViewport_Overlay_Gizmo_Rework_Checkpoint_2026-04-02.md index 839389cc..885aba72 100644 --- a/docs/plan/SceneViewport_Overlay_Gizmo_Rework_Checkpoint_2026-04-02.md +++ b/docs/plan/SceneViewport_Overlay_Gizmo_Rework_Checkpoint_2026-04-02.md @@ -1,5 +1,27 @@ # SceneViewport Overlay/Gizmo Rework Checkpoint +## Update 2026-04-04 Phase 5G + +### Chrome/Focus/Presentation Shell Formalization Completed + +- Added `SceneViewportChrome.{h,cpp}` to take over the remaining scene viewport chrome responsibilities: + - top toolbar rendering + - in-viewport tool overlay rendering + - unified tool-change command building and execution +- Extended `SceneViewportInteractionFrame.h` with explicit helpers for: + - post-interaction viewport focus decisions + - final scene viewport presentation refresh and HUD draw +- `SceneViewPanel` no longer owns inline scene toolbar/tool overlay rendering, ad-hoc tool switching glue, focus heuristics, or the final gizmo refresh/HUD presentation tail path. +- Added focused editor tests covering tool-command composition/execution and the new focus/presentation helpers. + +### Verification + +- `cmake --build build --config Debug --target editor_tests -- /p:BuildProjectReferences=false` +- `build/tests/Editor/Debug/editor_tests.exe --gtest_filter=SceneViewportChromeTest.*:SceneViewportNavigationTest.*:SceneViewportInteractionFrameTest.*:SceneViewportInteractionActionsTest.*:SceneViewportInteractionResolverTest.*:SceneViewportTransformGizmoCoordinatorTest.*:SceneViewportOverlayRenderer_Test.*:SceneViewportOverlayProviderRegistryTest.*:ViewportRenderFlowUtilsTest.*` +- `cmake --build build --config Debug --target XCEditor` + +All commands completed successfully in `Debug`. + ## Update 2026-04-04 Phase 5F ### Interaction Frame/Request Glue Formalization Completed diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index c02bfbbb..645207ed 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -83,6 +83,7 @@ add_executable(${PROJECT_NAME} WIN32 src/Viewport/SceneViewportRotateGizmo.cpp src/Viewport/SceneViewportScaleGizmo.cpp src/Viewport/SceneViewportHudOverlay.cpp + src/Viewport/SceneViewportChrome.cpp src/Viewport/SceneViewportInteractionActions.cpp src/Viewport/SceneViewportInteractionResolver.cpp src/Viewport/SceneViewportTransformGizmoCoordinator.cpp diff --git a/editor/src/Viewport/SceneViewportChrome.cpp b/editor/src/Viewport/SceneViewportChrome.cpp new file mode 100644 index 00000000..55ce61d2 --- /dev/null +++ b/editor/src/Viewport/SceneViewportChrome.cpp @@ -0,0 +1,231 @@ +#include "SceneViewportChrome.h" + +#include "Platform/Win32Utf8.h" +#include "UI/UI.h" + +#include + +#include +#include + +namespace XCEngine { +namespace Editor { +namespace { + +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); +} + +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) { + const std::filesystem::path exeDir( + Platform::Utf8ToWide(Platform::GetExecutableDirectoryUtf8())); + std::filesystem::path iconPath = + exeDir / L".." / L".." / L"resources" / L"Icons" / + std::filesystem::path(Platform::Utf8ToWide(iconBaseName)); + if (active) { + iconPath += L"_on"; + } + iconPath += L".png"; + return 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; +} + +} // namespace + +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); +} + +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 Editor +} // namespace XCEngine diff --git a/editor/src/Viewport/SceneViewportChrome.h b/editor/src/Viewport/SceneViewportChrome.h new file mode 100644 index 00000000..cc692ade --- /dev/null +++ b/editor/src/Viewport/SceneViewportChrome.h @@ -0,0 +1,88 @@ +#pragma once + +#include "Core/IUndoManager.h" +#include "SceneViewportMoveGizmo.h" +#include "SceneViewportNavigation.h" +#include "SceneViewportRotateGizmo.h" +#include "SceneViewportScaleGizmo.h" +#include "panels/ViewportPanelContent.h" + +namespace XCEngine { +namespace Editor { + +struct SceneViewportToolOverlayResult { + bool hovered = false; + bool clicked = false; + SceneViewportToolMode clickedTool = SceneViewportToolMode::Move; +}; + +struct SceneViewportToolCommand { + SceneViewportToolMode targetTool = SceneViewportToolMode::Move; + bool triggered = false; + bool cancelMoveGizmo = false; + bool cancelRotateGizmo = false; + bool cancelScaleGizmo = false; + + bool HasAction() const { + return triggered; + } +}; + +void RenderSceneViewportTopBar( + SceneViewportPivotMode& pivotMode, + SceneViewportTransformSpaceMode& transformSpaceMode); + +SceneViewportToolOverlayResult RenderSceneViewportToolOverlay( + const ViewportPanelContentResult& content, + SceneViewportToolMode activeTool); + +inline SceneViewportToolCommand BuildSceneViewportToolCommand( + const SceneViewportToolOverlayResult& overlayResult, + const SceneViewportToolShortcutAction& shortcutAction) { + SceneViewportToolCommand command = {}; + + if (overlayResult.clicked) { + command.targetTool = overlayResult.clickedTool; + command.triggered = true; + command.cancelMoveGizmo = true; + command.cancelRotateGizmo = true; + command.cancelScaleGizmo = true; + } + + if (shortcutAction.HasAction()) { + command.targetTool = shortcutAction.targetTool; + command.triggered = true; + command.cancelMoveGizmo = command.cancelMoveGizmo || shortcutAction.cancelMoveGizmo; + command.cancelRotateGizmo = command.cancelRotateGizmo || shortcutAction.cancelRotateGizmo; + command.cancelScaleGizmo = command.cancelScaleGizmo || shortcutAction.cancelScaleGizmo; + } + + return command; +} + +inline void ExecuteSceneViewportToolCommand( + const SceneViewportToolCommand& command, + IUndoManager& undoManager, + SceneViewportMoveGizmo& moveGizmo, + SceneViewportRotateGizmo& rotateGizmo, + SceneViewportScaleGizmo& scaleGizmo, + SceneViewportToolMode& toolMode) { + if (!command.HasAction()) { + return; + } + + if (command.cancelMoveGizmo && moveGizmo.IsActive()) { + moveGizmo.CancelDrag(&undoManager); + } + if (command.cancelRotateGizmo && rotateGizmo.IsActive()) { + rotateGizmo.CancelDrag(&undoManager); + } + if (command.cancelScaleGizmo && scaleGizmo.IsActive()) { + scaleGizmo.CancelDrag(&undoManager); + } + + toolMode = command.targetTool; +} + +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/Viewport/SceneViewportInteractionFrame.h b/editor/src/Viewport/SceneViewportInteractionFrame.h index 097d8231..764ddf98 100644 --- a/editor/src/Viewport/SceneViewportInteractionFrame.h +++ b/editor/src/Viewport/SceneViewportInteractionFrame.h @@ -1,9 +1,11 @@ #pragma once #include "Core/IEditorContext.h" +#include "SceneViewportNavigation.h" #include "IViewportHostService.h" #include "SceneViewportEditorModes.h" #include "SceneViewportHudOverlay.h" +#include "SceneViewportInteractionActions.h" #include "SceneViewportInteractionResolver.h" #include "SceneViewportTransformGizmoCoordinator.h" @@ -128,5 +130,62 @@ inline SceneViewportInteractionResolveRequest BuildSceneViewportInteractionResol return request; } +inline bool ShouldFocusSceneViewportAfterInteraction( + bool toolCommandTriggered, + const SceneViewportInteractionActions& interactionActions, + const SceneViewportNavigationUpdate& navigationUpdate) { + return toolCommandTriggered || + interactionActions.HasClickAction() || + navigationUpdate.beginLookDrag || + navigationUpdate.beginPanDrag; +} + +struct SceneViewportPresentationRequest { + IEditorContext* context = nullptr; + IViewportHostService* viewportHostService = nullptr; + bool hasInteractiveViewport = false; + SceneViewportFrameGeometry geometry = {}; + SceneViewportTransformGizmoFrameOptions gizmoFrameOptions = {}; + SceneViewportMoveGizmo* moveGizmo = nullptr; + SceneViewportRotateGizmo* rotateGizmo = nullptr; + SceneViewportScaleGizmo* scaleGizmo = nullptr; + ImDrawList* drawList = nullptr; + ImVec2 viewportMin = ImVec2(0.0f, 0.0f); + ImVec2 viewportMax = ImVec2(0.0f, 0.0f); + + bool IsValid() const { + return context != nullptr && + viewportHostService != nullptr && + moveGizmo != nullptr && + rotateGizmo != nullptr && + scaleGizmo != nullptr; + } +}; + +inline void RefreshAndDrawSceneViewportPresentation(const SceneViewportPresentationRequest& request) { + if (!request.IsValid() || !request.hasInteractiveViewport) { + return; + } + + const SceneViewportOverlayData overlay = request.viewportHostService->GetSceneViewOverlayData(); + RefreshAndSubmitSceneViewportTransformGizmoFrame( + *request.viewportHostService, + BuildSceneViewportTransformGizmoRefreshRequest( + *request.context, + overlay, + request.geometry.viewportSize, + request.geometry.localMousePosition, + request.gizmoFrameOptions), + *request.moveGizmo, + *request.rotateGizmo, + *request.scaleGizmo); + + DrawSceneViewportHudOverlay( + request.drawList, + BuildSceneViewportHudOverlayData(overlay), + request.viewportMin, + request.viewportMax); +} + } // namespace Editor } // namespace XCEngine diff --git a/editor/src/panels/SceneViewPanel.cpp b/editor/src/panels/SceneViewPanel.cpp index c0a8f06b..a1d0c2b2 100644 --- a/editor/src/panels/SceneViewPanel.cpp +++ b/editor/src/panels/SceneViewPanel.cpp @@ -3,8 +3,8 @@ #include "Core/ISceneManager.h" #include "Core/ISelectionManager.h" #include "SceneViewPanel.h" +#include "Viewport/SceneViewportChrome.h" #include "Viewport/SceneViewportEditorOverlayData.h" -#include "Viewport/SceneViewportHudOverlay.h" #include "Viewport/SceneViewportInteractionFrame.h" #include "Viewport/SceneViewportInteractionActions.h" #include "Viewport/SceneViewportInteractionResolver.h" @@ -12,241 +12,12 @@ #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() { @@ -265,11 +36,6 @@ void SceneViewPanel::Render() { 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; @@ -280,18 +46,15 @@ void SceneViewPanel::Render() { 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 SceneViewportToolCommand toolCommand = + BuildSceneViewportToolCommand(toolOverlay, toolShortcutAction); + ExecuteSceneViewportToolCommand( + toolCommand, + m_context->GetUndoManager(), + m_moveGizmo, + m_rotateGizmo, + m_scaleGizmo, + m_toolMode); const SceneViewportToolState toolState = BuildSceneViewportToolState( m_toolMode, @@ -362,8 +125,10 @@ void SceneViewPanel::Render() { UpdateSceneViewportNavigationState(navigationRequest); m_navigationState = navigationUpdate.state; - if (toolOverlay.clicked || interactionActions.HasClickAction() || navigationUpdate.beginLookDrag || - navigationUpdate.beginPanDrag) { + if (ShouldFocusSceneViewportAfterInteraction( + toolCommand.HasAction(), + interactionActions, + navigationUpdate)) { ImGui::SetWindowFocus(); } @@ -426,26 +191,19 @@ void SceneViewPanel::Render() { viewportHostService->UpdateSceneViewInput(*m_context, input); - if (content.hasViewportArea && content.frame.hasTexture) { - const SceneViewportOverlayData overlay = viewportHostService->GetSceneViewOverlayData(); - RefreshAndSubmitSceneViewportTransformGizmoFrame( - *viewportHostService, - BuildSceneViewportTransformGizmoRefreshRequest( - *m_context, - overlay, - frameGeometry.viewportSize, - frameGeometry.localMousePosition, - toolState.gizmoFrameOptions), - m_moveGizmo, - m_rotateGizmo, - m_scaleGizmo); - - DrawSceneViewportHudOverlay( - ImGui::GetWindowDrawList(), - BuildSceneViewportHudOverlayData(overlay), - content.itemMin, - content.itemMax); - } + SceneViewportPresentationRequest presentationRequest = {}; + presentationRequest.context = m_context; + presentationRequest.viewportHostService = viewportHostService; + presentationRequest.hasInteractiveViewport = hasInteractiveViewport; + presentationRequest.geometry = frameGeometry; + presentationRequest.gizmoFrameOptions = toolState.gizmoFrameOptions; + presentationRequest.moveGizmo = &m_moveGizmo; + presentationRequest.rotateGizmo = &m_rotateGizmo; + presentationRequest.scaleGizmo = &m_scaleGizmo; + presentationRequest.drawList = ImGui::GetWindowDrawList(); + presentationRequest.viewportMin = content.itemMin; + presentationRequest.viewportMax = content.itemMax; + RefreshAndDrawSceneViewportPresentation(presentationRequest); } Actions::ObserveInactiveActionRoute(*m_context); diff --git a/tests/editor/CMakeLists.txt b/tests/editor/CMakeLists.txt index 0dfd498c..5d94ea4b 100644 --- a/tests/editor/CMakeLists.txt +++ b/tests/editor/CMakeLists.txt @@ -15,6 +15,7 @@ set(EDITOR_TEST_SOURCES test_scene_viewport_interaction_actions.cpp test_scene_viewport_interaction_resolver.cpp test_scene_viewport_interaction_frame.cpp + test_scene_viewport_chrome.cpp test_scene_viewport_transform_gizmo_coordinator.cpp test_scene_viewport_shader_paths.cpp test_scene_viewport_overlay_renderer.cpp diff --git a/tests/editor/test_scene_viewport_chrome.cpp b/tests/editor/test_scene_viewport_chrome.cpp new file mode 100644 index 00000000..8e15ede7 --- /dev/null +++ b/tests/editor/test_scene_viewport_chrome.cpp @@ -0,0 +1,113 @@ +#include + +#include "Core/IUndoManager.h" +#include "Viewport/SceneViewportChrome.h" +#include "Viewport/SceneViewportMoveGizmo.h" +#include "Viewport/SceneViewportRotateGizmo.h" +#include "Viewport/SceneViewportScaleGizmo.h" + +namespace { + +using XCEngine::Editor::BuildSceneViewportToolCommand; +using XCEngine::Editor::ExecuteSceneViewportToolCommand; +using XCEngine::Editor::SceneViewportToolCommand; +using XCEngine::Editor::SceneViewportToolMode; +using XCEngine::Editor::SceneViewportToolOverlayResult; +using XCEngine::Editor::SceneViewportToolShortcutAction; + +class StubUndoManager : public XCEngine::Editor::IUndoManager { +public: + void ClearHistory() override {} + bool CanUndo() const override { return false; } + bool CanRedo() const override { return false; } + const std::string& GetUndoLabel() const override { return empty; } + const std::string& GetRedoLabel() const override { return empty; } + void Undo() override {} + void Redo() override {} + XCEngine::Editor::UndoStateSnapshot CaptureCurrentState() const override { return {}; } + void PushCommand(const std::string&, XCEngine::Editor::UndoStateSnapshot, XCEngine::Editor::UndoStateSnapshot) override {} + void BeginInteractiveChange(const std::string&) override {} + bool HasPendingInteractiveChange() const override { return false; } + void FinalizeInteractiveChange() override {} + void CancelInteractiveChange() override {} + +private: + std::string empty; +}; + +} // namespace + +TEST(SceneViewportChromeTest, ToolCommandUsesOverlayClickAsFullToolSwitch) { + SceneViewportToolOverlayResult overlay = {}; + overlay.clicked = true; + overlay.clickedTool = SceneViewportToolMode::Transform; + + const SceneViewportToolShortcutAction shortcut = {}; + const SceneViewportToolCommand command = BuildSceneViewportToolCommand(overlay, shortcut); + + EXPECT_TRUE(command.HasAction()); + EXPECT_EQ(command.targetTool, SceneViewportToolMode::Transform); + EXPECT_TRUE(command.cancelMoveGizmo); + EXPECT_TRUE(command.cancelRotateGizmo); + EXPECT_TRUE(command.cancelScaleGizmo); +} + +TEST(SceneViewportChromeTest, ToolCommandUsesShortcutWhenOverlayDidNotTrigger) { + const SceneViewportToolOverlayResult overlay = {}; + + SceneViewportToolShortcutAction shortcut = {}; + shortcut.triggered = true; + shortcut.targetTool = SceneViewportToolMode::Rotate; + shortcut.cancelMoveGizmo = true; + shortcut.cancelScaleGizmo = true; + + const SceneViewportToolCommand command = BuildSceneViewportToolCommand(overlay, shortcut); + + EXPECT_TRUE(command.HasAction()); + EXPECT_EQ(command.targetTool, SceneViewportToolMode::Rotate); + EXPECT_TRUE(command.cancelMoveGizmo); + EXPECT_FALSE(command.cancelRotateGizmo); + EXPECT_TRUE(command.cancelScaleGizmo); +} + +TEST(SceneViewportChromeTest, ShortcutOverridesOverlayTargetWhilePreservingCancelUnion) { + SceneViewportToolOverlayResult overlay = {}; + overlay.clicked = true; + overlay.clickedTool = SceneViewportToolMode::Move; + + SceneViewportToolShortcutAction shortcut = {}; + shortcut.triggered = true; + shortcut.targetTool = SceneViewportToolMode::Scale; + shortcut.cancelMoveGizmo = true; + shortcut.cancelRotateGizmo = true; + + const SceneViewportToolCommand command = BuildSceneViewportToolCommand(overlay, shortcut); + + EXPECT_TRUE(command.HasAction()); + EXPECT_EQ(command.targetTool, SceneViewportToolMode::Scale); + EXPECT_TRUE(command.cancelMoveGizmo); + EXPECT_TRUE(command.cancelRotateGizmo); + EXPECT_TRUE(command.cancelScaleGizmo); +} + +TEST(SceneViewportChromeTest, ExecuteToolCommandUpdatesToolModeWhenTriggered) { + StubUndoManager undoManager = {}; + XCEngine::Editor::SceneViewportMoveGizmo moveGizmo = {}; + XCEngine::Editor::SceneViewportRotateGizmo rotateGizmo = {}; + XCEngine::Editor::SceneViewportScaleGizmo scaleGizmo = {}; + SceneViewportToolMode toolMode = SceneViewportToolMode::Move; + + SceneViewportToolCommand command = {}; + command.triggered = true; + command.targetTool = SceneViewportToolMode::ViewMove; + + ExecuteSceneViewportToolCommand( + command, + undoManager, + moveGizmo, + rotateGizmo, + scaleGizmo, + toolMode); + + EXPECT_EQ(toolMode, SceneViewportToolMode::ViewMove); +} diff --git a/tests/editor/test_scene_viewport_interaction_frame.cpp b/tests/editor/test_scene_viewport_interaction_frame.cpp index 58c459f0..5eeb4506 100644 --- a/tests/editor/test_scene_viewport_interaction_frame.cpp +++ b/tests/editor/test_scene_viewport_interaction_frame.cpp @@ -28,16 +28,21 @@ using XCEngine::Editor::IProjectManager; using XCEngine::Editor::ISceneManager; using XCEngine::Editor::ISelectionManager; using XCEngine::Editor::IViewportHostService; +using XCEngine::Editor::RefreshAndDrawSceneViewportPresentation; using XCEngine::Editor::SceneSnapshot; +using XCEngine::Editor::SceneViewportInteractionActions; using XCEngine::Editor::SceneViewportInput; using XCEngine::Editor::SceneViewportInteractionFrameState; +using XCEngine::Editor::SceneViewportNavigationUpdate; using XCEngine::Editor::SceneViewportOrientationAxis; using XCEngine::Editor::SceneViewportOverlayData; using XCEngine::Editor::SceneViewportOverlayFrameData; using XCEngine::Editor::SceneViewportPivotMode; +using XCEngine::Editor::SceneViewportPresentationRequest; using XCEngine::Editor::SceneViewportToolMode; using XCEngine::Editor::SceneViewportTransformGizmoOverlayState; using XCEngine::Editor::SceneViewportTransformSpaceMode; +using XCEngine::Editor::ShouldFocusSceneViewportAfterInteraction; using XCEngine::Rendering::RenderContext; class EmptySelectionManager : public ISelectionManager { @@ -335,3 +340,82 @@ TEST(SceneViewportInteractionFrameTest, ResolveRequestCopiesFrameStateAndViewpor EXPECT_FLOAT_EQ(request.absoluteMousePosition.x, 180.0f); EXPECT_FLOAT_EQ(request.absoluteMousePosition.y, 260.0f); } + +TEST(SceneViewportInteractionFrameTest, FocusHelperTracksToolActionsAndNavigationBegins) { + const SceneViewportInteractionActions noActions = {}; + const SceneViewportNavigationUpdate noNavigation = {}; + EXPECT_FALSE(ShouldFocusSceneViewportAfterInteraction(false, noActions, noNavigation)); + EXPECT_TRUE(ShouldFocusSceneViewportAfterInteraction(true, noActions, noNavigation)); + + SceneViewportInteractionActions clickActions = {}; + clickActions.sceneIconClick = true; + EXPECT_TRUE(ShouldFocusSceneViewportAfterInteraction(false, clickActions, noNavigation)); + + SceneViewportNavigationUpdate navigationUpdate = {}; + navigationUpdate.beginPanDrag = true; + EXPECT_TRUE(ShouldFocusSceneViewportAfterInteraction(false, noActions, navigationUpdate)); +} + +TEST(SceneViewportInteractionFrameTest, PresentationHelperSkipsNonInteractiveViewport) { + StubEditorContext context = {}; + StubViewportHostService viewportHostService = {}; + context.viewportHostService = &viewportHostService; + + XCEngine::Editor::SceneViewportMoveGizmo moveGizmo = {}; + XCEngine::Editor::SceneViewportRotateGizmo rotateGizmo = {}; + XCEngine::Editor::SceneViewportScaleGizmo scaleGizmo = {}; + + SceneViewportPresentationRequest request = {}; + request.context = &context; + request.viewportHostService = &viewportHostService; + request.hasInteractiveViewport = false; + request.geometry = BuildSceneViewportFrameGeometry( + ImVec2(640.0f, 360.0f), + ImVec2(10.0f, 20.0f), + ImVec2(40.0f, 60.0f)); + request.gizmoFrameOptions = BuildSceneViewportToolState( + SceneViewportToolMode::Move, + SceneViewportPivotMode::Pivot, + SceneViewportTransformSpaceMode::Global) + .gizmoFrameOptions; + request.moveGizmo = &moveGizmo; + request.rotateGizmo = &rotateGizmo; + request.scaleGizmo = &scaleGizmo; + + RefreshAndDrawSceneViewportPresentation(request); + EXPECT_EQ(viewportHostService.submissionCount, 0); +} + +TEST(SceneViewportInteractionFrameTest, PresentationHelperRefreshesInteractiveViewportWithoutDrawList) { + StubEditorContext context = {}; + StubViewportHostService viewportHostService = {}; + context.viewportHostService = &viewportHostService; + viewportHostService.overlay.valid = true; + viewportHostService.overlay.verticalFovDegrees = 50.0f; + + XCEngine::Editor::SceneViewportMoveGizmo moveGizmo = {}; + XCEngine::Editor::SceneViewportRotateGizmo rotateGizmo = {}; + XCEngine::Editor::SceneViewportScaleGizmo scaleGizmo = {}; + + SceneViewportPresentationRequest request = {}; + request.context = &context; + request.viewportHostService = &viewportHostService; + request.hasInteractiveViewport = true; + request.geometry = BuildSceneViewportFrameGeometry( + ImVec2(640.0f, 360.0f), + ImVec2(10.0f, 20.0f), + ImVec2(40.0f, 60.0f)); + request.gizmoFrameOptions = BuildSceneViewportToolState( + SceneViewportToolMode::Move, + SceneViewportPivotMode::Pivot, + SceneViewportTransformSpaceMode::Global) + .gizmoFrameOptions; + request.moveGizmo = &moveGizmo; + request.rotateGizmo = &rotateGizmo; + request.scaleGizmo = &scaleGizmo; + request.viewportMin = ImVec2(10.0f, 20.0f); + request.viewportMax = ImVec2(650.0f, 380.0f); + + RefreshAndDrawSceneViewportPresentation(request); + EXPECT_EQ(viewportHostService.submissionCount, 1); +}