#include "MetaCoreFoundation/MetaCoreProject.h" #include "MetaCorePlatform/MetaCoreWindow.h" #include "MetaCoreRender/MetaCoreEditorViewportRenderer.h" #include "MetaCoreRender/MetaCoreRenderDevice.h" #include #if defined(_WIN32) #include #else #include #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(_WIN32) #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif #include #else #include #include #include #endif #if defined(_WIN32) extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); #else extern char** environ; #endif namespace { struct MetaCoreLauncherProjectRecord { std::string Name{}; std::filesystem::path Root{}; std::string LastOpened{}; }; enum class MetaCoreLauncherPathPickerTarget { NewProjectParent, ExistingProject }; struct MetaCoreLauncherPathPickerEntry { std::filesystem::path Path{}; std::string Label{}; bool IsDirectory = false; bool IsProjectFile = false; }; [[nodiscard]] std::string MetaCoreToLower(std::string value) { std::transform(value.begin(), value.end(), value.begin(), [](unsigned char character) { return static_cast(std::tolower(character)); }); return value; } [[nodiscard]] std::string MetaCoreNowIsoString() { const auto now = std::chrono::system_clock::now(); const auto timeValue = std::chrono::system_clock::to_time_t(now); std::tm timeInfo{}; #if defined(_WIN32) localtime_s(&timeInfo, &timeValue); #else localtime_r(&timeValue, &timeInfo); #endif std::ostringstream stream; stream << std::put_time(&timeInfo, "%Y-%m-%dT%H:%M:%S"); return stream.str(); } [[nodiscard]] std::filesystem::path MetaCoreGetExecutableDirectory() { #if defined(_WIN32) std::vector buffer(MAX_PATH, L'\0'); while (true) { const DWORD copied = GetModuleFileNameW(nullptr, buffer.data(), static_cast(buffer.size())); if (copied == 0) { return {}; } if (copied < buffer.size() - 1) { return std::filesystem::path(std::wstring(buffer.data(), copied)).parent_path(); } buffer.resize(buffer.size() * 2, L'\0'); } #else std::vector buffer(PATH_MAX, '\0'); while (true) { const ssize_t copied = readlink("/proc/self/exe", buffer.data(), buffer.size()); if (copied < 0) { return {}; } if (static_cast(copied) < buffer.size()) { return std::filesystem::path(std::string(buffer.data(), static_cast(copied))).parent_path(); } buffer.resize(buffer.size() * 2, '\0'); } #endif } [[nodiscard]] std::filesystem::path MetaCoreGetLauncherStatePath() { #if defined(_WIN32) if (const char* appData = std::getenv("APPDATA"); appData != nullptr && appData[0] != '\0') { return std::filesystem::path(appData) / "MetaCore" / "launcher_projects.json"; } #else if (const char* home = std::getenv("HOME"); home != nullptr && home[0] != '\0') { return std::filesystem::path(home) / ".metacore" / "launcher_projects.json"; } #endif return std::filesystem::current_path() / ".metacore" / "launcher_projects.json"; } [[nodiscard]] std::filesystem::path MetaCoreGetHomeDirectory() { #if defined(_WIN32) if (const char* userProfile = std::getenv("USERPROFILE"); userProfile != nullptr && userProfile[0] != '\0') { return std::filesystem::path(userProfile); } #else if (const char* home = std::getenv("HOME"); home != nullptr && home[0] != '\0') { return std::filesystem::path(home); } #endif return std::filesystem::current_path(); } [[nodiscard]] std::string MetaCoreReadProjectDisplayName(const std::filesystem::path& projectRoot) { if (const auto projectDocument = MetaCore::MetaCoreReadProjectFile(MetaCore::MetaCoreGetProjectFilePath(projectRoot)); projectDocument.has_value() && !projectDocument->Name.empty()) { return projectDocument->Name; } return projectRoot.filename().string(); } [[nodiscard]] std::string MetaCoreProjectRootKey(const std::filesystem::path& projectRoot) { if (const auto resolvedRoot = MetaCore::MetaCoreResolveProjectRootCandidate(projectRoot); resolvedRoot.has_value()) { return resolvedRoot->generic_string(); } return projectRoot.lexically_normal().generic_string(); } void MetaCoreCopyToBuffer(std::array& buffer, const std::string& value) { std::memset(buffer.data(), 0, buffer.size()); std::strncpy(buffer.data(), value.c_str(), buffer.size() - 1); } void MetaCoreCopyToBuffer(std::array& buffer, const std::string& value) { std::memset(buffer.data(), 0, buffer.size()); std::strncpy(buffer.data(), value.c_str(), buffer.size() - 1); } [[nodiscard]] std::vector MetaCoreCollectPathPickerEntries( const std::filesystem::path& directory, bool includeProjectFile ) { std::vector entries; std::error_code errorCode; const std::filesystem::directory_options options = std::filesystem::directory_options::skip_permission_denied; for (auto iterator = std::filesystem::directory_iterator(directory, options, errorCode); iterator != std::filesystem::directory_iterator(); iterator.increment(errorCode)) { if (errorCode) { errorCode.clear(); continue; } const std::filesystem::path entryPath = iterator->path(); errorCode.clear(); const bool isDirectory = iterator->is_directory(errorCode); errorCode.clear(); const bool isProjectFile = includeProjectFile && iterator->is_regular_file(errorCode) && entryPath.filename() == "MetaCore.project.json"; if (!isDirectory && !isProjectFile) { continue; } MetaCoreLauncherPathPickerEntry entry; entry.Path = entryPath; entry.Label = entryPath.filename().string(); entry.IsDirectory = isDirectory; entry.IsProjectFile = isProjectFile; entries.push_back(std::move(entry)); } std::sort(entries.begin(), entries.end(), [](const auto& lhs, const auto& rhs) { if (lhs.IsDirectory != rhs.IsDirectory) { return lhs.IsDirectory; } return MetaCoreToLower(lhs.Label) < MetaCoreToLower(rhs.Label); }); return entries; } [[nodiscard]] std::filesystem::path MetaCoreEditorExecutablePath() { std::filesystem::path editorPath = MetaCoreGetExecutableDirectory() / "MetaCoreEditorApp"; #if defined(_WIN32) editorPath += ".exe"; #endif return editorPath; } bool MetaCoreLaunchEditorForProject( const std::filesystem::path& projectRoot, std::string& outError, bool removeLauncherOwnedBackend = false ) { const std::filesystem::path editorPath = MetaCoreEditorExecutablePath(); if (!std::filesystem::exists(editorPath)) { outError = "启动器旁边找不到 MetaCoreEditorApp。"; return false; } #if defined(_WIN32) SetEnvironmentVariableW(L"METACORE_PROJECT_PATH", projectRoot.wstring().c_str()); std::wstring commandLine = L"\"" + editorPath.wstring() + L"\" --project \"" + projectRoot.wstring() + L"\""; STARTUPINFOW startupInfo{}; startupInfo.cb = sizeof(startupInfo); PROCESS_INFORMATION processInfo{}; std::vector mutableCommandLine(commandLine.begin(), commandLine.end()); mutableCommandLine.push_back(L'\0'); const BOOL created = CreateProcessW( nullptr, mutableCommandLine.data(), nullptr, nullptr, FALSE, 0, nullptr, editorPath.parent_path().wstring().c_str(), &startupInfo, &processInfo ); if (!created) { outError = "启动 MetaCoreEditorApp 失败。"; return false; } CloseHandle(processInfo.hThread); CloseHandle(processInfo.hProcess); return true; #else const std::string editorPathString = editorPath.string(); const std::string editorNameString = editorPath.filename().string(); const std::string projectRootString = projectRoot.string(); std::vector environmentStrings; for (char** iterator = environ; iterator != nullptr && *iterator != nullptr; ++iterator) { const std::string_view entry(*iterator); if (entry.rfind("METACORE_PROJECT_PATH=", 0) == 0) { continue; } if (removeLauncherOwnedBackend && entry.rfind("METACORE_FILAMENT_BACKEND=", 0) == 0) { continue; } environmentStrings.emplace_back(*iterator); } environmentStrings.emplace_back("METACORE_PROJECT_PATH=" + projectRootString); std::vector environmentPointers; environmentPointers.reserve(environmentStrings.size() + 1); for (std::string& entry : environmentStrings) { environmentPointers.push_back(entry.data()); } environmentPointers.push_back(nullptr); std::array arguments = { const_cast(editorNameString.c_str()), const_cast("--project"), const_cast(projectRootString.c_str()), nullptr }; pid_t childPid = 0; const int result = posix_spawn( &childPid, editorPathString.c_str(), nullptr, nullptr, arguments.data(), environmentPointers.data() ); if (result != 0) { outError = "启动 MetaCoreEditorApp 失败。"; return false; } return true; #endif } void MetaCoreConfigureLauncherFont() { ImGuiIO& io = ImGui::GetIO(); io.Fonts->AddFontDefault(); std::vector candidatePaths; if (const char* overrideFontPath = std::getenv("METACORE_IMGUI_FONT"); overrideFontPath != nullptr && overrideFontPath[0] != '\0') { candidatePaths.emplace_back(overrideFontPath); } #if defined(_WIN32) char* windowsDirectory = nullptr; std::size_t windowsDirectoryLength = 0; if (_dupenv_s(&windowsDirectory, &windowsDirectoryLength, "WINDIR") == 0 && windowsDirectory != nullptr) { const std::filesystem::path fontsPath = std::filesystem::path(windowsDirectory) / "Fonts"; candidatePaths.push_back(fontsPath / "NotoSansSC-VF.ttf"); candidatePaths.push_back(fontsPath / "msyh.ttc"); candidatePaths.push_back(fontsPath / "msyhbd.ttc"); candidatePaths.push_back(fontsPath / "Deng.ttf"); candidatePaths.push_back(fontsPath / "simhei.ttf"); free(windowsDirectory); } #else candidatePaths.emplace_back("/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc"); candidatePaths.emplace_back("/usr/share/fonts/opentype/noto/NotoSansCJK-Medium.ttc"); candidatePaths.emplace_back("/usr/share/fonts/truetype/wqy/wqy-microhei.ttc"); candidatePaths.emplace_back("/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc"); candidatePaths.emplace_back("/usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf"); candidatePaths.emplace_back("/usr/share/fonts/truetype/arphic/uming.ttc"); #endif for (const auto& candidatePath : candidatePaths) { if (!std::filesystem::exists(candidatePath)) { continue; } try { ImFont* font = io.Fonts->AddFontFromFileTTF( candidatePath.string().c_str(), 17.0F, nullptr, io.Fonts->GetGlyphRangesChineseSimplifiedCommon() ); if (font != nullptr) { io.FontDefault = font; return; } } catch (const std::bad_alloc&) { } } } void MetaCoreApplyLauncherStyle() { ImGuiStyle& style = ImGui::GetStyle(); ImGui::StyleColorsDark(); style.WindowRounding = 0.0F; style.ChildRounding = 6.0F; style.PopupRounding = 6.0F; style.FrameRounding = 6.0F; style.GrabRounding = 6.0F; style.WindowBorderSize = 0.0F; style.FrameBorderSize = 1.0F; style.WindowPadding = ImVec2(0.0F, 0.0F); style.FramePadding = ImVec2(10.0F, 7.0F); style.ItemSpacing = ImVec2(8.0F, 8.0F); style.ScrollbarSize = 8.0F; style.Colors[ImGuiCol_Text] = ImVec4(0.81F, 0.81F, 0.81F, 1.0F); style.Colors[ImGuiCol_TextDisabled] = ImVec4(0.42F, 0.42F, 0.42F, 1.0F); style.Colors[ImGuiCol_WindowBg] = ImVec4(0.098F, 0.098F, 0.098F, 1.0F); style.Colors[ImGuiCol_ChildBg] = ImVec4(0.125F, 0.125F, 0.125F, 1.0F); style.Colors[ImGuiCol_PopupBg] = ImVec4(0.125F, 0.125F, 0.125F, 0.98F); style.Colors[ImGuiCol_Border] = ImVec4(0.18F, 0.18F, 0.18F, 1.0F); style.Colors[ImGuiCol_FrameBg] = ImVec4(0.125F, 0.125F, 0.125F, 1.0F); style.Colors[ImGuiCol_FrameBgHovered] = ImVec4(0.165F, 0.165F, 0.165F, 1.0F); style.Colors[ImGuiCol_FrameBgActive] = ImVec4(0.20F, 0.20F, 0.20F, 1.0F); style.Colors[ImGuiCol_Button] = ImVec4(0.125F, 0.125F, 0.125F, 1.0F); style.Colors[ImGuiCol_ButtonHovered] = ImVec4(0.165F, 0.165F, 0.165F, 1.0F); style.Colors[ImGuiCol_ButtonActive] = ImVec4(0.20F, 0.20F, 0.20F, 1.0F); style.Colors[ImGuiCol_Header] = ImVec4(0.20F, 0.20F, 0.20F, 1.0F); style.Colors[ImGuiCol_HeaderHovered] = ImVec4(0.24F, 0.24F, 0.24F, 1.0F); style.Colors[ImGuiCol_HeaderActive] = ImVec4(1.0F, 1.0F, 1.0F, 0.18F); style.Colors[ImGuiCol_Separator] = ImVec4(0.18F, 0.18F, 0.18F, 1.0F); } class MetaCoreLauncherApp { public: ~MetaCoreLauncherApp() { Shutdown(); } bool Initialize() { if (std::getenv("METACORE_FILAMENT_BACKEND") == nullptr) { #if defined(_WIN32) _putenv_s("METACORE_FILAMENT_BACKEND", "opengl"); #else setenv("METACORE_FILAMENT_BACKEND", "opengl", 0); #endif LauncherSetFilamentBackend_ = true; } if (!Window_.Initialize(1080, 720, "MetaCore 启动器")) { return false; } #if defined(_WIN32) Window_.SetNativeWindowMessageHandler([](void* nativeWindowHandle, unsigned int message, std::uintptr_t wparam, std::intptr_t lparam) { return ImGui_ImplWin32_WndProcHandler(static_cast(nativeWindowHandle), message, static_cast(wparam), static_cast(lparam)) != 0; }); #endif if (!RenderDevice_.Initialize(Window_)) { return false; } IMGUI_CHECKVERSION(); ImGui::CreateContext(); ImGuiIO& io = ImGui::GetIO(); io.IniFilename = nullptr; io.LogFilename = nullptr; MetaCoreConfigureLauncherFont(); MetaCoreApplyLauncherStyle(); #if defined(_WIN32) if (!ImGui_ImplWin32_InitForOpenGL(Window_.GetNativeWindowHandle())) { #else if (!ImGui_ImplGlfw_InitForOther(static_cast(Window_.GetNativeWindowHandle()), true)) { #endif return false; } if (!ViewportRenderer_.Initialize(RenderDevice_, Window_, true)) { return false; } LoadState(); MetaCoreCopyToBuffer(NewProjectParentBuffer_, std::filesystem::current_path().string()); MetaCoreCopyToBuffer(NewProjectNameBuffer_, "新项目"); Initialized_ = true; return true; } int Run() { while (!Window_.ShouldClose()) { Window_.BeginFrame(); #if defined(_WIN32) ImGui_ImplWin32_NewFrame(); #else ImGui_ImplGlfw_NewFrame(); #endif ImGui::NewFrame(); DrawFrame(); ImGui::Render(); ViewportRenderer_.RenderAll(); Window_.EndFrame(); } return 0; } void Shutdown() { if (!Initialized_) { return; } ViewportRenderer_.Shutdown(); #if defined(_WIN32) ImGui_ImplWin32_Shutdown(); #else ImGui_ImplGlfw_Shutdown(); #endif ImGui::DestroyContext(); RenderDevice_.Shutdown(); Window_.Shutdown(); Initialized_ = false; } private: void LoadState() { Projects_.clear(); const std::filesystem::path statePath = MetaCoreGetLauncherStatePath(); std::ifstream input(statePath); if (!input.is_open()) { return; } try { nlohmann::json json; input >> json; if (!json.contains("recent_projects") || !json["recent_projects"].is_array()) { return; } for (const auto& item : json["recent_projects"]) { if (!item.contains("root") || !item["root"].is_string()) { continue; } MetaCoreLauncherProjectRecord record; record.Root = item["root"].get(); if (item.contains("name") && item["name"].is_string()) { record.Name = item["name"].get(); } if (item.contains("last_opened") && item["last_opened"].is_string()) { record.LastOpened = item["last_opened"].get(); } if (const auto resolvedRoot = MetaCore::MetaCoreResolveProjectRootCandidate(record.Root); resolvedRoot.has_value()) { record.Root = *resolvedRoot; record.Name = MetaCoreReadProjectDisplayName(record.Root); } else if (record.Name.empty()) { record.Name = record.Root.filename().string(); } Projects_.push_back(std::move(record)); } } catch (const std::exception& exceptionObject) { StatusMessage_ = std::string("读取启动器状态失败:") + exceptionObject.what(); } } void SaveState() const { const std::filesystem::path statePath = MetaCoreGetLauncherStatePath(); std::error_code errorCode; std::filesystem::create_directories(statePath.parent_path(), errorCode); if (errorCode) { return; } nlohmann::json json; json["recent_projects"] = nlohmann::json::array(); for (const auto& project : Projects_) { json["recent_projects"].push_back({ {"name", project.Name}, {"root", project.Root.generic_string()}, {"last_opened", project.LastOpened} }); } std::ofstream output(statePath, std::ios::trunc); if (output.is_open()) { output << json.dump(2); } } bool UpsertProject(const std::filesystem::path& projectRoot) { const auto resolvedRoot = MetaCore::MetaCoreResolveProjectRootCandidate(projectRoot); if (!resolvedRoot.has_value()) { StatusMessage_ = "该路径不是有效的 MetaCore 项目。"; return false; } const std::string key = MetaCoreProjectRootKey(*resolvedRoot); auto iterator = std::find_if(Projects_.begin(), Projects_.end(), [&](const MetaCoreLauncherProjectRecord& record) { return MetaCoreProjectRootKey(record.Root) == key; }); MetaCoreLauncherProjectRecord record; record.Root = *resolvedRoot; record.Name = MetaCoreReadProjectDisplayName(*resolvedRoot); record.LastOpened = MetaCoreNowIsoString(); if (iterator != Projects_.end()) { *iterator = std::move(record); const auto index = static_cast(std::distance(Projects_.begin(), iterator)); SelectedIndex_ = index; std::rotate(Projects_.begin(), Projects_.begin() + index, Projects_.begin() + index + 1); SelectedIndex_ = 0; } else { Projects_.insert(Projects_.begin(), std::move(record)); SelectedIndex_ = 0; } SaveState(); return true; } void LaunchSelectedProject() { if (SelectedIndex_ < 0 || SelectedIndex_ >= static_cast(Projects_.size())) { StatusMessage_ = "请先选择一个项目。"; return; } auto& project = Projects_[static_cast(SelectedIndex_)]; const auto resolvedRoot = MetaCore::MetaCoreResolveProjectRootCandidate(project.Root); if (!resolvedRoot.has_value()) { StatusMessage_ = "项目文件夹缺失或无效。"; return; } std::string error; if (!MetaCoreLaunchEditorForProject(*resolvedRoot, error, LauncherSetFilamentBackend_)) { StatusMessage_ = error; return; } project.Root = *resolvedRoot; project.Name = MetaCoreReadProjectDisplayName(*resolvedRoot); project.LastOpened = MetaCoreNowIsoString(); SaveState(); StatusMessage_ = "已启动编辑器:" + project.Name; } void RemoveSelectedProjectRecord() { if (SelectedIndex_ < 0 || SelectedIndex_ >= static_cast(Projects_.size())) { StatusMessage_ = "请先选择一条项目记录。"; return; } Projects_.erase(Projects_.begin() + SelectedIndex_); if (Projects_.empty()) { SelectedIndex_ = -1; } else if (SelectedIndex_ >= static_cast(Projects_.size())) { SelectedIndex_ = static_cast(Projects_.size()) - 1; } SaveState(); } bool SetPathPickerDirectory(const std::filesystem::path& requestedPath) { std::filesystem::path directory = requestedPath; std::error_code errorCode; if (directory.filename() == "MetaCore.project.json" || std::filesystem::is_regular_file(directory, errorCode)) { directory = directory.parent_path(); } errorCode.clear(); if (!std::filesystem::exists(directory, errorCode) || !std::filesystem::is_directory(directory, errorCode)) { PathPickerError_ = "目录不存在或无法访问。"; return false; } errorCode.clear(); const std::filesystem::path canonicalDirectory = std::filesystem::weakly_canonical(directory, errorCode); PathPickerCurrentDirectory_ = !errorCode && !canonicalDirectory.empty() ? canonicalDirectory : directory.lexically_normal(); MetaCoreCopyToBuffer(PathPickerPathBuffer_, PathPickerCurrentDirectory_.string()); PathPickerSelectedPath_.clear(); PathPickerError_.clear(); return true; } void OpenPathPicker(MetaCoreLauncherPathPickerTarget target) { PathPickerTarget_ = target; PathPickerSelectedPath_.clear(); PathPickerError_.clear(); std::filesystem::path initialPath = target == MetaCoreLauncherPathPickerTarget::NewProjectParent ? std::filesystem::path(NewProjectParentBuffer_.data()) : std::filesystem::path(AddExistingPathBuffer_.data()); if (initialPath.empty()) { initialPath = std::filesystem::current_path(); } if (!SetPathPickerDirectory(initialPath)) { PathPickerError_.clear(); if (!SetPathPickerDirectory(std::filesystem::current_path())) { (void)SetPathPickerDirectory(MetaCoreGetHomeDirectory()); } } OpenPathPickerPopup_ = true; } void ApplyPickedPath(const std::filesystem::path& pickedPath) { if (PathPickerTarget_ == MetaCoreLauncherPathPickerTarget::NewProjectParent) { MetaCoreCopyToBuffer(NewProjectParentBuffer_, pickedPath.string()); } else { MetaCoreCopyToBuffer(AddExistingPathBuffer_, pickedPath.string()); } } void DrawFrame() { const ImGuiViewport* viewport = ImGui::GetMainViewport(); ImGui::SetNextWindowPos(viewport->Pos); ImGui::SetNextWindowSize(viewport->Size); ImGui::SetNextWindowViewport(viewport->ID); constexpr ImGuiWindowFlags rootFlags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoBringToFrontOnFocus; ImGui::Begin("MetaCoreLauncherRoot", nullptr, rootFlags); DrawSidebar(); ImGui::SameLine(0.0F, 0.0F); DrawProjectsPage(); DrawNewProjectPopup(); DrawAddExistingPopup(); ImGui::End(); } void DrawSidebar() { ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.078F, 0.078F, 0.078F, 1.0F)); ImGui::BeginChild("Sidebar", ImVec2(220.0F, 0.0F), false, ImGuiWindowFlags_NoScrollbar); ImGui::SetCursorPos(ImVec2(20.0F, 24.0F)); ImGui::TextUnformatted("MetaCore"); ImGui::SetCursorPosX(20.0F); ImGui::TextDisabled("启动器"); ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.145F, 0.145F, 0.145F, 1.0F)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.16F, 0.16F, 0.16F, 1.0F)); ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.18F, 0.18F, 0.18F, 1.0F)); ImGui::SetCursorPosX(12.0F); if (ImGui::Button("项目", ImVec2(196.0F, 38.0F))) { StatusMessage_ = "当前页面:项目"; } ImGui::PopStyleColor(3); ImGui::EndChild(); ImGui::PopStyleColor(); } void DrawProjectsPage() { ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.098F, 0.098F, 0.098F, 1.0F)); ImGui::BeginChild("ProjectsPage", ImVec2(0.0F, 0.0F), false); ImGui::SetCursorPos(ImVec2(28.0F, 24.0F)); ImGui::BeginGroup(); constexpr float pageRightPadding = 28.0F; const float headerStartX = ImGui::GetCursorPosX(); const float pageContentWidth = std::max(1.0F, ImGui::GetContentRegionAvail().x - pageRightPadding); const float headerButtonsWidth = 112.0F + 112.0F + 76.0F + 112.0F + ImGui::GetStyle().ItemSpacing.x * 3.0F; ImGui::TextUnformatted("项目"); ImGui::SameLine(headerStartX + std::max(0.0F, pageContentWidth - headerButtonsWidth)); if (ImGui::Button("新建项目", ImVec2(112.0F, 36.0F))) { MetaCoreCopyToBuffer(NewProjectNameBuffer_, "新项目"); if (NewProjectParentBuffer_[0] == '\0') { MetaCoreCopyToBuffer(NewProjectParentBuffer_, std::filesystem::current_path().string()); } OpenNewProjectPopup_ = true; } ImGui::SameLine(); if (ImGui::Button("添加已有", ImVec2(112.0F, 36.0F))) { MetaCoreCopyToBuffer(AddExistingPathBuffer_, std::filesystem::current_path().string()); OpenAddExistingPopup_ = true; } ImGui::SameLine(); if (ImGui::Button("启动", ImVec2(76.0F, 36.0F))) { LaunchSelectedProject(); } ImGui::SameLine(); if (ImGui::Button("移除记录", ImVec2(112.0F, 36.0F))) { RemoveSelectedProjectRecord(); } ImGui::SetCursorPosX(headerStartX); ImGui::SetNextItemWidth(pageContentWidth); ImGui::InputTextWithHint("##SearchProjects", "搜索项目...", SearchBuffer_.data(), SearchBuffer_.size()); if (!StatusMessage_.empty()) { ImGui::SetCursorPosX(headerStartX); ImGui::TextDisabled("%s", StatusMessage_.c_str()); } ImGui::SetCursorPosX(headerStartX); ImGui::BeginChild("ProjectCards", ImVec2(pageContentWidth, 0.0F), false); DrawProjectCards(); ImGui::EndChild(); ImGui::EndGroup(); ImGui::EndChild(); ImGui::PopStyleColor(); } void DrawProjectCards() { const std::string searchNeedle = MetaCoreToLower(SearchBuffer_.data()); int visibleCount = 0; for (std::size_t index = 0; index < Projects_.size(); ++index) { const auto& project = Projects_[index]; const std::string haystack = MetaCoreToLower(project.Name + " " + project.Root.string()); if (!searchNeedle.empty() && haystack.find(searchNeedle) == std::string::npos) { continue; } ++visibleCount; const bool selected = SelectedIndex_ == static_cast(index); const bool missing = !MetaCore::MetaCoreResolveProjectRootCandidate(project.Root).has_value(); const ImVec4 cardColor = selected ? ImVec4(0.20F, 0.20F, 0.20F, 1.0F) : ImVec4(0.125F, 0.125F, 0.125F, 1.0F); ImGui::PushID(static_cast(index)); ImGui::PushStyleColor(ImGuiCol_ChildBg, cardColor); ImGui::PushStyleColor(ImGuiCol_Border, selected ? ImVec4(1.0F, 1.0F, 1.0F, 0.65F) : ImVec4(0.18F, 0.18F, 0.18F, 1.0F)); ImGui::BeginChild("ProjectCard", ImVec2(0.0F, 96.0F), true, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); ImGui::SetCursorPos(ImVec2(14.0F, 12.0F)); ImGui::BeginGroup(); if (missing) { ImGui::TextDisabled("%s", project.Name.c_str()); } else { ImGui::TextUnformatted(project.Name.c_str()); } ImGui::TextDisabled("%s", project.Root.string().c_str()); if (missing) { ImGui::TextColored(ImVec4(0.92F, 0.34F, 0.34F, 1.0F), "项目缺失"); } else if (!project.LastOpened.empty()) { ImGui::TextDisabled("上次打开 %s", project.LastOpened.c_str()); } ImGui::EndGroup(); ImGui::EndChild(); if (ImGui::IsItemHovered()) { if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { SelectedIndex_ = static_cast(index); } if (!missing && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { SelectedIndex_ = static_cast(index); LaunchSelectedProject(); } } ImGui::PopStyleColor(2); ImGui::PopID(); } if (visibleCount == 0) { ImGui::Dummy(ImVec2(1.0F, 48.0F)); ImGui::TextDisabled("没有项目"); } } void DrawNewProjectPopup() { if (OpenNewProjectPopup_) { ImGui::OpenPopup("新建项目"); OpenNewProjectPopup_ = false; } ImGui::SetNextWindowSize(ImVec2(620.0F, 300.0F), ImGuiCond_Always); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(28.0F, 24.0F)); if (ImGui::BeginPopupModal("新建项目", nullptr, ImGuiWindowFlags_NoResize)) { const float popupInputWidth = ImGui::GetContentRegionAvail().x; ImGui::TextUnformatted("项目名称"); ImGui::SetNextItemWidth(popupInputWidth); ImGui::InputText("##NewProjectName", NewProjectNameBuffer_.data(), NewProjectNameBuffer_.size()); ImGui::Spacing(); ImGui::TextUnformatted("父目录"); const float browseButtonWidth = 88.0F; ImGui::SetNextItemWidth(std::max(1.0F, popupInputWidth - browseButtonWidth - ImGui::GetStyle().ItemSpacing.x)); ImGui::InputText("##NewProjectParent", NewProjectParentBuffer_.data(), NewProjectParentBuffer_.size()); ImGui::SameLine(); if (ImGui::Button("选择...", ImVec2(browseButtonWidth, 0.0F))) { OpenPathPicker(MetaCoreLauncherPathPickerTarget::NewProjectParent); } ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); ImGui::SetCursorPosX(std::max(28.0F, ImGui::GetWindowWidth() - 28.0F - 96.0F * 2.0F - ImGui::GetStyle().ItemSpacing.x)); if (ImGui::Button("创建", ImVec2(96.0F, 32.0F))) { const std::string name = NewProjectNameBuffer_.data(); const std::filesystem::path parent = NewProjectParentBuffer_.data(); if (name.empty() || parent.empty()) { StatusMessage_ = "项目名称和父目录不能为空。"; } else { const std::filesystem::path projectRoot = parent / name; if (MetaCore::MetaCoreCreateProjectSkeleton(projectRoot, name)) { if (UpsertProject(projectRoot)) { StatusMessage_ = "已创建项目:" + name; ImGui::CloseCurrentPopup(); } } else { StatusMessage_ = "创建项目失败。"; } } } ImGui::SameLine(); if (ImGui::Button("取消", ImVec2(96.0F, 32.0F))) { ImGui::CloseCurrentPopup(); } DrawPathPickerPopup(); ImGui::EndPopup(); } ImGui::PopStyleVar(); } void DrawAddExistingPopup() { if (OpenAddExistingPopup_) { ImGui::OpenPopup("添加已有项目"); OpenAddExistingPopup_ = false; } ImGui::SetNextWindowSize(ImVec2(620.0F, 240.0F), ImGuiCond_Always); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(28.0F, 24.0F)); if (ImGui::BeginPopupModal("添加已有项目", nullptr, ImGuiWindowFlags_NoResize)) { const float popupInputWidth = ImGui::GetContentRegionAvail().x; ImGui::TextUnformatted("项目路径"); const float browseButtonWidth = 88.0F; ImGui::SetNextItemWidth(std::max(1.0F, popupInputWidth - browseButtonWidth - ImGui::GetStyle().ItemSpacing.x)); ImGui::InputText("##ExistingProjectPath", AddExistingPathBuffer_.data(), AddExistingPathBuffer_.size()); ImGui::SameLine(); if (ImGui::Button("选择...", ImVec2(browseButtonWidth, 0.0F))) { OpenPathPicker(MetaCoreLauncherPathPickerTarget::ExistingProject); } ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); ImGui::SetCursorPosX(std::max(28.0F, ImGui::GetWindowWidth() - 28.0F - 96.0F * 2.0F - ImGui::GetStyle().ItemSpacing.x)); if (ImGui::Button("添加", ImVec2(96.0F, 32.0F))) { if (UpsertProject(std::filesystem::path(AddExistingPathBuffer_.data()))) { StatusMessage_ = "已添加项目:" + Projects_[static_cast(SelectedIndex_)].Name; ImGui::CloseCurrentPopup(); } } ImGui::SameLine(); if (ImGui::Button("取消", ImVec2(96.0F, 32.0F))) { ImGui::CloseCurrentPopup(); } DrawPathPickerPopup(); ImGui::EndPopup(); } ImGui::PopStyleVar(); } void DrawPathPickerPopup() { if (OpenPathPickerPopup_) { ImGui::OpenPopup("选择路径"); OpenPathPickerPopup_ = false; } ImGui::SetNextWindowSize(ImVec2(760.0F, 520.0F), ImGuiCond_Always); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(28.0F, 24.0F)); if (ImGui::BeginPopupModal("选择路径", nullptr, ImGuiWindowFlags_NoResize)) { const bool pickingNewProjectParent = PathPickerTarget_ == MetaCoreLauncherPathPickerTarget::NewProjectParent; ImGui::TextUnformatted(pickingNewProjectParent ? "选择新项目父目录" : "选择已有项目"); ImGui::TextDisabled(pickingNewProjectParent ? "选择项目将要创建到的父目录。" : "选择项目根目录,或选择 MetaCore.project.json。"); ImGui::Spacing(); const float popupWidth = ImGui::GetContentRegionAvail().x; const float actionButtonWidth = 76.0F; const float pathInputWidth = std::max( 1.0F, popupWidth - actionButtonWidth * 3.0F - ImGui::GetStyle().ItemSpacing.x * 3.0F ); ImGui::SetNextItemWidth(pathInputWidth); ImGui::InputText("##PathPickerPath", PathPickerPathBuffer_.data(), PathPickerPathBuffer_.size()); ImGui::SameLine(); if (ImGui::Button("前往", ImVec2(actionButtonWidth, 0.0F))) { (void)SetPathPickerDirectory(std::filesystem::path(PathPickerPathBuffer_.data())); } ImGui::SameLine(); if (ImGui::Button("上一级", ImVec2(actionButtonWidth, 0.0F))) { const std::filesystem::path parentPath = PathPickerCurrentDirectory_.parent_path(); if (!parentPath.empty() && parentPath != PathPickerCurrentDirectory_) { (void)SetPathPickerDirectory(parentPath); } } ImGui::SameLine(); if (ImGui::Button("主目录", ImVec2(actionButtonWidth, 0.0F))) { (void)SetPathPickerDirectory(MetaCoreGetHomeDirectory()); } ImGui::Spacing(); ImGui::TextDisabled("%s", PathPickerCurrentDirectory_.string().c_str()); ImGui::Spacing(); ImGui::BeginChild("PathPickerEntries", ImVec2(0.0F, 300.0F), true); const std::vector entries = MetaCoreCollectPathPickerEntries(PathPickerCurrentDirectory_, !pickingNewProjectParent); if (entries.empty()) { ImGui::TextDisabled("没有可选择的目录或项目文件。"); } for (const auto& entry : entries) { const std::string label = entry.IsDirectory ? std::string("[目录] ") + entry.Label : std::string("[项目文件] ") + entry.Label; const bool selected = !PathPickerSelectedPath_.empty() && PathPickerSelectedPath_ == entry.Path; if (ImGui::Selectable(label.c_str(), selected)) { PathPickerSelectedPath_ = entry.Path; } if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { if (entry.IsDirectory) { (void)SetPathPickerDirectory(entry.Path); } else { ApplyPickedPath(entry.Path); ImGui::CloseCurrentPopup(); } } } ImGui::EndChild(); if (!PathPickerError_.empty()) { ImGui::TextColored(ImVec4(0.92F, 0.34F, 0.34F, 1.0F), "%s", PathPickerError_.c_str()); } else if (!pickingNewProjectParent && std::filesystem::exists(PathPickerCurrentDirectory_ / "MetaCore.project.json")) { ImGui::TextDisabled("当前目录包含 MetaCore.project.json。"); } else { ImGui::TextDisabled("双击目录进入,单击目录后可选择该目录。"); } ImGui::Spacing(); const float footerButtonWidth = 128.0F; ImGui::SetCursorPosX(std::max( 28.0F, ImGui::GetWindowWidth() - 28.0F - footerButtonWidth * 2.0F - ImGui::GetStyle().ItemSpacing.x )); if (ImGui::Button( PathPickerSelectedPath_.empty() ? "选择当前目录" : "选择选中项", ImVec2(footerButtonWidth, 32.0F) )) { ApplyPickedPath(PathPickerSelectedPath_.empty() ? PathPickerCurrentDirectory_ : PathPickerSelectedPath_); ImGui::CloseCurrentPopup(); } ImGui::SameLine(); if (ImGui::Button("取消", ImVec2(footerButtonWidth, 32.0F))) { ImGui::CloseCurrentPopup(); } ImGui::EndPopup(); } ImGui::PopStyleVar(); } MetaCore::MetaCoreWindow Window_{}; MetaCore::MetaCoreRenderDevice RenderDevice_{}; MetaCore::MetaCoreEditorViewportRenderer ViewportRenderer_{}; std::vector Projects_{}; std::array SearchBuffer_{}; std::array NewProjectNameBuffer_{}; std::array NewProjectParentBuffer_{}; std::array AddExistingPathBuffer_{}; std::array PathPickerPathBuffer_{}; std::string StatusMessage_{}; std::string PathPickerError_{}; std::filesystem::path PathPickerCurrentDirectory_{}; std::filesystem::path PathPickerSelectedPath_{}; int SelectedIndex_ = -1; bool OpenNewProjectPopup_ = false; bool OpenAddExistingPopup_ = false; bool OpenPathPickerPopup_ = false; MetaCoreLauncherPathPickerTarget PathPickerTarget_ = MetaCoreLauncherPathPickerTarget::NewProjectParent; bool LauncherSetFilamentBackend_ = false; bool Initialized_ = false; }; } // namespace int main() { #if !defined(_WIN32) std::signal(SIGCHLD, SIG_IGN); #endif MetaCoreLauncherApp app; if (!app.Initialize()) { std::cerr << "MetaCoreLauncher initialize failed\n"; return 1; } return app.Run(); }