feat: add editor project switching workflow

This commit is contained in:
2026-03-28 16:19:15 +08:00
parent 359fe2adb3
commit 1fa97dc246
18 changed files with 983 additions and 101 deletions

View File

@@ -0,0 +1,78 @@
# Editor模块 CMake直链Release版XCEngine库破坏配置一致性
## 1. 问题定义
当前 editor 可执行目标的链接方式仍然是硬编码文件路径:
- [`editor/CMakeLists.txt`](D:\Xuanchi\Main\XCEngine\editor\CMakeLists.txt)
当前写法:
- `target_link_libraries(... ${CMAKE_CURRENT_SOURCE_DIR}/../build/engine/Release/XCEngine.lib)`
这不是正常的 CMake target 依赖,而是直接绑死到一个磁盘上的 `Release` 静态库文件。
---
## 2. 当前影响
这会导致几个非常实际的问题:
1. Debug editor 也可能链接到旧的 Release 版引擎库
2. editor 对 `XCEngine` 目标没有真实依赖关系
3. 引擎库变更后editor 可能不会按正确配置自动重建
4. Debug / Release 混链风险被隐藏
当前本地状态就能看到:
- `build/engine/Debug/XCEngine.lib`
- `build/engine/Release/XCEngine.lib`
两者时间戳和体积明显不同,但 editor 目标仍然硬连 Release 文件。
---
## 3. 为什么这是重大缺陷
这条会直接污染后续 Viewport 对接的开发过程:
- 你以为 editor 正在吃最新的 Renderer / RHI 改动
- 实际上它可能仍在链接旧的 Release 库
这样一来:
- viewport 接入问题很难调
- Debug 行为和 Release 行为可能不一致
- CI / 新机器 / 干净构建环境也更容易炸
它不是“代码风格问题”,而是构建依赖关系本身不正确。
---
## 4. 建议方案
应改成标准 CMake target 依赖:
1. editor 直接链接 `XCEngine`
2. 不再手写 `../build/engine/Release/XCEngine.lib`
3. 由 CMake 自己处理 Debug / Release / RelWithDebInfo 的库选择
4. editor/include 路径和 link 关系都从 target 传播,而不是继续手写 build 输出路径
---
## 5. 验收标准
完成后至少应满足:
1. `editor` 目标通过 `target_link_libraries(... XCEngine)` 链接引擎
2. Debug editor 自动吃 Debug `XCEngine`
3. Release editor 自动吃 Release `XCEngine`
4. 修改 engine 后重新构建 editor 时,依赖关系正确生效
5. 干净构建环境不依赖预先存在的某个磁盘库文件
---
## 6. 优先级
中高。
它未必第一时间阻塞面板 UI 开发,但会显著污染后续所有 viewport / renderer 联调结果,建议尽快修。

View File

@@ -0,0 +1,94 @@
# Editor模块 宿主渲染与EngineRHI未统一导致Viewport纹理无法接入
## 1. 问题定义
当前 editor 的窗口宿主渲染仍然是一套独立的原生 D3D12 路径:
- [`editor/src/Application.h`](D:\Xuanchi\Main\XCEngine\editor\src\Application.h)
- [`editor/src/Application.cpp`](D:\Xuanchi\Main\XCEngine\editor\src\Application.cpp)
- [`editor/src/Platform/D3D12WindowRenderer.h`](D:\Xuanchi\Main\XCEngine\editor\src\Platform\D3D12WindowRenderer.h)
这套路径只负责:
- 创建 Win32 窗口交换链
- 创建独立的 `ID3D12Device / ID3D12CommandQueue`
- 渲染 ImGui 主界面
而当前引擎的 RHI / Renderer 则是另一套独立设备与上下文体系。
这意味着后续即使 Renderer 能把 `SceneView/GameView` 渲染到离屏目标editor 侧也没有稳定的统一设备桥接层把那张纹理安全地贴到 ImGui。
---
## 2. 当前现状
当前 `Application::InitializeWindowRenderer()` 直接创建原生 D3D12 宿主:
- editor 只知道 `Platform::D3D12WindowRenderer`
- editor 并不持有 `RHIDevice / RenderContext / RenderSurface`
- `SceneViewPanel / GameViewPanel` 也没有可复用的 viewport host 对象
结果是:
- editor 主界面渲染和引擎 Renderer 渲染仍然分裂
- 未来 viewport 要么走不通
- 要么被迫写一层高风险的 D3D12 私有纹理互拷/句柄桥接
- 要么反向污染 Renderer使其去适配 editor 私有宿主
---
## 3. 为什么这是重大缺陷
这不是“面板里还没把图显示出来”的小缺口,而是 viewport 接入的根部边界问题。
如果不先统一宿主层,后面很容易走成错误路线:
- Editor 继续维护一套私有 D3D12 设备
- Renderer 再维护一套引擎 RHI 设备
- `SceneView``GameView` 为了显示纹理被迫做后端专用互操作
- Vulkan / OpenGL 路径在 editor 中天然失去接入可能
这会直接破坏:
- RHI 抽象边界
- Renderer 的后端无关性
- 后续 editor viewport 与 runtime 共用同一渲染链路的目标
---
## 4. 建议方案
正确方向应该是:
1. editor 宿主层只保留“窗口 + ImGui 宿主”职责
2. viewport 输出统一来自引擎 Renderer 的 `RenderSurface`
3. editor 增加专门的 viewport bridge / host 层,而不是把渲染实现塞进 panel
4. 该 bridge 层需要明确处理:
- 使用哪个 RHI backend
- 如何创建离屏 color/depth 目标
- 如何把离屏结果暴露为 ImGui 可显示纹理
- resize 生命周期
- device/context 所有权
建议不要继续扩张 `D3D12WindowRenderer` 的职责。
它可以继续作为 editor 主窗口 UI 宿主,但不应成为 viewport 真实渲染实现本体。
---
## 5. 验收标准
完成后至少应满足:
1. editor 可以不依赖私有 D3D12 纹理路径来显示 viewport
2. `SceneView``GameView` 都走统一的 viewport host 接口
3. viewport 输出来自引擎 `RenderSurface`
4. editor 不需要因为切换 OpenGL / D3D12 / Vulkan 而重写面板逻辑
5. Renderer 不需要反向依赖 editor 平台实现
---
## 6. 优先级
高。
在开始正式做 Scene/Game Viewport 之前必须先收敛这个边界,否则后面的接入实现会天然带着架构债。

View File

@@ -0,0 +1,87 @@
# Editor模块 项目根路径仍绑定可执行目录阻塞真实场景与资源加载
## 1. 问题定义
当前 editor 初始化 `EditorContext` 时,把 project path 直接设成了可执行文件目录:
- [`editor/src/Application.cpp`](D:\Xuanchi\Main\XCEngine\editor\src\Application.cpp)
具体行为是:
- 通过 `GetExecutableDirectoryUtf8()` 拿 exe 目录
- 用这个目录初始化 `EditorContext`
- `ProjectPanel``SceneManager::LoadStartupScene()` 都基于这个路径工作
这意味着 editor 当前没有“真实工程根目录”的概念。
---
## 2. 当前影响
基于当前实现:
- `ProjectManager::Initialize()` 会把 `<projectPath>/Assets` 当作项目资源根
- [`editor/src/Managers/ProjectManager.cpp`](D:\Xuanchi\Main\XCEngine\editor\src\Managers\ProjectManager.cpp)
- [`editor/src/Core/EditorWorkspace.h`](D:\Xuanchi\Main\XCEngine\editor\src\Core\EditorWorkspace.h)
`projectPath` 现在是 `editor/bin/...`
结果就是:
- Project 面板看到的是 exe 目录下的 `Assets`
- Startup Scene 也是从 exe 目录下找 `Assets/Scenes/Main.xc`
- viewport 后面如果要加载真实 scene / material / texture / mesh也会默认走错根目录
对“真正接引擎工程内容”来说,这是实打实的阻塞项。
---
## 3. 为什么这是重大缺陷
Scene/Game Viewport 一旦接 Renderer就不再只是“显示一个空测试图”。
它需要基于当前工程:
- 加载场景
- 找到 mesh / material / texture / shader 资产
- 正确解析 `Assets/...` 相对路径
如果 editor 的项目根仍然绑定在 exe 目录:
- 资源加载结果会和实际工程目录脱钩
- Editor 和运行时看到的资产树不是同一棵
- 后续 Project 面板、Scene 保存、Viewport 渲染都会形成伪项目环境
---
## 4. 建议方案
建议尽快引入明确的工程根路径入口,而不是继续默认 exe 目录:
1. `Application` 启动时明确解析 editor project root
2. 最少先支持:
- 命令行传入工程根
- 或固定读取 workspace/project 配置
3. `EditorContext / ProjectManager / SceneManager` 统一只认这一份 project root
4. `ProjectPanel`、startup scene、future viewport asset loading 全部复用同一来源
在没有真实 project root 之前,不建议开始做 viewport 里的正式资源接入。
---
## 5. 验收标准
完成后至少应满足:
1. editor 能明确知道当前工程根目录,而不是推断 exe 目录
2. Project 面板显示的是工程真实 `Assets`
3. Startup Scene 从真实工程根加载
4. Scene/Game Viewport 后续使用的资源路径与 Project 面板一致
5. Debug / Release / editor/bin 变化不会改变 editor 看到的项目内容
---
## 6. 优先级
高。
这条不解决Viewport 接入后只会跑在一个“伪项目目录”里,后面越做越难回收。

View File

@@ -55,19 +55,24 @@ target_include_directories(${PROJECT_NAME} PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
${imgui_SOURCE_DIR}
${imgui_SOURCE_DIR}/backends
${CMAKE_CURRENT_SOURCE_DIR}/../engine/include
${CMAKE_CURRENT_SOURCE_DIR}/../engine/third_party/stb
${CMAKE_CURRENT_SOURCE_DIR}/../tests/OpenGL/package/glm
)
target_compile_definitions(${PROJECT_NAME} PRIVATE UNICODE _UNICODE)
target_compile_options(${PROJECT_NAME} PRIVATE /utf-8 /MD)
target_compile_options(${PROJECT_NAME} PRIVATE /utf-8)
if(MSVC)
set_property(TARGET ${PROJECT_NAME} PROPERTY
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
endif()
target_link_libraries(${PROJECT_NAME} PRIVATE
XCEngine
d3d12.lib
dxgi.lib
d3dcompiler.lib
${CMAKE_CURRENT_SOURCE_DIR}/../build/engine/Release/XCEngine.lib
Ole32.lib
Shell32.lib
Uuid.lib
)
set_target_properties(${PROJECT_NAME} PROPERTIES

View File

@@ -16,6 +16,18 @@ namespace XCEngine {
namespace Editor {
namespace Actions {
inline ActionBinding MakeNewProjectAction() {
return MakeAction("New Project...");
}
inline ActionBinding MakeOpenProjectAction() {
return MakeAction("Open Project...");
}
inline ActionBinding MakeSaveProjectAction() {
return MakeAction("Save Project");
}
inline ActionBinding MakeNewSceneAction() {
return MakeAction("New Scene", "Ctrl+N", false, true, true, Shortcut(ImGuiKey_N, true));
}

View File

@@ -2,6 +2,7 @@
#include "EditActionRouter.h"
#include "EditorActions.h"
#include "Commands/ProjectCommands.h"
#include "Commands/SceneCommands.h"
#include "Core/EditorEvents.h"
#include "Core/EventBus.h"
@@ -17,6 +18,18 @@ inline void ExecuteNewScene(IEditorContext& context) {
Commands::NewScene(context);
}
inline void ExecuteNewProject(IEditorContext& context) {
Commands::NewProjectWithDialog(context);
}
inline void ExecuteOpenProject(IEditorContext& context) {
Commands::OpenProjectWithDialog(context);
}
inline void ExecuteSaveProject(IEditorContext& context) {
Commands::SaveProject(context);
}
inline void ExecuteOpenScene(IEditorContext& context) {
Commands::OpenSceneWithDialog(context);
}
@@ -60,6 +73,10 @@ inline void HandleMainMenuShortcuts(IEditorContext& context, const ShortcutConte
}
inline void DrawFileMenuActions(IEditorContext& context) {
DrawMenuAction(MakeNewProjectAction(), [&]() { ExecuteNewProject(context); });
DrawMenuAction(MakeOpenProjectAction(), [&]() { ExecuteOpenProject(context); });
DrawMenuAction(MakeSaveProjectAction(), [&]() { ExecuteSaveProject(context); });
DrawMenuSeparator();
DrawMenuAction(MakeNewSceneAction(), [&]() { ExecuteNewScene(context); });
DrawMenuAction(MakeOpenSceneAction(), [&]() { ExecuteOpenScene(context); });
DrawMenuAction(MakeSaveSceneAction(), [&]() { ExecuteSaveScene(context); });

View File

@@ -1,5 +1,6 @@
#include "Application.h"
#include "Core/EditorLoggingSetup.h"
#include "Core/ProjectRootResolver.h"
#include "Core/EditorWindowTitle.h"
#include "Layers/EditorLayer.h"
#include "Core/EditorContext.h"
@@ -8,6 +9,7 @@
#include "UI/BuiltInIcons.h"
#include "Platform/Win32Utf8.h"
#include "Platform/WindowsProcessDiagnostics.h"
#include <XCEngine/Debug/Logger.h>
#include <windows.h>
namespace XCEngine {
@@ -105,13 +107,25 @@ bool Application::Initialize(HWND hwnd) {
const std::string exeDir = Platform::GetExecutableDirectoryUtf8();
ConfigureEditorLogging(exeDir);
const std::string projectRoot = ResolveEditorProjectRootUtf8();
const bool workingDirectoryChanged = SetEditorWorkingDirectory(projectRoot);
auto& logger = Debug::Logger::Get();
const std::string projectRootMessage = "Editor project root: " + projectRoot;
logger.Info(Debug::LogCategory::General, projectRootMessage.c_str());
if (!workingDirectoryChanged) {
const std::string warningMessage = "Failed to switch editor working directory to project root: " + projectRoot;
logger.Warning(Debug::LogCategory::General, warningMessage.c_str());
}
m_hwnd = hwnd;
if (!InitializeWindowRenderer(hwnd)) {
return false;
}
InitializeEditorContext(exeDir);
InitializeEditorContext(projectRoot);
InitializeImGui(hwnd);
AttachEditorLayer();
m_renderReady = true;
@@ -151,5 +165,31 @@ void Application::OnResize(int width, int height) {
m_windowRenderer.Resize(width, height);
}
bool Application::SwitchProject(const std::string& projectPath) {
if (!m_editorContext || projectPath.empty()) {
return false;
}
m_imguiSession.SetProjectPath(projectPath);
m_editorContext->SetProjectPath(projectPath);
auto& logger = Debug::Logger::Get();
if (!SetEditorWorkingDirectory(projectPath)) {
const std::string warningMessage = "Failed to switch editor working directory to project root: " + projectPath;
logger.Warning(Debug::LogCategory::General, warningMessage.c_str());
}
const std::string infoMessage = "Switched editor project root: " + projectPath;
logger.Info(Debug::LogCategory::General, infoMessage.c_str());
m_lastWindowTitle.clear();
UpdateWindowTitle();
return true;
}
void Application::SaveProjectState() {
m_imguiSession.SaveSettings();
}
}
}

View File

@@ -24,6 +24,8 @@ public:
void Shutdown();
void Render();
void OnResize(int width, int height);
bool SwitchProject(const std::string& projectPath);
void SaveProjectState();
bool IsRenderReady() const { return m_renderReady; }
HWND GetWindowHandle() const { return m_hwnd; }

View File

@@ -1,10 +1,17 @@
#pragma once
#include "Application.h"
#include "Core/AssetItem.h"
#include "Core/IEditorContext.h"
#include "Core/IProjectManager.h"
#include "Core/ISelectionManager.h"
#include "Core/ISceneManager.h"
#include "Core/IUndoManager.h"
#include "SceneCommands.h"
#include "Utils/FileDialogUtils.h"
#include "Utils/ProjectFileUtils.h"
#include <filesystem>
#include <string>
namespace XCEngine {
@@ -68,6 +75,115 @@ inline bool MoveAssetToFolder(
return projectManager.MoveItem(sourceFullPath, targetFolder->fullPath);
}
inline std::string BuildProjectFallbackScenePath(const std::string& projectPath) {
return (std::filesystem::path(projectPath) / "Assets" / "Scenes" / "Main.xc").string();
}
inline bool EnsureProjectStructure(const std::string& projectPath) {
if (projectPath.empty()) {
return false;
}
namespace fs = std::filesystem;
std::error_code ec;
fs::create_directories(fs::path(projectPath) / "Assets" / "Scenes", ec);
ec.clear();
fs::create_directories(fs::path(projectPath) / ".xceditor", ec);
return true;
}
inline ProjectFileUtils::ProjectDescriptor BuildProjectDescriptor(IEditorContext& context) {
auto& sceneManager = context.GetSceneManager();
ProjectFileUtils::ProjectDescriptor descriptor;
descriptor.name = ProjectFileUtils::GetProjectName(context.GetProjectPath());
const std::string currentScenePath = sceneManager.GetCurrentScenePath();
if (!currentScenePath.empty()) {
descriptor.startupScene = ProjectFileUtils::MakeProjectRelativePath(
context.GetProjectPath(),
currentScenePath);
}
if (descriptor.startupScene.empty()) {
descriptor.startupScene = "Assets/Scenes/Main.xc";
}
return descriptor;
}
inline bool SaveProjectDescriptor(IEditorContext& context) {
return ProjectFileUtils::SaveProjectDescriptor(
context.GetProjectPath(),
BuildProjectDescriptor(context));
}
inline bool SaveProject(IEditorContext& context) {
if (!EnsureProjectStructure(context.GetProjectPath())) {
return false;
}
if (!SaveDirtySceneWithFallback(context, BuildProjectFallbackScenePath(context.GetProjectPath()))) {
return false;
}
context.GetProjectManager().RefreshCurrentFolder();
Application::Get().SaveProjectState();
return SaveProjectDescriptor(context);
}
inline bool SwitchProject(IEditorContext& context, const std::string& projectPath) {
if (projectPath.empty()) {
return false;
}
if (!SceneEditorUtils::ConfirmSceneSwitch(context)) {
return false;
}
if (!EnsureProjectStructure(projectPath)) {
return false;
}
if (!Application::Get().SwitchProject(projectPath)) {
return false;
}
context.SetProjectPath(projectPath);
context.GetProjectManager().Initialize(projectPath);
const bool loaded = context.GetSceneManager().LoadStartupScene(projectPath);
context.GetSelectionManager().ClearSelection();
context.GetUndoManager().ClearHistory();
context.GetProjectManager().RefreshCurrentFolder();
if (loaded) {
SaveProjectDescriptor(context);
}
return loaded;
}
inline bool NewProjectWithDialog(IEditorContext& context) {
const std::string projectPath = FileDialogUtils::PickFolderDialog(
L"Select New Project Folder",
context.GetProjectPath());
if (projectPath.empty()) {
return false;
}
return SwitchProject(context, projectPath);
}
inline bool OpenProjectWithDialog(IEditorContext& context) {
const std::string projectPath = FileDialogUtils::PickFolderDialog(
L"Open Project Folder",
context.GetProjectPath());
if (projectPath.empty()) {
return false;
}
return SwitchProject(context, projectPath);
}
} // namespace Commands
} // namespace Editor
} // namespace XCEngine

View File

@@ -0,0 +1,165 @@
#pragma once
#include "Platform/Win32Utf8.h"
#include <shellapi.h>
#include <filesystem>
#include <optional>
#include <string>
#include <system_error>
namespace XCEngine {
namespace Editor {
namespace fs = std::filesystem;
namespace detail {
inline fs::path NormalizePath(const fs::path& path, const fs::path& basePath = {}) {
if (path.empty()) {
return {};
}
fs::path absolutePath = path;
std::error_code ec;
if (absolutePath.is_relative()) {
absolutePath = basePath.empty()
? fs::absolute(absolutePath, ec)
: fs::absolute(basePath / absolutePath, ec);
if (ec) {
absolutePath = basePath.empty() ? absolutePath : (basePath / path);
}
}
ec.clear();
const fs::path canonicalPath = fs::weakly_canonical(absolutePath, ec);
if (!ec) {
return canonicalPath.lexically_normal();
}
return absolutePath.lexically_normal();
}
inline bool IsWorkspaceRoot(const fs::path& candidate) {
std::error_code ec;
return fs::exists(candidate / L"CMakeLists.txt", ec) &&
fs::is_directory(candidate / L"editor", ec) &&
fs::is_directory(candidate / L"engine", ec);
}
inline bool IsEditorProjectRoot(const fs::path& candidate) {
std::error_code ec;
return fs::exists(candidate / L"Project.xcproject", ec) ||
fs::is_directory(candidate / L"Assets", ec);
}
inline fs::path ResolveWorkspaceDefaultProjectRoot(const fs::path& workspaceRoot) {
if (workspaceRoot.empty()) {
return {};
}
const fs::path preferredProjectRoot = workspaceRoot / L"project";
if (IsEditorProjectRoot(preferredProjectRoot)) {
return NormalizePath(preferredProjectRoot);
}
if (IsEditorProjectRoot(workspaceRoot)) {
return NormalizePath(workspaceRoot);
}
return NormalizePath(preferredProjectRoot);
}
inline std::optional<fs::path> FindWorkspaceRoot(fs::path start) {
if (start.empty()) {
return std::nullopt;
}
start = NormalizePath(start);
for (fs::path current = start; !current.empty(); ) {
if (IsWorkspaceRoot(current)) {
return current;
}
const fs::path parent = current.parent_path();
if (parent == current) {
break;
}
current = parent;
}
return std::nullopt;
}
inline std::optional<fs::path> ParseCommandLineProjectOverride(const fs::path& workingDirectory) {
int argc = 0;
LPWSTR* argv = CommandLineToArgvW(GetCommandLineW(), &argc);
if (!argv || argc <= 1) {
if (argv) {
LocalFree(argv);
}
return std::nullopt;
}
std::optional<fs::path> projectOverride;
for (int i = 1; i < argc; ++i) {
const std::wstring arg = argv[i] ? argv[i] : L"";
if ((arg == L"--project" || arg == L"-p") && i + 1 < argc) {
projectOverride = NormalizePath(fs::path(argv[++i]), workingDirectory);
break;
}
static const std::wstring kProjectArgPrefix = L"--project=";
if (arg.rfind(kProjectArgPrefix, 0) == 0) {
projectOverride = NormalizePath(fs::path(arg.substr(kProjectArgPrefix.size())), workingDirectory);
break;
}
}
LocalFree(argv);
return projectOverride;
}
inline std::string PathToUtf8(const fs::path& path) {
return Platform::WideToUtf8(path.wstring());
}
} // namespace detail
inline std::string ResolveEditorProjectRootUtf8() {
std::error_code ec;
const fs::path workingDirectory = detail::NormalizePath(fs::current_path(ec));
const fs::path executableDirectory = detail::NormalizePath(fs::path(Platform::Utf8ToWide(Platform::GetExecutableDirectoryUtf8())));
if (const auto projectOverride = detail::ParseCommandLineProjectOverride(workingDirectory.empty() ? executableDirectory : workingDirectory)) {
return detail::PathToUtf8(*projectOverride);
}
if (const auto workspaceRoot = detail::FindWorkspaceRoot(workingDirectory)) {
return detail::PathToUtf8(detail::ResolveWorkspaceDefaultProjectRoot(*workspaceRoot));
}
if (const auto workspaceRoot = detail::FindWorkspaceRoot(executableDirectory)) {
return detail::PathToUtf8(detail::ResolveWorkspaceDefaultProjectRoot(*workspaceRoot));
}
if (!workingDirectory.empty()) {
return detail::PathToUtf8(workingDirectory);
}
return detail::PathToUtf8(executableDirectory);
}
inline bool SetEditorWorkingDirectory(const std::string& projectRootUtf8) {
if (projectRootUtf8.empty()) {
return false;
}
std::error_code ec;
fs::current_path(fs::path(Platform::Utf8ToWide(projectRootUtf8)), ec);
return !ec;
}
} // namespace Editor
} // namespace XCEngine

View File

@@ -1,6 +1,7 @@
#include "SceneManager.h"
#include "Core/EventBus.h"
#include "Core/EditorEvents.h"
#include "Utils/ProjectFileUtils.h"
#include <XCEngine/Components/ComponentFactoryRegistry.h>
#include <XCEngine/Components/CameraComponent.h>
#include <XCEngine/Components/LightComponent.h>
@@ -387,6 +388,14 @@ std::string SceneManager::GetScenesDirectory(const std::string& projectPath) {
std::string SceneManager::ResolveDefaultScenePath(const std::string& projectPath) {
namespace fs = std::filesystem;
if (const auto descriptor = ProjectFileUtils::LoadProjectDescriptor(projectPath)) {
const std::string configuredScenePath =
ProjectFileUtils::ResolveProjectPath(projectPath, descriptor->startupScene);
if (IsSceneFileUsable(configuredScenePath)) {
return configuredScenePath;
}
}
const fs::path scenesDir = GetScenesDirectory(projectPath);
const fs::path mainXC = scenesDir / "Main.xc";
if (fs::exists(mainXC)) {

View File

@@ -48,6 +48,17 @@ public:
return m_iniPath;
}
void SetProjectPath(const std::string& projectPath) {
if (ImGui::GetCurrentContext() == nullptr) {
return;
}
SaveSettings();
ImGuiIO& io = ImGui::GetIO();
ConfigureIniFile(projectPath, io);
ImGui::LoadIniSettingsFromDisk(m_iniPath.c_str());
}
private:
static constexpr float kUiFontSize = 18.0f;
static constexpr const char* kPrimaryUiFontPath = "C:/Windows/Fonts/segoeui.ttf";

View File

@@ -86,6 +86,30 @@ inline void DrawComboPreviewFrame(
textColor);
}
inline void DrawControlFrameChrome(
ImDrawList* drawList,
const ImVec2& min,
const ImVec2& max,
bool hovered,
bool active = false) {
if (!drawList) {
return;
}
const ImGuiStyle& style = ImGui::GetStyle();
const ImU32 frameColor = ImGui::GetColorU32(
active ? ImGuiCol_FrameBgActive : (hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg));
const ImU32 borderColor = ImGui::GetColorU32(ImGuiCol_Border);
drawList->AddRectFilled(min, max, frameColor, style.FrameRounding);
drawList->AddRect(
ImVec2(min.x + 0.5f, min.y + 0.5f),
ImVec2(max.x - 0.5f, max.y - 0.5f),
borderColor,
style.FrameRounding,
0,
style.FrameBorderSize);
}
inline bool DrawFloat(
const char* label,
float& value,
@@ -113,7 +137,8 @@ inline bool DrawLinearSlider(
const bool active = ImGui::IsItemActive();
const ImVec2 min = ImGui::GetItemRectMin();
const ImVec2 max = ImGui::GetItemRectMax();
const float trackPadding = LinearSliderHorizontalPadding();
const ImGuiStyle& style = ImGui::GetStyle();
const float trackPadding = (std::max)(LinearSliderHorizontalPadding(), style.FramePadding.x);
const float trackMinX = min.x + trackPadding;
const float trackMaxX = max.x - trackPadding;
const float centerY = (min.y + max.y) * 0.5f;
@@ -121,6 +146,7 @@ inline bool DrawLinearSlider(
const float trackHalfThickness = LinearSliderTrackThickness() * 0.5f;
ImDrawList* drawList = ImGui::GetWindowDrawList();
DrawControlFrameChrome(drawList, min, max, hovered, active);
drawList->AddRectFilled(
ImVec2(trackMinX, centerY - trackHalfThickness),
ImVec2(trackMaxX, centerY + trackHalfThickness),

View File

@@ -2,6 +2,8 @@
#include "StyleTokens.h"
#include <algorithm>
#include <cmath>
#include <imgui.h>
namespace XCEngine {
@@ -51,14 +53,22 @@ inline SplitterResult DrawSplitter(
const ImVec4 color = active
? PanelSplitterActiveColor()
: (hovered ? PanelSplitterHoveredColor() : PanelSplitterIdleColor());
const float thickness = PanelSplitterVisibleThickness();
const float thickness = hitThickness > 0.0f
? (std::min)( (std::max)(PanelSplitterVisibleThickness(), 1.0f), hitThickness)
: (std::max)(PanelSplitterVisibleThickness(), 1.0f);
ImDrawList* drawList = ImGui::GetWindowDrawList();
if (vertical) {
const float centerX = min.x + (size.x * 0.5f);
drawList->AddLine(ImVec2(centerX, min.y), ImVec2(centerX, min.y + size.y), ImGui::GetColorU32(color), thickness);
const float visualMinX = std::floor(min.x + (size.x - thickness) * 0.5f);
drawList->AddRectFilled(
ImVec2(visualMinX, min.y),
ImVec2(visualMinX + thickness, min.y + size.y),
ImGui::GetColorU32(color));
} else {
const float centerY = min.y + (size.y * 0.5f);
drawList->AddLine(ImVec2(min.x, centerY), ImVec2(min.x + size.x, centerY), ImGui::GetColorU32(color), thickness);
const float visualMinY = std::floor(min.y + (size.y - thickness) * 0.5f);
drawList->AddRectFilled(
ImVec2(min.x, visualMinY),
ImVec2(min.x + size.x, visualMinY + thickness),
ImGui::GetColorU32(color));
}
return SplitterResult{ hovered, active, delta };

View File

@@ -199,7 +199,7 @@ inline float PanelSplitterHitThickness() {
}
inline float PanelSplitterVisibleThickness() {
return 1.0f;
return 2.0f;
}
inline ImVec4 PanelSplitterIdleColor() {
@@ -235,7 +235,7 @@ inline float CompactNavigationTreeIndentSpacing() {
}
inline float NavigationTreeIconSize() {
return 17.0f;
return 18.0f;
}
inline float NavigationTreePrefixWidth() {

View File

@@ -0,0 +1,167 @@
#pragma once
#include "Platform/Win32Utf8.h"
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#include <commdlg.h>
#include <shobjidl.h>
#include <wrl/client.h>
#include <array>
#include <string>
namespace XCEngine {
namespace Editor {
namespace FileDialogUtils {
inline HWND GetDialogOwnerWindow() {
HWND owner = GetActiveWindow();
if (!owner) {
owner = GetForegroundWindow();
}
return owner;
}
inline std::string OpenFileDialog(
const wchar_t* filter,
const std::string& initialDirectory = {},
const std::string& initialPath = {},
DWORD flags = OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR) {
std::array<wchar_t, 1024> fileBuffer{};
if (!initialPath.empty()) {
const std::wstring initialFile = Platform::Utf8ToWide(initialPath);
wcsncpy_s(fileBuffer.data(), fileBuffer.size(), initialFile.c_str(), _TRUNCATE);
}
const std::wstring initialDir = Platform::Utf8ToWide(initialDirectory);
OPENFILENAMEW dialog{};
dialog.lStructSize = sizeof(dialog);
dialog.hwndOwner = GetDialogOwnerWindow();
dialog.lpstrFilter = filter;
dialog.lpstrFile = fileBuffer.data();
dialog.nMaxFile = static_cast<DWORD>(fileBuffer.size());
dialog.lpstrInitialDir = initialDir.empty() ? nullptr : initialDir.c_str();
dialog.Flags = flags;
if (!GetOpenFileNameW(&dialog)) {
return {};
}
return Platform::WideToUtf8(fileBuffer.data());
}
inline std::string SaveFileDialog(
const wchar_t* filter,
const std::string& initialDirectory = {},
const std::string& suggestedPath = {},
const wchar_t* defaultExtension = nullptr,
DWORD flags = OFN_OVERWRITEPROMPT | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR) {
std::array<wchar_t, 1024> fileBuffer{};
if (!suggestedPath.empty()) {
const std::wstring suggestedWide = Platform::Utf8ToWide(suggestedPath);
wcsncpy_s(fileBuffer.data(), fileBuffer.size(), suggestedWide.c_str(), _TRUNCATE);
}
const std::wstring initialDir = Platform::Utf8ToWide(initialDirectory);
OPENFILENAMEW dialog{};
dialog.lStructSize = sizeof(dialog);
dialog.hwndOwner = GetDialogOwnerWindow();
dialog.lpstrFilter = filter;
dialog.lpstrFile = fileBuffer.data();
dialog.nMaxFile = static_cast<DWORD>(fileBuffer.size());
dialog.lpstrInitialDir = initialDir.empty() ? nullptr : initialDir.c_str();
dialog.lpstrDefExt = defaultExtension;
dialog.Flags = flags;
if (!GetSaveFileNameW(&dialog)) {
return {};
}
return Platform::WideToUtf8(fileBuffer.data());
}
inline std::string PickFolderDialog(const wchar_t* title, const std::string& initialDirectory = {}) {
const HRESULT initHr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
if (FAILED(initHr) && initHr != RPC_E_CHANGED_MODE) {
return {};
}
const bool shouldUninitialize = SUCCEEDED(initHr);
Microsoft::WRL::ComPtr<IFileDialog> dialog;
HRESULT hr = CoCreateInstance(CLSID_FileOpenDialog, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&dialog));
if (FAILED(hr)) {
if (shouldUninitialize) {
CoUninitialize();
}
return {};
}
DWORD options = 0;
dialog->GetOptions(&options);
dialog->SetOptions(options | FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM | FOS_PATHMUSTEXIST);
if (title && title[0] != L'\0') {
dialog->SetTitle(title);
}
if (!initialDirectory.empty()) {
Microsoft::WRL::ComPtr<IShellItem> initialFolder;
hr = SHCreateItemFromParsingName(
Platform::Utf8ToWide(initialDirectory).c_str(),
nullptr,
IID_PPV_ARGS(&initialFolder));
if (SUCCEEDED(hr) && initialFolder) {
dialog->SetFolder(initialFolder.Get());
dialog->SetDefaultFolder(initialFolder.Get());
}
}
hr = dialog->Show(GetDialogOwnerWindow());
if (hr == HRESULT_FROM_WIN32(ERROR_CANCELLED)) {
if (shouldUninitialize) {
CoUninitialize();
}
return {};
}
if (FAILED(hr)) {
if (shouldUninitialize) {
CoUninitialize();
}
return {};
}
Microsoft::WRL::ComPtr<IShellItem> result;
hr = dialog->GetResult(&result);
if (FAILED(hr) || !result) {
if (shouldUninitialize) {
CoUninitialize();
}
return {};
}
PWSTR selectedPath = nullptr;
hr = result->GetDisplayName(SIGDN_FILESYSPATH, &selectedPath);
if (FAILED(hr) || !selectedPath) {
if (shouldUninitialize) {
CoUninitialize();
}
return {};
}
const std::string folderPath = Platform::WideToUtf8(selectedPath);
CoTaskMemFree(selectedPath);
if (shouldUninitialize) {
CoUninitialize();
}
return folderPath;
}
} // namespace FileDialogUtils
} // namespace Editor
} // namespace XCEngine

View File

@@ -0,0 +1,119 @@
#pragma once
#include <filesystem>
#include <fstream>
#include <optional>
#include <string>
namespace XCEngine {
namespace Editor {
namespace ProjectFileUtils {
namespace fs = std::filesystem;
struct ProjectDescriptor {
std::string name;
std::string startupScene;
};
inline fs::path GetProjectFilePath(const std::string& projectRoot) {
return fs::path(projectRoot) / "Project.xcproject";
}
inline std::string GetProjectName(const std::string& projectRoot) {
const fs::path rootPath(projectRoot);
const fs::path folderName = rootPath.filename().empty() ? rootPath.parent_path().filename() : rootPath.filename();
return folderName.empty() ? "XCEngineProject" : folderName.string();
}
inline std::string Trim(const std::string& value) {
const size_t begin = value.find_first_not_of(" \t\r\n");
if (begin == std::string::npos) {
return {};
}
const size_t end = value.find_last_not_of(" \t\r\n");
return value.substr(begin, end - begin + 1);
}
inline bool SaveProjectDescriptor(const std::string& projectRoot, const ProjectDescriptor& descriptor) {
std::error_code ec;
fs::create_directories(fs::path(projectRoot), ec);
std::ofstream output(GetProjectFilePath(projectRoot), std::ios::out | std::ios::trunc);
if (!output.is_open()) {
return false;
}
output << "version=1\n";
output << "name=" << descriptor.name << "\n";
output << "startup_scene=" << descriptor.startupScene << "\n";
return output.good();
}
inline std::optional<ProjectDescriptor> LoadProjectDescriptor(const std::string& projectRoot) {
std::ifstream input(GetProjectFilePath(projectRoot));
if (!input.is_open()) {
return std::nullopt;
}
ProjectDescriptor descriptor;
descriptor.name = GetProjectName(projectRoot);
std::string line;
while (std::getline(input, line)) {
const size_t equalsPos = line.find('=');
if (equalsPos == std::string::npos) {
continue;
}
const std::string key = Trim(line.substr(0, equalsPos));
const std::string value = Trim(line.substr(equalsPos + 1));
if (key == "name") {
descriptor.name = value;
} else if (key == "startup_scene") {
descriptor.startupScene = value;
}
}
return descriptor;
}
inline std::string MakeProjectRelativePath(const std::string& projectRoot, const std::string& fullPath) {
if (projectRoot.empty() || fullPath.empty()) {
return {};
}
std::error_code ec;
const fs::path root = fs::weakly_canonical(fs::path(projectRoot), ec);
ec.clear();
const fs::path target = fs::weakly_canonical(fs::path(fullPath), ec);
if (ec) {
return fullPath;
}
ec.clear();
const fs::path relative = fs::relative(target, root, ec);
if (ec) {
return fullPath;
}
return relative.lexically_normal().generic_string();
}
inline std::string ResolveProjectPath(const std::string& projectRoot, const std::string& pathValue) {
if (pathValue.empty()) {
return {};
}
const fs::path path(pathValue);
if (path.is_absolute()) {
return path.lexically_normal().string();
}
return (fs::path(projectRoot) / path).lexically_normal().string();
}
} // namespace ProjectFileUtils
} // namespace Editor
} // namespace XCEngine

View File

@@ -3,15 +3,10 @@
#include "Core/IEditorContext.h"
#include "Core/IProjectManager.h"
#include "Core/ISceneManager.h"
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include "Utils/FileDialogUtils.h"
#include <windows.h>
#include <commdlg.h>
#include <array>
#include <filesystem>
#include <string>
@@ -19,36 +14,6 @@ namespace XCEngine {
namespace Editor {
namespace SceneEditorUtils {
inline std::wstring Utf8ToWide(const std::string& value) {
if (value.empty()) {
return {};
}
const int length = MultiByteToWideChar(CP_UTF8, 0, value.c_str(), -1, nullptr, 0);
if (length <= 0) {
return {};
}
std::wstring result(length - 1, L'\0');
MultiByteToWideChar(CP_UTF8, 0, value.c_str(), -1, &result[0], length);
return result;
}
inline std::string WideToUtf8(const std::wstring& value) {
if (value.empty()) {
return {};
}
const int length = WideCharToMultiByte(CP_UTF8, 0, value.c_str(), -1, nullptr, 0, nullptr, nullptr);
if (length <= 0) {
return {};
}
std::string result(length - 1, '\0');
WideCharToMultiByte(CP_UTF8, 0, value.c_str(), -1, &result[0], length, nullptr, nullptr);
return result;
}
inline std::string SanitizeSceneFileName(const std::string& value) {
std::string result = value.empty() ? "Untitled Scene" : value;
for (char& ch : result) {
@@ -74,14 +39,6 @@ inline std::string SanitizeSceneFileName(const std::string& value) {
return result;
}
inline HWND GetDialogOwnerWindow() {
HWND owner = GetActiveWindow();
if (!owner) {
owner = GetForegroundWindow();
}
return owner;
}
inline const wchar_t* GetSceneFilter() {
static const wchar_t filter[] =
L"XCEngine Scene (*.xc)\0*.xc\0Legacy Scene (*.scene;*.unity)\0*.scene;*.unity\0All Files (*.*)\0*.*\0\0";
@@ -92,28 +49,10 @@ inline std::string OpenSceneFileDialog(const std::string& projectPath, const std
namespace fs = std::filesystem;
const fs::path scenesDir = fs::path(projectPath) / "Assets" / "Scenes";
const std::wstring initialDir = Utf8ToWide(scenesDir.string());
std::array<wchar_t, 1024> fileBuffer{};
if (!initialPath.empty()) {
const std::wstring initialFile = Utf8ToWide(initialPath);
wcsncpy_s(fileBuffer.data(), fileBuffer.size(), initialFile.c_str(), _TRUNCATE);
}
OPENFILENAMEW dialog{};
dialog.lStructSize = sizeof(dialog);
dialog.hwndOwner = GetDialogOwnerWindow();
dialog.lpstrFilter = GetSceneFilter();
dialog.lpstrFile = fileBuffer.data();
dialog.nMaxFile = static_cast<DWORD>(fileBuffer.size());
dialog.lpstrInitialDir = initialDir.empty() ? nullptr : initialDir.c_str();
dialog.Flags = OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR;
if (!GetOpenFileNameW(&dialog)) {
return {};
}
return WideToUtf8(fileBuffer.data());
return FileDialogUtils::OpenFileDialog(
GetSceneFilter(),
scenesDir.string(),
initialPath);
}
inline std::string SaveSceneFileDialog(
@@ -127,26 +66,11 @@ inline std::string SaveSceneFileDialog(
? scenesDir / (SanitizeSceneFileName(currentSceneName) + ".xc")
: fs::path(currentScenePath).replace_extension(".xc");
std::array<wchar_t, 1024> fileBuffer{};
const std::wstring suggestedWide = Utf8ToWide(suggestedPath.string());
wcsncpy_s(fileBuffer.data(), fileBuffer.size(), suggestedWide.c_str(), _TRUNCATE);
const std::wstring initialDir = Utf8ToWide(suggestedPath.parent_path().string());
OPENFILENAMEW dialog{};
dialog.lStructSize = sizeof(dialog);
dialog.hwndOwner = GetDialogOwnerWindow();
dialog.lpstrFilter = GetSceneFilter();
dialog.lpstrFile = fileBuffer.data();
dialog.nMaxFile = static_cast<DWORD>(fileBuffer.size());
dialog.lpstrInitialDir = initialDir.empty() ? nullptr : initialDir.c_str();
dialog.lpstrDefExt = L"xc";
dialog.Flags = OFN_OVERWRITEPROMPT | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR;
if (!GetSaveFileNameW(&dialog)) {
return {};
}
return WideToUtf8(fileBuffer.data());
return FileDialogUtils::SaveFileDialog(
GetSceneFilter(),
suggestedPath.parent_path().string(),
suggestedPath.string(),
L"xc");
}
inline bool SaveCurrentScene(IEditorContext& context) {
@@ -177,7 +101,7 @@ inline bool ConfirmSceneSwitch(IEditorContext& context) {
}
const int result = MessageBoxW(
GetDialogOwnerWindow(),
FileDialogUtils::GetDialogOwnerWindow(),
L"Save changes to the current scene before continuing?",
L"Unsaved Scene Changes",
MB_YESNOCANCEL | MB_ICONWARNING);