summaryrefslogtreecommitdiff
path: root/core
diff options
context:
space:
mode:
authorrtk0c <[email protected]>2021-03-29 17:55:02 -0700
committerrtk0c <[email protected]>2021-03-29 17:55:02 -0700
commit70cc233165b5efa3a3888af34f7afce095fe6947 (patch)
treeb15e8f2e3816acc204846188e78514f2ba6ad816 /core
parent6032ae108064650324b2c45352e1baa5b36961cc (diff)
More work on project tab
Diffstat (limited to 'core')
-rw-r--r--core/CMakeLists.txt3
-rw-r--r--core/locale/zh_CN.json16
-rw-r--r--core/src/Entrypoint/main.cpp4
-rw-r--r--core/src/Model/GlobalStates.cpp110
-rw-r--r--core/src/Model/GlobalStates.hpp43
-rw-r--r--core/src/Model/Project.cpp50
-rw-r--r--core/src/Model/Project.hpp9
-rw-r--r--core/src/Model/fwd.hpp3
-rw-r--r--core/src/UI/Localization.hpp20
-rw-r--r--core/src/UI/States.cpp15
-rw-r--r--core/src/UI/States.hpp6
-rw-r--r--core/src/UI/UI.hpp7
-rw-r--r--core/src/UI/UI_MainWindow.cpp129
-rw-r--r--core/src/UI/UI_Utils.cpp16
-rw-r--r--core/src/Utils/StandardDirectories.cpp74
-rw-r--r--core/src/Utils/StandardDirectories.hpp10
16 files changed, 501 insertions, 14 deletions
diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt
index 3f8fb12..be089d8 100644
--- a/core/CMakeLists.txt
+++ b/core/CMakeLists.txt
@@ -34,6 +34,7 @@ set(ENTRYPOINT_MODULE_SOURCES
)
add_source_group(MODEL_MODULE_SOURCES
+ src/Model/GlobalStates.cpp
src/Model/Project.cpp
src/Model/Stock.cpp
)
@@ -46,11 +47,13 @@ add_source_group(UI_MODULE_SOURCES
src/UI/UI_Items.cpp
src/UI/UI_MainWindow.cpp
src/UI/UI_Settings.cpp
+ src/UI/UI_Utils.cpp
)
add_source_group(UTILS_MODULE_SOURCES
src/Utils/I18n.cpp
src/Utils/Sigslot.cpp
+ src/Utils/StandardDirectories.cpp
src/Utils/String.cpp
)
diff --git a/core/locale/zh_CN.json b/core/locale/zh_CN.json
index d455f61..8550b1d 100644
--- a/core/locale/zh_CN.json
+++ b/core/locale/zh_CN.json
@@ -5,8 +5,18 @@
"MainWindow.Tab.DatabaseView": "\uf1c0 数据",
"MainWindow.Tab.Items": "\uf466 物品",
"MainWindow.Tab.Exports": "\uf56e 导出",
- "Project.NewProject": "新建项目...",
- "Project.OpenProject": "打开项目...",
+ "Project.New": "新建项目...",
+ "Project.New.DialogTitle": "新建项目向导",
+ "Project.New.Confirm": "确定",
+ "Project.New.Cancel": "取消",
+ "Project.New.Name": "项目名称",
+ "Project.New.Path": "项目路径",
+ "Project.New.EmptyName": "项目名不能为空",
+ "Project.New.InvalidPath": "无效路径",
+ "Project.Open": "打开项目...",
"Project.Recents": "最近使用",
- "Project.ClearRecents": "清空",
+ "Project.Recents.Clear": "清空",
+ "Project.Recents.NonePresent": "(暂无最近使用的项目)",
+ "Project.Recents.Open.Tooltip": "打开该项目",
+ "Project.Recents.Delete.Tooltip": "将该项目从最近使用列表中删除,项目本身将不受影响。",
} \ No newline at end of file
diff --git a/core/src/Entrypoint/main.cpp b/core/src/Entrypoint/main.cpp
index 39943d6..ce8aab7 100644
--- a/core/src/Entrypoint/main.cpp
+++ b/core/src/Entrypoint/main.cpp
@@ -5,6 +5,7 @@
#include "Entrypoint/OpenGL2.hpp"
#include "Entrypoint/OpenGL3.hpp"
#include "Entrypoint/Vulkan.hpp"
+#include "Model/GlobalStates.hpp"
#include "UI/Localization.hpp"
#include "UI/States.hpp"
#include "UI/UI.hpp"
@@ -143,6 +144,7 @@ int main(int argc, char* argv[]) {
I18n::Init();
I18n::SetLanguage("zh_CN");
+ GlobalStates::Init();
UIState::Init();
auto window = backend->GetWindow();
@@ -152,5 +154,7 @@ int main(int argc, char* argv[]) {
backend->EndFrame();
}
+ GlobalStates::Shutdown();
+
return 0;
}
diff --git a/core/src/Model/GlobalStates.cpp b/core/src/Model/GlobalStates.cpp
new file mode 100644
index 0000000..cd076f4
--- /dev/null
+++ b/core/src/Model/GlobalStates.cpp
@@ -0,0 +1,110 @@
+#include "GlobalStates.hpp"
+
+#include "Model/Project.hpp"
+#include "Utils/StandardDirectories.hpp"
+
+#include <json/reader.h>
+#include <json/value.h>
+#include <json/writer.h>
+#include <cassert>
+#include <filesystem>
+#include <fstream>
+#include <memory>
+
+namespace fs = std::filesystem;
+
+static std::unique_ptr<GlobalStates> globalStateInstance;
+static fs::path globalDataPath;
+
+void GlobalStates::Init() {
+ globalStateInstance = std::make_unique<GlobalStates>();
+ globalDataPath = StandardDirectories::UserData() / "cplt";
+ fs::create_directories(globalDataPath);
+
+ // Reading recent projects
+ [&]() {
+ std::ifstream ifs(globalDataPath / "recents.json");
+ if (!ifs) return;
+
+ Json::Value root;
+ ifs >> root;
+
+ if (!root.isObject()) return;
+ if (auto& recents = root["RecentProjects"]; recents.isArray()) {
+ for (auto& elm : recents) {
+ if (!elm.isString()) continue;
+
+ fs::path path(elm.asCString());
+ if (!fs::exists(path)) continue;
+
+ auto utf8String = path.string();
+ globalStateInstance->mRecentProjects.push_back(RecentProject{
+ .path = std::move(path),
+ .cachedUtf8String = std::move(utf8String),
+ });
+ }
+ }
+ }();
+}
+
+void GlobalStates::Shutdown() {
+ if (!globalStateInstance) return;
+ if (globalStateInstance->mDirty) {
+ globalStateInstance->WriteToDisk();
+ }
+}
+
+GlobalStates& GlobalStates::GetInstance() {
+ return *globalStateInstance;
+}
+
+const std::filesystem::path& GlobalStates::GetUserDataPath() {
+ return globalDataPath;
+}
+
+const std::vector<GlobalStates::RecentProject>& GlobalStates::GetRecentProjects() const {
+ return mRecentProjects;
+}
+
+void GlobalStates::ClearRecentProjects() {
+ mRecentProjects.clear();
+ MarkDirty();
+}
+
+void GlobalStates::AddRecentProject(const Project& project) {
+ mRecentProjects.push_back(RecentProject{
+ .path = project.GetPath(),
+ .cachedUtf8String = project.GetPath().string(),
+ });
+ MarkDirty();
+}
+
+void GlobalStates::RemoveRecentProject(int idx) {
+ assert(idx >= 0 && idx < mRecentProjects.size());
+
+ mRecentProjects.erase(mRecentProjects.begin() + idx);
+ MarkDirty();
+}
+
+void GlobalStates::WriteToDisk() const {
+ Json::Value root;
+
+ auto& recentProjects = root["RecentProjects"] = Json::Value(Json::arrayValue);
+ for (auto& [path, _] : mRecentProjects) {
+ recentProjects.append(Json::Value(path.string()));
+ }
+
+ std::ofstream ofs(globalDataPath / "recents.json");
+ ofs << root;
+
+ mDirty = false;
+}
+
+bool GlobalStates::IsDirty() const {
+ return mDirty;
+}
+
+void GlobalStates::MarkDirty() {
+ mDirty = true;
+ OnModified();
+}
diff --git a/core/src/Model/GlobalStates.hpp b/core/src/Model/GlobalStates.hpp
new file mode 100644
index 0000000..e6d823b
--- /dev/null
+++ b/core/src/Model/GlobalStates.hpp
@@ -0,0 +1,43 @@
+#pragma once
+
+#include "Utils/Sigslot.hpp"
+#include "cplt_fwd.hpp"
+
+#include <filesystem>
+#include <string>
+#include <vector>
+
+class GlobalStates {
+public:
+ static void Init();
+ static void Shutdown();
+
+ static GlobalStates& GetInstance();
+ static const std::filesystem::path& GetUserDataPath();
+
+ struct RecentProject {
+ std::filesystem::path path;
+ std::string cachedUtf8String;
+ };
+
+public:
+ Signal<> OnModified;
+
+private:
+ std::vector<RecentProject> mRecentProjects;
+ mutable bool mDirty = false;
+
+public:
+ const std::vector<RecentProject>& GetRecentProjects() const;
+ void ClearRecentProjects();
+ void AddRecentProject(const Project& project);
+ void RemoveRecentProject(int idx);
+
+ // TODO async autosaving to prevent data loss on crash
+ void WriteToDisk() const;
+
+ bool IsDirty() const;
+
+private:
+ void MarkDirty();
+};
diff --git a/core/src/Model/Project.cpp b/core/src/Model/Project.cpp
index f301bb8..c54a02c 100644
--- a/core/src/Model/Project.cpp
+++ b/core/src/Model/Project.cpp
@@ -1,8 +1,56 @@
#include "Project.hpp"
+#include <json/reader.h>
+#include <json/value.h>
+#include <json/writer.h>
+#include <filesystem>
+#include <fstream>
+#include <stdexcept>
#include <utility>
-const std::filesystem::path& Project::GetPath() const {
+namespace fs = std::filesystem;
+
+Project Project::Load(const fs::path& path) {
+ // TODO better diagnostic
+ const char* kInvalidFormatErr = "Failed to load project: invalid format.";
+
+ std::ifstream ifs(path);
+ if (!ifs) {
+ std::string message;
+ message += "Failed to load project file at '";
+ message += path.string();
+ message += "'.";
+ throw std::runtime_error(message);
+ }
+
+ Project proj;
+ proj.mRootPath = path.parent_path();
+
+ Json::Value root;
+ ifs >> root;
+
+ const auto& croot = root; // Use const reference so that accessors default to returning a null if not found, instead of siliently creating new elements
+ if (!croot.isObject()) {
+ throw std::runtime_error(kInvalidFormatErr);
+ }
+
+ if (auto& name = croot["Name"]; name.isString()) {
+ proj.mName = name.asString();
+ } else {
+ throw std::runtime_error(kInvalidFormatErr);
+ }
+
+ return proj;
+}
+
+Project Project::Create(std::string name, const fs::path& path) {
+ Project proj;
+ proj.mRootPath = path;
+ proj.mName = std::move(name);
+ return proj;
+}
+
+const fs::path& Project::GetPath() const {
return mRootPath;
}
diff --git a/core/src/Model/Project.hpp b/core/src/Model/Project.hpp
index 34ae1d7..7b5c7e3 100644
--- a/core/src/Model/Project.hpp
+++ b/core/src/Model/Project.hpp
@@ -9,9 +9,18 @@ public:
std::string mName;
public:
+ /// Load the project from a cplt_project.json file.
+ static Project Load(const std::filesystem::path& path);
+ /// Create a project with the given name in the given path. Note that the path should be a directory that will contain the project files once created.
+ /// This function assumes the given directory will exist and is empty.
+ static Project Create(std::string name, const std::filesystem::path& path);
+
// Path to a *directory* that contains the project file.
const std::filesystem::path& GetPath() const;
const std::string& GetName() const;
void SetName(std::string name);
+
+private:
+ Project() = default;
};
diff --git a/core/src/Model/fwd.hpp b/core/src/Model/fwd.hpp
index f5a5818..6bbc0b7 100644
--- a/core/src/Model/fwd.hpp
+++ b/core/src/Model/fwd.hpp
@@ -1,4 +1,7 @@
#pragma once
+// GlobalStates.hpp
+class GlobalStates;
+
// Project.hpp
class Project;
diff --git a/core/src/UI/Localization.hpp b/core/src/UI/Localization.hpp
index 648dd6d..7b401ef 100644
--- a/core/src/UI/Localization.hpp
+++ b/core/src/UI/Localization.hpp
@@ -18,8 +18,20 @@ public:
BasicTranslation TabItems{ "MainWindow.Tab.Items"sv };
BasicTranslation TabExport{ "MainWindow.Tab.Exports"sv };
- BasicTranslation NewProject{ "Project.NewProject"sv };
- BasicTranslation OpenProject{ "Project.OpenProject"sv };
- BasicTranslation Recents{ "Project.Recents"sv };
- BasicTranslation ClearRecents{ "Project.ClearRecents"sv };
+ BasicTranslation NewProject{ "Project.New"sv };
+ BasicTranslation TitleNewProject{ "Project.New.DialogTitle"sv };
+ BasicTranslation ActionNewProjectConfirm{ "Project.New.Confirm"sv };
+ BasicTranslation ActionNewProjectCancel{ "Project.New.Cancel"sv };
+ BasicTranslation HintNewProjectName{ "Project.New.Name"sv };
+ BasicTranslation HintNewProjectPath{ "Project.New.Path"sv };
+ BasicTranslation ErrorNewProjectEmptyName{ "Project.New.EmptyName"sv };
+ BasicTranslation ErrorNewProjectInvalidPath{ "Project.New.InvalidPath"sv };
+
+ BasicTranslation OpenProject{ "Project.Open"sv };
+
+ BasicTranslation RecentProjects{ "Project.Recents"sv };
+ BasicTranslation ActionClearRecentProjects{ "Project.Recents.Clear"sv };
+ BasicTranslation MessageNoRecentProjects{ "Project.Recents.NonePresent"sv };
+ BasicTranslation TooltipOpenRecentProject{ "Project.Recents.Open.Tooltip"sv };
+ BasicTranslation TooltipDeleteRecentProject{ "Project.Recents.Delete.Tooltip"sv };
};
diff --git a/core/src/UI/States.cpp b/core/src/UI/States.cpp
index efae152..07bbcf7 100644
--- a/core/src/UI/States.cpp
+++ b/core/src/UI/States.cpp
@@ -2,6 +2,9 @@
#include "Model/Project.hpp"
+#include <memory>
+#include <utility>
+
static std::unique_ptr<UIState> uiStateInstance;
void UIState::Init() {
@@ -11,3 +14,15 @@ void UIState::Init() {
UIState& UIState::GetInstance() {
return *uiStateInstance;
}
+
+void UIState::SetCurrentProject(std::unique_ptr<Project> project) {
+ CloseCurrentProject();
+ CurrentProject = std::move(project);
+}
+
+void UIState::CloseCurrentProject() {
+ if (CurrentProject) {
+ // TODO save stuff
+ CurrentProject = nullptr;
+ }
+}
diff --git a/core/src/UI/States.hpp b/core/src/UI/States.hpp
index d1c1faf..cbb556f 100644
--- a/core/src/UI/States.hpp
+++ b/core/src/UI/States.hpp
@@ -4,6 +4,8 @@
#include <memory>
+/// Minimal state shared by all UI components, such as database, items, export, etc.
+/// Note that global components (settings) is not supposed to access these.
class UIState {
public:
static void Init();
@@ -11,4 +13,8 @@ public:
public:
std::unique_ptr<Project> CurrentProject;
+
+public:
+ void SetCurrentProject(std::unique_ptr<Project> project);
+ void CloseCurrentProject();
};
diff --git a/core/src/UI/UI.hpp b/core/src/UI/UI.hpp
index 08f5771..b0c3aaa 100644
--- a/core/src/UI/UI.hpp
+++ b/core/src/UI/UI.hpp
@@ -1,5 +1,12 @@
#pragma once
+namespace ImGui {
+
+void ErrorIcon();
+void WarningIcon();
+
+} // namespace ImGui
+
namespace UI {
void MainWindow();
diff --git a/core/src/UI/UI_MainWindow.cpp b/core/src/UI/UI_MainWindow.cpp
index 0c8e7b9..9b20550 100644
--- a/core/src/UI/UI_MainWindow.cpp
+++ b/core/src/UI/UI_MainWindow.cpp
@@ -1,34 +1,151 @@
#include "UI.hpp"
+#include "Model/GlobalStates.hpp"
#include "Model/Project.hpp"
#include "UI/Localization.hpp"
#include "UI/States.hpp"
+#include <IconsFontAwesome.h>
#include <imgui.h>
+#include <imgui_internal.h>
+#include <imgui_stdlib.h>
+#include <filesystem>
+
+namespace fs = std::filesystem;
namespace {
+void LoadProjectAt(const std::filesystem::path& path) {
+ auto& uis = UIState::GetInstance();
+ auto& gs = GlobalStates::GetInstance();
+
+ if (uis.CurrentProject) {
+ uis.CloseCurrentProject();
+ }
+}
+
void ProjectTab_Normal() {
// TODO
}
void ProjectTab_NoProject() {
auto ls = LocaleStrings::Instance.get();
+ auto& gs = GlobalStates::GetInstance();
+ auto& uis = UIState::GetInstance();
+ static std::string projectName;
+ static std::string dirName;
+ static fs::path dirPath;
+ static bool dirNameIsValid = false;
if (ImGui::Button(ls->NewProject.Get())) {
- // TODO
+ auto vs = ImGui::GetMainViewport()->Size; // Viewport Size
+ ImGui::SetNextWindowSize({ vs.x * 0.5f, vs.y * 0.5f });
+ ImGui::SetNextWindowPos({ vs.x / 2, vs.y / 2 }, ImGuiCond_Always, { 0.5f, 0.5f }); // Center window initially
+ ImGui::OpenPopup(ls->TitleNewProject.Get());
}
+
+ // Make it so that the modal dialog has a close button
+ bool newProjectDialogDummyTrue = true;
+ if (ImGui::BeginPopupModal(ls->TitleNewProject.Get(), &newProjectDialogDummyTrue)) {
+ ImGui::InputTextWithHint("##ProjectName", ls->HintNewProjectName.Get(), &projectName);
+
+ if (ImGui::InputTextWithHint("##ProjectPath", ls->HintNewProjectPath.Get(), &dirName)) {
+ // Changed, validate value
+ fs::path newPath(dirName);
+ if (fs::exists(newPath)) {
+ dirNameIsValid = true;
+ dirPath = std::move(newPath);
+ } else {
+ dirNameIsValid = false;
+ }
+ }
+ ImGui::SameLine();
+ if (ImGui::Button("...")) {
+ // TODO file dialog
+ }
+
+ if (projectName.empty()) {
+ ImGui::ErrorIcon();
+
+ ImGui::SameLine();
+ ImGui::Text(ls->ErrorNewProjectEmptyName.Get());
+ }
+
+ if (!dirNameIsValid) {
+ ImGui::ErrorIcon();
+
+ ImGui::SameLine();
+ ImGui::Text(ls->ErrorNewProjectInvalidPath.Get());
+ }
+
+ ImGui::Spacing();
+
+ bool formValid = dirNameIsValid && !projectName.empty();
+
+ if (!formValid) {
+ ImGui::PushItemFlag(ImGuiItemFlags_Disabled, false);
+ ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.5f * ImGui::GetStyle().Alpha);
+ }
+ if (ImGui::Button(ls->ActionNewProjectConfirm.Get())) {
+ ImGui::CloseCurrentPopup();
+
+ auto project = Project::Create(std::move(projectName), dirPath);
+ auto uptr = std::unique_ptr<Project>(new Project(std::move(project)));
+ uis.SetCurrentProject(std::move(uptr));
+
+ // Dialog just got closed, reset states
+ projectName = "";
+ dirName = "";
+ dirPath = fs::path{};
+ dirNameIsValid = false;
+ }
+ if (!formValid) {
+ ImGui::PopItemFlag();
+ ImGui::PopStyleVar();
+ }
+
+ ImGui::SameLine();
+ if (ImGui::Button(ls->ActionNewProjectCancel.Get())) {
+ ImGui::CloseCurrentPopup();
+ }
+
+ ImGui::EndPopup();
+ }
+
if (ImGui::Button(ls->OpenProject.Get())) {
// TODO
}
ImGui::Separator();
- ImGui::Text(ls->Recents.Get());
+ ImGui::Text(ls->RecentProjects.Get());
ImGui::SameLine();
- if (ImGui::Button(ls->ClearRecents.Get())) {
- // TODO
+ if (ImGui::Button(ls->ActionClearRecentProjects.Get())) {
+ gs.ClearRecentProjects();
}
- // TODO
+ auto& recentProjects = gs.GetRecentProjects();
+ if (recentProjects.empty()) {
+ ImGui::Text(ls->MessageNoRecentProjects.Get());
+ }
+ for (auto it = recentProjects.begin(); it != recentProjects.end(); ++it) {
+ auto& [path, recent] = *it;
+ ImGui::Text(recent.c_str());
+
+ ImGui::SameLine();
+ if (ImGui::Button(ICON_FA_EDIT)) {
+ LoadProjectAt(path);
+ }
+ if (ImGui::IsItemHovered()) {
+ ImGui::SetTooltip(ls->TooltipOpenRecentProject.Get());
+ }
+
+ ImGui::SameLine();
+ if (ImGui::Button(ICON_FA_TRASH)) {
+ gs.RemoveRecentProject(std::distance(recentProjects.begin(), it));
+ }
+ if (ImGui::IsItemHovered()) {
+ ImGui::SetTooltip(ls->TooltipDeleteRecentProject.Get());
+ }
+ }
}
} // namespace
@@ -46,7 +163,7 @@ void UI::MainWindow() {
ImGui::EndTabItem();
}
- if (ImGui::BeginTabItem(ls->TabProject.Get())) {
+ if (ImGui::BeginTabItem(ls->TabProject.Get(), nullptr, ImGuiTabItemFlags_SetSelected)) {
if (uis.CurrentProject) {
ProjectTab_Normal();
} else {
diff --git a/core/src/UI/UI_Utils.cpp b/core/src/UI/UI_Utils.cpp
new file mode 100644
index 0000000..61a62f0
--- /dev/null
+++ b/core/src/UI/UI_Utils.cpp
@@ -0,0 +1,16 @@
+#include "UI.hpp"
+
+#include <IconsFontAwesome.h>
+#include <imgui.h>
+
+void ImGui::ErrorIcon() {
+ ImGui::PushStyleColor(ImGuiCol_Text, ImVec4{ 237 / 255.0f, 67 / 255.0f, 55 / 255.0f, 1.0f }); // #ED4337
+ ImGui::Text(ICON_FA_EXCLAMATION_CIRCLE);
+ ImGui::PopStyleColor();
+}
+
+void ImGui::WarningIcon() {
+ ImGui::PushStyleColor(ImGuiCol_Text, ImVec4{ 255 / 255.0f, 184 / 255.0f, 24 / 255.0f, 1.0f }); // #FFB818
+ ImGui::Text(ICON_FA_EXCLAMATION_TRIANGLE);
+ ImGui::PopStyleColor();
+}
diff --git a/core/src/Utils/StandardDirectories.cpp b/core/src/Utils/StandardDirectories.cpp
new file mode 100644
index 0000000..7defc5d
--- /dev/null
+++ b/core/src/Utils/StandardDirectories.cpp
@@ -0,0 +1,74 @@
+#include "StandardDirectories.hpp"
+
+#include <filesystem>
+#include <stdexcept>
+
+namespace fs = std::filesystem;
+
+#if PLATFORM_WIN32
+// https://stackoverflow.com/questions/54499256/how-to-find-the-saved-games-folder-programmatically-in-c-c
+# include <ShlObj_core.h>
+# include <objbase.h>
+# pragma comment(lib, "shell32.lib")
+# pragma comment(lib, "ole32.lib")
+
+static fs::path GetAppDataRoaming() {
+ PWSTR path = nullptr;
+ HRESULT hr = SHGetKnownFolderPath(FOLDERID_RoamingAppData, KF_FLAG_CREATE, nullptr, &path);
+ if (SUCCEEDED(hr)) {
+ auto dataDir = fs::path(path);
+ CoTaskMemFree(path);
+
+ fs::create_directories(dataDir);
+ return dataDir;
+ } else {
+ fs::path dataDir("~/AppData/Roaming");
+ fs::create_directories(dataDir);
+ return dataDir;
+ }
+}
+
+#elif PLATFORM_MACOS
+// TODO
+#elif PLATFORM_LINUX
+# include <cstdlib>
+
+static fs::path GetEnvVar(const char* name, const char* backup) {
+ if (const char* path = std::getenv(name)) {
+ fs::path dataDir(path);
+ fs::create_directories(dataDir);
+ return dataDir;
+ } else {
+ fs::path dataDir(backup);
+ fs::create_directories(dataDir);
+ return dataDir;
+ }
+}
+
+#endif
+
+const std::filesystem::path& StandardDirectories::UserData() {
+ static auto userDataDir = []() -> fs::path {
+#if PLATFORM_WIN32
+ return GetAppDataRoaming();
+#elif PLATFORM_MACOS
+ // TODO where?
+#elif PLATFORM_LINUX
+ return GetEnvVar("XDG_DATA_HOME", "~/.local/share");
+#endif
+ }();
+ return userDataDir;
+}
+
+const std::filesystem::path& StandardDirectories::UserConfig() {
+ static auto userConfigDir = []() -> fs::path {
+#if PLATFORM_WIN32
+ return GetAppDataRoaming();
+#elif PLATFORM_MACOS
+ // TODO where?
+#elif PLATFORM_LINUX
+ return GetEnvVar("XDG_CONFIG_HOME", "~/.config");
+#endif
+ }();
+ return userConfigDir;
+}
diff --git a/core/src/Utils/StandardDirectories.hpp b/core/src/Utils/StandardDirectories.hpp
new file mode 100644
index 0000000..4f7e5e2
--- /dev/null
+++ b/core/src/Utils/StandardDirectories.hpp
@@ -0,0 +1,10 @@
+#pragma once
+
+#include <filesystem>
+
+namespace StandardDirectories {
+
+const std::filesystem::path& UserData();
+const std::filesystem::path& UserConfig();
+
+} // namespace StandardDirectories