feat: add top run toolbar controls

This commit is contained in:
2026-04-02 21:11:08 +08:00
parent 84e1ba3600
commit 8e362fc4c0
6 changed files with 210 additions and 3 deletions

View File

@@ -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"`

View File

@@ -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$<$<CONFIG:Debug>:Debug>DLL")
endif()

View File

@@ -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(

View File

@@ -62,15 +62,31 @@ inline void RequestDockLayoutReset(IEditorContext& context) {
context.GetEventBus().Publish(DockLayoutResetRequestedEvent{});
}
inline void RequestTogglePlayMode(IEditorContext& context) {
if (context.GetRuntimeMode() == EditorRuntimeMode::Edit) {
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{});

View File

@@ -3,11 +3,108 @@
#include "Actions/MainMenuActionRouter.h"
#include "MenuBar.h"
#include "Core/IEditorContext.h"
#include "Platform/Win32Utf8.h"
#include "UI/UI.h"
#include <imgui.h>
#include <imgui_internal.h>
#include <algorithm>
#include <array>
#include <filesystem>
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<const char*, 3> kFileNames = {
"play_button.png",
"stop_button.png",
"step_button.png"
};
static std::array<std::string, kFileNames.size()> 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();
}
}
}

View File

@@ -12,6 +12,8 @@ public:
void Render() override;
private:
void RenderRunToolbar();
UI::DeferredPopupState m_aboutPopup;
};