diff --git a/docs/plan/Unity式Tick系统与PlayMode运行时方案-阶段进展.md b/docs/plan/Unity式Tick系统与PlayMode运行时方案-阶段进展.md index d2595d17..6f42f794 100644 --- a/docs/plan/Unity式Tick系统与PlayMode运行时方案-阶段进展.md +++ b/docs/plan/Unity式Tick系统与PlayMode运行时方案-阶段进展.md @@ -28,11 +28,21 @@ - `Paused` 下维持 runtime world,不回退到 editor scene - `Step` 现在只在 `Paused` 下有效,并保持 `Paused` 状态不变 +### 阶段 D 当前收口 + +- 已在软件顶部增加独立运行栏 +- 运行栏已接入 `Play / Stop / Step` 三个图标按钮 +- 顶部按钮直接复用现有 PlayMode 请求通道,不额外分叉状态机 +- `Play` 仅在 `Edit` 且存在活动场景时可用 +- `Stop` 在 `Play / Paused` 下可用 +- `Step` 仍只在 `Paused` 下可用,`Pause / Resume` 继续走 `Run` 菜单与 `F6` + ## 本轮验证 - 已重新执行 `cmake -S . -B build` - 已通过 `cmake --build build --config Debug --target scene_tests` - 已通过 `cmake --build build --config Debug --target editor_tests -- /m:1 /v:minimal` +- 已通过 `cmake --build build --config Debug --target XCEditor -- /m:1 /v:minimal` - 已通过聚焦测试: `ctest --test-dir build -C Debug --output-on-failure -j1 -R "RuntimeLoopTest|PlaySessionControllerTest|EditorActionRoutingTest.*PlayMode|EditorActionRoutingTest.*MainMenuRouterRequestsPlayPauseResumeAndStepEvents"` diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 9020786c..078ac87d 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -102,6 +102,7 @@ target_compile_definitions(${PROJECT_NAME} PRIVATE UNICODE _UNICODE) target_compile_options(${PROJECT_NAME} PRIVATE /utf-8) if(MSVC) + target_compile_options(${PROJECT_NAME} PRIVATE /FS) set_property(TARGET ${PROJECT_NAME} PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL") endif() diff --git a/editor/src/Actions/EditorActions.h b/editor/src/Actions/EditorActions.h index bb500881..c4c21884 100644 --- a/editor/src/Actions/EditorActions.h +++ b/editor/src/Actions/EditorActions.h @@ -176,6 +176,26 @@ inline ActionBinding MakeTogglePlayModeAction(EditorRuntimeMode mode, bool enabl Shortcut(ImGuiKey_F5)); } +inline ActionBinding MakeStartPlayModeAction(EditorRuntimeMode mode, bool hasActiveScene = true) { + return MakeAction( + "Play", + "F5", + IsEditorRuntimeActive(mode), + mode == EditorRuntimeMode::Edit && hasActiveScene, + false, + Shortcut(ImGuiKey_F5)); +} + +inline ActionBinding MakeStopPlayModeAction(EditorRuntimeMode mode) { + return MakeAction( + "Stop", + "F5", + false, + IsEditorRuntimeActive(mode), + false, + Shortcut(ImGuiKey_F5)); +} + inline ActionBinding MakeTogglePauseModeAction(EditorRuntimeMode mode, bool enabled = true) { const bool paused = mode == EditorRuntimeMode::Paused; return MakeAction( diff --git a/editor/src/Actions/MainMenuActionRouter.h b/editor/src/Actions/MainMenuActionRouter.h index 0b976521..18b132e7 100644 --- a/editor/src/Actions/MainMenuActionRouter.h +++ b/editor/src/Actions/MainMenuActionRouter.h @@ -62,15 +62,31 @@ inline void RequestDockLayoutReset(IEditorContext& context) { context.GetEventBus().Publish(DockLayoutResetRequestedEvent{}); } -inline void RequestTogglePlayMode(IEditorContext& context) { - if (context.GetRuntimeMode() == EditorRuntimeMode::Edit) { - context.GetEventBus().Publish(PlayModeStartRequestedEvent{}); +inline void RequestStartPlayMode(IEditorContext& context) { + if (context.GetRuntimeMode() != EditorRuntimeMode::Edit) { + return; + } + + context.GetEventBus().Publish(PlayModeStartRequestedEvent{}); +} + +inline void RequestStopPlayMode(IEditorContext& context) { + if (!IsEditorRuntimeActive(context.GetRuntimeMode())) { return; } context.GetEventBus().Publish(PlayModeStopRequestedEvent{}); } +inline void RequestTogglePlayMode(IEditorContext& context) { + if (context.GetRuntimeMode() == EditorRuntimeMode::Edit) { + RequestStartPlayMode(context); + return; + } + + RequestStopPlayMode(context); +} + inline void RequestTogglePauseMode(IEditorContext& context) { if (context.GetRuntimeMode() == EditorRuntimeMode::Play) { context.GetEventBus().Publish(PlayModePauseRequestedEvent{}); diff --git a/editor/src/panels/MenuBar.cpp b/editor/src/panels/MenuBar.cpp index 91db2ca5..68efcb1d 100644 --- a/editor/src/panels/MenuBar.cpp +++ b/editor/src/panels/MenuBar.cpp @@ -3,11 +3,108 @@ #include "Actions/MainMenuActionRouter.h" #include "MenuBar.h" #include "Core/IEditorContext.h" +#include "Platform/Win32Utf8.h" +#include "UI/UI.h" #include +#include + +#include +#include +#include namespace XCEngine { namespace Editor { +namespace { + +constexpr float kRunToolbarHeight = 40.0f; +constexpr float kRunToolbarButtonExtent = 28.0f; +constexpr float kRunToolbarButtonSpacing = 10.0f; +constexpr float kRunToolbarIconInset = 5.0f; + +std::string BuildRunToolbarIconPath(const char* fileName) { + const std::filesystem::path exeDir( + Platform::Utf8ToWide(Platform::GetExecutableDirectoryUtf8())); + const std::filesystem::path iconPath = + (exeDir / L".." / L".." / L"resources" / L"Icons" / + std::filesystem::path(Platform::Utf8ToWide(fileName))) + .lexically_normal(); + return Platform::WideToUtf8(iconPath.wstring()); +} + +const std::string& GetRunToolbarIconPath(size_t index) { + static const std::array kFileNames = { + "play_button.png", + "stop_button.png", + "step_button.png" + }; + static std::array s_cachedPaths = {}; + + std::string& cachedPath = s_cachedPaths[index]; + if (!cachedPath.empty()) { + return cachedPath; + } + + cachedPath = BuildRunToolbarIconPath(kFileNames[index]); + return cachedPath; +} + +bool DrawRunToolbarIconButton( + const char* id, + const Actions::ActionBinding& action, + const std::string& iconPath, + const char* fallbackGlyph) { + ImGui::BeginDisabled(!action.enabled); + ImGui::PushID(id); + + const ImVec2 buttonSize(kRunToolbarButtonExtent, kRunToolbarButtonExtent); + const ImVec2 buttonMin = ImGui::GetCursorScreenPos(); + const bool pressed = ImGui::InvisibleButton("##RunToolbarButton", buttonSize); + const ImVec2 buttonMax(buttonMin.x + buttonSize.x, buttonMin.y + buttonSize.y); + const bool hovered = ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled); + const bool held = ImGui::IsItemActive(); + + ImDrawList* drawList = ImGui::GetWindowDrawList(); + if (drawList != nullptr) { + const ImU32 fillColor = ImGui::GetColorU32( + held ? UI::ToolbarButtonActiveColor() + : hovered ? UI::ToolbarButtonHoveredColor(action.selected) + : UI::ToolbarButtonColor(action.selected)); + const ImU32 borderColor = IM_COL32(255, 255, 255, action.selected ? 56 : 28); + drawList->AddRectFilled(buttonMin, buttonMax, fillColor, 5.0f); + drawList->AddRect(buttonMin, buttonMax, borderColor, 5.0f, 0, 1.0f); + + const ImVec2 iconMin(buttonMin.x + kRunToolbarIconInset, buttonMin.y + kRunToolbarIconInset); + const ImVec2 iconMax(buttonMax.x - kRunToolbarIconInset, buttonMax.y - kRunToolbarIconInset); + if (!UI::DrawTextureAssetPreview(drawList, iconMin, iconMax, iconPath)) { + const ImVec2 textSize = ImGui::CalcTextSize(fallbackGlyph); + drawList->AddText( + ImVec2( + buttonMin.x + (buttonSize.x - textSize.x) * 0.5f, + buttonMin.y + (buttonSize.y - textSize.y) * 0.5f), + ImGui::GetColorU32(ImVec4(0.88f, 0.88f, 0.88f, 1.0f)), + fallbackGlyph); + } + } + + if (hovered) { + UI::BeginTitledTooltip(action.label.c_str()); + if (!action.shortcutLabel.empty()) { + ImGui::Text("Shortcut: %s", action.shortcutLabel.c_str()); + } + if (!action.enabled && action.label == "Step") { + ImGui::TextUnformatted("Available while runtime is paused."); + } + UI::EndTitledTooltip(); + } + + ImGui::PopID(); + ImGui::EndDisabled(); + return action.enabled && pressed; +} + +} // namespace + MenuBar::MenuBar() : Panel("MenuBar") {} void MenuBar::Render() { @@ -17,8 +114,69 @@ void MenuBar::Render() { Actions::HandleMenuBarShortcuts(*m_context); Actions::DrawMainMenuBar(*m_context, m_aboutPopup); + RenderRunToolbar(); Actions::DrawMainMenuOverlays(m_context, m_aboutPopup); } +void MenuBar::RenderRunToolbar() { + ImGuiViewport* viewport = ImGui::GetMainViewport(); + if (viewport == nullptr) { + return; + } + + constexpr ImGuiWindowFlags kWindowFlags = + ImGuiWindowFlags_NoScrollbar | + ImGuiWindowFlags_NoScrollWithMouse | + ImGuiWindowFlags_NoSavedSettings | + ImGuiWindowFlags_NoDocking | + ImGuiWindowFlags_NoTitleBar | + ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoNavFocus; + + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(12.0f, 6.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); + ImGui::PushStyleColor(ImGuiCol_WindowBg, UI::ToolbarBackgroundColor()); + const bool open = + ImGui::BeginViewportSideBar("##MainRunToolbar", viewport, ImGuiDir_Up, kRunToolbarHeight, kWindowFlags); + ImGui::PopStyleColor(); + ImGui::PopStyleVar(3); + + if (open) { + const Actions::ActionBinding playAction = + Actions::MakeStartPlayModeAction(m_context->GetRuntimeMode(), m_context->GetSceneManager().HasActiveScene()); + const Actions::ActionBinding stopAction = Actions::MakeStopPlayModeAction(m_context->GetRuntimeMode()); + const Actions::ActionBinding stepAction = + Actions::MakeStepPlayModeAction(m_context->GetRuntimeMode() == EditorRuntimeMode::Paused); + + const float totalWidth = + kRunToolbarButtonExtent * 3.0f + kRunToolbarButtonSpacing * 2.0f; + const float startX = + ImGui::GetCursorPosX() + (std::max)(0.0f, (ImGui::GetContentRegionAvail().x - totalWidth) * 0.5f); + const float startY = + (std::max)(ImGui::GetCursorPosY(), (ImGui::GetWindowHeight() - kRunToolbarButtonExtent) * 0.5f); + ImGui::SetCursorPos(ImVec2(startX, startY)); + + if (DrawRunToolbarIconButton("Play", playAction, GetRunToolbarIconPath(0), "P")) { + Actions::RequestStartPlayMode(*m_context); + } + + ImGui::SameLine(0.0f, kRunToolbarButtonSpacing); + if (DrawRunToolbarIconButton("Stop", stopAction, GetRunToolbarIconPath(1), "S")) { + Actions::RequestStopPlayMode(*m_context); + } + + ImGui::SameLine(0.0f, kRunToolbarButtonSpacing); + if (DrawRunToolbarIconButton("Step", stepAction, GetRunToolbarIconPath(2), ">")) { + Actions::RequestStepPlayMode(*m_context); + } + + UI::DrawCurrentWindowBottomBorder(); + } + + ImGui::End(); +} + } } diff --git a/editor/src/panels/MenuBar.h b/editor/src/panels/MenuBar.h index d8acc8e0..a41e84f4 100644 --- a/editor/src/panels/MenuBar.h +++ b/editor/src/panels/MenuBar.h @@ -12,6 +12,8 @@ public: void Render() override; private: + void RenderRunToolbar(); + UI::DeferredPopupState m_aboutPopup; };