MetaCore/Apps/MetaCoreLauncher/main.cpp

1056 lines
41 KiB
C++

#include "MetaCoreFoundation/MetaCoreProject.h"
#include "MetaCorePlatform/MetaCoreWindow.h"
#include "MetaCoreRender/MetaCoreEditorViewportRenderer.h"
#include "MetaCoreRender/MetaCoreRenderDevice.h"
#include <imgui.h>
#if defined(_WIN32)
#include <imgui_impl_win32.h>
#else
#include <imgui_impl_glfw.h>
#include <GLFW/glfw3.h>
#endif
#include <nlohmann/json.hpp>
#include <algorithm>
#include <array>
#include <chrono>
#include <csignal>
#include <cctype>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <new>
#include <optional>
#include <sstream>
#include <string>
#include <string_view>
#include <system_error>
#include <vector>
#if defined(_WIN32)
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#else
#include <limits.h>
#include <spawn.h>
#include <unistd.h>
#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<char>(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<wchar_t> buffer(MAX_PATH, L'\0');
while (true) {
const DWORD copied = GetModuleFileNameW(nullptr, buffer.data(), static_cast<DWORD>(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<char> 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<std::size_t>(copied) < buffer.size()) {
return std::filesystem::path(std::string(buffer.data(), static_cast<std::size_t>(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<char, 512>& 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<char, 128>& 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<MetaCoreLauncherPathPickerEntry> MetaCoreCollectPathPickerEntries(
const std::filesystem::path& directory,
bool includeProjectFile
) {
std::vector<MetaCoreLauncherPathPickerEntry> 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<wchar_t> 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<std::string> 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<char*> environmentPointers;
environmentPointers.reserve(environmentStrings.size() + 1);
for (std::string& entry : environmentStrings) {
environmentPointers.push_back(entry.data());
}
environmentPointers.push_back(nullptr);
std::array<char*, 4> arguments = {
const_cast<char*>(editorNameString.c_str()),
const_cast<char*>("--project"),
const_cast<char*>(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<std::filesystem::path> 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<HWND>(nativeWindowHandle), message, static_cast<WPARAM>(wparam), static_cast<LPARAM>(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<GLFWwindow*>(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<std::string>();
if (item.contains("name") && item["name"].is_string()) {
record.Name = item["name"].get<std::string>();
}
if (item.contains("last_opened") && item["last_opened"].is_string()) {
record.LastOpened = item["last_opened"].get<std::string>();
}
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<int>(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<int>(Projects_.size())) {
StatusMessage_ = "请先选择一个项目。";
return;
}
auto& project = Projects_[static_cast<std::size_t>(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<int>(Projects_.size())) {
StatusMessage_ = "请先选择一条项目记录。";
return;
}
Projects_.erase(Projects_.begin() + SelectedIndex_);
if (Projects_.empty()) {
SelectedIndex_ = -1;
} else if (SelectedIndex_ >= static_cast<int>(Projects_.size())) {
SelectedIndex_ = static_cast<int>(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<int>(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<int>(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<int>(index);
}
if (!missing && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
SelectedIndex_ = static_cast<int>(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<std::size_t>(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<MetaCoreLauncherPathPickerEntry> 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<MetaCoreLauncherProjectRecord> Projects_{};
std::array<char, 256> SearchBuffer_{};
std::array<char, 128> NewProjectNameBuffer_{};
std::array<char, 512> NewProjectParentBuffer_{};
std::array<char, 512> AddExistingPathBuffer_{};
std::array<char, 512> 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();
}