From 1fa97dc24677f29a0d1d9726550ae6e4883b4a60 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Sat, 28 Mar 2026 16:19:15 +0800 Subject: [PATCH] feat: add editor project switching workflow --- ...直链Release版XCEngine库破坏配置一致性3.28.md | 78 ++++++++ ...与EngineRHI未统一导致Viewport纹理无法接入3.28.md | 94 ++++++++++ ...径仍绑定可执行目录阻塞真实场景与资源加载3.28.md | 87 +++++++++ editor/CMakeLists.txt | 15 +- editor/src/Actions/EditorActions.h | 12 ++ editor/src/Actions/MainMenuActionRouter.h | 17 ++ editor/src/Application.cpp | 42 ++++- editor/src/Application.h | 2 + editor/src/Commands/ProjectCommands.h | 116 ++++++++++++ editor/src/Core/ProjectRootResolver.h | 165 +++++++++++++++++ editor/src/Managers/SceneManager.cpp | 9 + editor/src/UI/ImGuiSession.h | 11 ++ editor/src/UI/ScalarControls.h | 28 ++- editor/src/UI/SplitterChrome.h | 20 ++- editor/src/UI/StyleTokens.h | 4 +- editor/src/Utils/FileDialogUtils.h | 167 ++++++++++++++++++ editor/src/Utils/ProjectFileUtils.h | 119 +++++++++++++ editor/src/Utils/SceneEditorUtils.h | 98 ++-------- 18 files changed, 983 insertions(+), 101 deletions(-) create mode 100644 docs/issues/Editor模块_CMake直链Release版XCEngine库破坏配置一致性3.28.md create mode 100644 docs/issues/Editor模块_宿主渲染与EngineRHI未统一导致Viewport纹理无法接入3.28.md create mode 100644 docs/issues/Editor模块_项目根路径仍绑定可执行目录阻塞真实场景与资源加载3.28.md create mode 100644 editor/src/Core/ProjectRootResolver.h create mode 100644 editor/src/Utils/FileDialogUtils.h create mode 100644 editor/src/Utils/ProjectFileUtils.h diff --git a/docs/issues/Editor模块_CMake直链Release版XCEngine库破坏配置一致性3.28.md b/docs/issues/Editor模块_CMake直链Release版XCEngine库破坏配置一致性3.28.md new file mode 100644 index 00000000..bb2319e2 --- /dev/null +++ b/docs/issues/Editor模块_CMake直链Release版XCEngine库破坏配置一致性3.28.md @@ -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 联调结果,建议尽快修。 diff --git a/docs/issues/Editor模块_宿主渲染与EngineRHI未统一导致Viewport纹理无法接入3.28.md b/docs/issues/Editor模块_宿主渲染与EngineRHI未统一导致Viewport纹理无法接入3.28.md new file mode 100644 index 00000000..b77f68e9 --- /dev/null +++ b/docs/issues/Editor模块_宿主渲染与EngineRHI未统一导致Viewport纹理无法接入3.28.md @@ -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 之前必须先收敛这个边界,否则后面的接入实现会天然带着架构债。 diff --git a/docs/issues/Editor模块_项目根路径仍绑定可执行目录阻塞真实场景与资源加载3.28.md b/docs/issues/Editor模块_项目根路径仍绑定可执行目录阻塞真实场景与资源加载3.28.md new file mode 100644 index 00000000..7dde01f8 --- /dev/null +++ b/docs/issues/Editor模块_项目根路径仍绑定可执行目录阻塞真实场景与资源加载3.28.md @@ -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()` 会把 `/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 接入后只会跑在一个“伪项目目录”里,后面越做越难回收。 diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index d5028f84..b033f31c 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -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$<$: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 diff --git a/editor/src/Actions/EditorActions.h b/editor/src/Actions/EditorActions.h index de23957e..81806f13 100644 --- a/editor/src/Actions/EditorActions.h +++ b/editor/src/Actions/EditorActions.h @@ -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)); } diff --git a/editor/src/Actions/MainMenuActionRouter.h b/editor/src/Actions/MainMenuActionRouter.h index 218b9a11..55e838b6 100644 --- a/editor/src/Actions/MainMenuActionRouter.h +++ b/editor/src/Actions/MainMenuActionRouter.h @@ -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); }); diff --git a/editor/src/Application.cpp b/editor/src/Application.cpp index a601fc8f..91002b11 100644 --- a/editor/src/Application.cpp +++ b/editor/src/Application.cpp @@ -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 #include 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(); +} + } } diff --git a/editor/src/Application.h b/editor/src/Application.h index b80245a3..9e4fcf8d 100644 --- a/editor/src/Application.h +++ b/editor/src/Application.h @@ -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; } diff --git a/editor/src/Commands/ProjectCommands.h b/editor/src/Commands/ProjectCommands.h index 4a9c47bd..13e0dfa0 100644 --- a/editor/src/Commands/ProjectCommands.h +++ b/editor/src/Commands/ProjectCommands.h @@ -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 #include 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 diff --git a/editor/src/Core/ProjectRootResolver.h b/editor/src/Core/ProjectRootResolver.h new file mode 100644 index 00000000..511449eb --- /dev/null +++ b/editor/src/Core/ProjectRootResolver.h @@ -0,0 +1,165 @@ +#pragma once + +#include "Platform/Win32Utf8.h" + +#include + +#include +#include +#include +#include + +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 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 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 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 diff --git a/editor/src/Managers/SceneManager.cpp b/editor/src/Managers/SceneManager.cpp index e6c70ca5..d2e2c3e4 100644 --- a/editor/src/Managers/SceneManager.cpp +++ b/editor/src/Managers/SceneManager.cpp @@ -1,6 +1,7 @@ #include "SceneManager.h" #include "Core/EventBus.h" #include "Core/EditorEvents.h" +#include "Utils/ProjectFileUtils.h" #include #include #include @@ -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)) { diff --git a/editor/src/UI/ImGuiSession.h b/editor/src/UI/ImGuiSession.h index 1e5833ae..b62f20aa 100644 --- a/editor/src/UI/ImGuiSession.h +++ b/editor/src/UI/ImGuiSession.h @@ -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"; diff --git a/editor/src/UI/ScalarControls.h b/editor/src/UI/ScalarControls.h index e432f109..8fe4715a 100644 --- a/editor/src/UI/ScalarControls.h +++ b/editor/src/UI/ScalarControls.h @@ -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), diff --git a/editor/src/UI/SplitterChrome.h b/editor/src/UI/SplitterChrome.h index a99e74a5..262ba7a9 100644 --- a/editor/src/UI/SplitterChrome.h +++ b/editor/src/UI/SplitterChrome.h @@ -2,6 +2,8 @@ #include "StyleTokens.h" +#include +#include #include 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 }; diff --git a/editor/src/UI/StyleTokens.h b/editor/src/UI/StyleTokens.h index 0e1d60e3..ccb56fba 100644 --- a/editor/src/UI/StyleTokens.h +++ b/editor/src/UI/StyleTokens.h @@ -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() { diff --git a/editor/src/Utils/FileDialogUtils.h b/editor/src/Utils/FileDialogUtils.h new file mode 100644 index 00000000..07b4521f --- /dev/null +++ b/editor/src/Utils/FileDialogUtils.h @@ -0,0 +1,167 @@ +#pragma once + +#include "Platform/Win32Utf8.h" + +#ifndef NOMINMAX +#define NOMINMAX +#endif + +#include +#include +#include +#include + +#include +#include + +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 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(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 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(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 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 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 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 diff --git a/editor/src/Utils/ProjectFileUtils.h b/editor/src/Utils/ProjectFileUtils.h new file mode 100644 index 00000000..4ca7a269 --- /dev/null +++ b/editor/src/Utils/ProjectFileUtils.h @@ -0,0 +1,119 @@ +#pragma once + +#include +#include +#include +#include + +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 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 diff --git a/editor/src/Utils/SceneEditorUtils.h b/editor/src/Utils/SceneEditorUtils.h index dbedb0dc..3fd25c9f 100644 --- a/editor/src/Utils/SceneEditorUtils.h +++ b/editor/src/Utils/SceneEditorUtils.h @@ -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 -#include -#include #include #include @@ -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 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(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 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(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);