diff --git a/private/raid/SModule b/private/raid/SModule index b0efdda..6874b80 100644 --- a/private/raid/SModule +++ b/private/raid/SModule @@ -8,6 +8,7 @@ if not hasattr(env, 'Jinja'): src_files = Split(""" application.cpp + config.cpp fonts.gen.cpp stb_image.cpp """) diff --git a/private/raid/config.cpp b/private/raid/config.cpp new file mode 100644 index 0000000..eae51c9 --- /dev/null +++ b/private/raid/config.cpp @@ -0,0 +1,365 @@ + +#include "raid/config.hpp" + +#include +#include +#include +#include +#include + +template<> +struct YAML::convert +{ + static Node encode(const raid::ConfigSection& section) + { + Node node; + for (const auto& [key, value] : section.getValues()) { + node[key] = value; + } + return node; + } + + static bool decode(const Node& node, raid::ConfigSection& section) + { + if (!node.IsMap()) { + return false; + } + + mijin::VectorMap values; + for (YAML::const_iterator it = node.begin(); it != node.end(); ++it) { + values.emplace(it->first.as(), it->second.as()); + } + section = raid::ConfigSection(std::move(values)); + return true; + } +}; +template<> +struct YAML::convert +{ + static Node encode(const raid::ConfigArray& array) + { + Node node; + for (const raid::ConfigValue& value : array.getValues()) { + node.push_back(value); + } + return node; + } + + static bool decode(const Node& node, raid::ConfigArray& array) + { + if (!node.IsSequence()) { + return false; + } + + std::vector values; + for (const YAML::Node& value : node) { + values.push_back(value.as()); + } + array = raid::ConfigArray(std::move(values)); + return true; + } +}; +template<> +struct YAML::convert +{ + static Node encode(const raid::ConfigValue& value) + { + Node node; + value.visit([&](const T& content) { + if constexpr (!std::is_same_v) { + node = content; + } + else { + node = {}; + } + }); + return node; + } + + static bool decode(const Node& node, raid::ConfigValue& value) + { + switch (node.Type()) + { + case YAML::NodeType::Null: + case YAML::NodeType::Undefined: + value = {}; + break; + case YAML::NodeType::Sequence: + value = node.as(); + break; + case YAML::NodeType::Map: + value = node.as(); + break; + case YAML::NodeType::Scalar: + try + { + value = node.as(); + break; + } + catch(const YAML::Exception&) {} // NOLINT(bugprone-empty-catch) + try + { + value = node.as(); + } + catch(const YAML::Exception&) {} // NOLINT(bugprone-empty-catch) + value = node.as(); + break; + default: + return false; + } + return true; + } +}; + +namespace raid +{ +namespace +{ +const ConfigValue EMPTY_VALUE; +} + +const ConfigValue& ConfigSection::operator[](std::string_view key) const noexcept +{ + auto it = mValues.find(key); + if (it == mValues.end()) { + return EMPTY_VALUE; + } + return it->second; +} + +void ConfigArray::append(ConfigValue value) +{ + mValues.push_back(std::move(value)); +} + +void ConfigArray::setAt(std::size_t idx, ConfigValue value) +{ + mValues[idx] = std::move(value); +} + +void ConfigArray::removeAt(std::size_t idx) +{ + mValues.erase(mValues.begin() + static_cast(idx)); +} + + +ConfigValue& ConfigSection::getOrAdd(std::string_view key) +{ + return mValues[key]; +} + +void ConfigSection::set(std::string_view key, ConfigValue value) +{ + mValues[key] = std::move(value); +} + +bool ConfigValue::asBool() const noexcept +{ + return visit(mijin::Visitor{ + [](std::nullptr_t) { return false; }, + [](bool boolValue) { return boolValue; }, + [](config_int_t intValue) { return intValue != 0; }, + [](double doubleValue) { return doubleValue != 0.0; }, + [](const std::string& stringValue) { return stringValue == "true"; }, + [](const ConfigArray&) { return false; }, + [](const ConfigSection&) { return false; } + }); +} + +config_int_t ConfigValue::asInt() const noexcept +{ + return visit(mijin::Visitor{ + [](std::nullptr_t) { return config_int_t(0); }, + [](bool boolValue) { return config_int_t(boolValue); }, + [](config_int_t intValue) { return intValue; }, + [](double doubleValue) { return static_cast(doubleValue); }, + [](const std::string& stringValue) { + config_int_t intValue = 0; + if (mijin::toNumber(stringValue, intValue)) { + return intValue; + } + return config_int_t(0); + }, + [](const ConfigArray&) { return config_int_t(0); }, + [](const ConfigSection&) { return config_int_t(0); } + }); +} + +double ConfigValue::asDouble() const noexcept +{ + return visit(mijin::Visitor{ + [](std::nullptr_t) { return 0.0; }, + [](bool boolValue) { return double(boolValue); }, + [](config_int_t intValue) { return static_cast(intValue); }, + [](double doubleValue) { return doubleValue; }, + [](const std::string& stringValue) { + double doubleValue = 0; + if (mijin::toNumber(stringValue, doubleValue)) { + return doubleValue; + } + return 0.0; + }, + [](const ConfigArray&) { return 0.0; }, + [](const ConfigSection&) { return 0.0; } + }); +} + +const std::string& ConfigValue::asString() const noexcept +{ + static const std::string TRUE = "true"; + static const std::string FALSE = "false"; + static const std::string EMPTY{}; + static thread_local std::string convertBuffer; + + return visit(mijin::Visitor{ + [](std::nullptr_t) -> const std::string& { return EMPTY; }, + [](bool boolValue) -> const std::string& { return boolValue ? TRUE : FALSE; }, + [](config_int_t intValue) -> const std::string& { + convertBuffer = std::to_string(intValue); + return convertBuffer; + }, + [](double doubleValue) -> const std::string& { + convertBuffer = std::to_string(doubleValue); + return convertBuffer; + }, + [](const std::string& stringValue) -> const std::string& { + return stringValue; // NOLINT(bugprone-return-const-ref-from-parameter) + }, + [](const ConfigArray&) -> const std::string& { return EMPTY; }, + [](const ConfigSection&) -> const std::string& { return EMPTY; } + }); +} + +const ConfigArray& ConfigValue::asArray() const noexcept +{ + static const ConfigArray EMPTY; + if (isArray()) { + return std::get(mContent); + } + return EMPTY; +} + +ConfigSection& ConfigValue::asMutableSection() noexcept +{ + MIJIN_ASSERT(isSection(), "Cannot call this on non-section values!"); + return std::get(mContent); +} + +const ConfigSection& ConfigValue::asSection() const noexcept +{ + static const ConfigSection EMPTY; + if (isSection()) { + return std::get(mContent); + } + return EMPTY; +} + +const ConfigValue& FileConfig::getValue(std::string_view path) const noexcept +{ + const ConfigSection* section = &mRoot; + while(true) + { + MIJIN_ASSERT(!path.empty(), "Invalid config value path."); + + const std::string_view::size_type pos = path.find('/'); + if (pos == std::string_view::npos) { + break; + } + section = &(*section)[path.substr(0, pos)].asSection(); + path = path.substr(pos + 1); + } + return (*section)[path]; +} + +void FileConfig::setValue(std::string_view path, ConfigValue value) noexcept +{ + ConfigSection* section = &mRoot; + while (true) + { + MIJIN_ASSERT(!path.empty(), "Invalid config value path."); + + const std::string_view::size_type pos = path.find('/'); + if (pos == std::string_view::npos) { + break; + } + + ConfigValue& existing = section->getOrAdd(path.substr(0, pos)); + if (existing.isUndefined()) { + existing = ConfigSection(); + } + else if (!existing.isSection()) + { + MIJIN_ERROR("Value already exists, but is not a section."); + return; + } + section = &existing.asMutableSection(); + path = path.substr(pos + 1); + } + + section->set(path, std::move(value)); + mDirty = true; +} + + +mijin::Result<> FileConfig::init(mijin::PathReference path) +{ + mPath = std::move(path); + + if (mPath.getInfo().exists) { + return load(); + } + return {}; +} + +mijin::Result<> FileConfig::load() +{ + std::unique_ptr stream; + if (const mijin::StreamError result = mPath.open(mijin::FileOpenMode::READ, stream); result != mijin::StreamError::SUCCESS) { + return mijin::ResultError(mijin::errorName(result)); + } + + mijin::IOStreamAdapter streamAdapter(*stream); + YAML::Node root; + + try { + root = YAML::Load(streamAdapter); + } + catch(const YAML::Exception& exc) { + return mijin::ResultError(exc.what()); + } + + if (!root.IsMap()) { + return mijin::ResultError("invalid config file, expected a map"); + } + + try { + mRoot = root.as(); + } + catch(const YAML::Exception& exc) { + return mijin::ResultError(exc.what()); + } + return {}; +} + +mijin::Result<> FileConfig::save(bool force) +{ + if (!force && !mDirty) { + return {}; + } + + mDirty = false; + + std::unique_ptr stream; + if (const mijin::StreamError result = mPath.open(mijin::FileOpenMode::WRITE, stream); result != mijin::StreamError::SUCCESS) { + return mijin::ResultError(mijin::errorName(result)); + } + + YAML::Emitter emitter; + emitter << YAML::Node(mRoot); + if (const mijin::StreamError result = stream->writeText(emitter.c_str()); result != mijin::StreamError::SUCCESS) { + return mijin::ResultError(mijin::errorName(result)); + } + + return {}; +} +} diff --git a/private/raid_test/SModule b/private/raid_test/SModule index a62ff14..0b64ccc 100644 --- a/private/raid_test/SModule +++ b/private/raid_test/SModule @@ -5,6 +5,7 @@ src_files = Split(""" main.cpp application.cpp + frames/config.cpp frames/data_table.cpp """) diff --git a/private/raid_test/application.cpp b/private/raid_test/application.cpp index 9bff3d7..beff5bd 100644 --- a/private/raid_test/application.cpp +++ b/private/raid_test/application.cpp @@ -14,6 +14,10 @@ bool Application::init() setMainWindowStyle(ImGuiStyleVar_WindowPadding, ImVec2()); setMainWindowStyle(ImGuiStyleVar_WindowBorderSize, 0.f); std::ranges::fill(mFrameOpen, true); + + if (const mijin::Result<> result = mConfig.init(getFS().getPath("/config/persistent.yml")); !result.isSuccess()) { + msgError("Error initializing config: {}.", result.getError().message); + } return true; } @@ -26,6 +30,10 @@ void Application::configureImgui() void Application::render() { + if (const mijin::Result<> result = mConfig.save(); !result.isSuccess()) { + msgError("Error while saving config: {}.", result.getError().message); + } + if (ImGui::BeginMenuBar()) { if (ImGui::BeginMenu("File")) diff --git a/private/raid_test/application.hpp b/private/raid_test/application.hpp index f119334..e74fbc9 100644 --- a/private/raid_test/application.hpp +++ b/private/raid_test/application.hpp @@ -6,6 +6,7 @@ #include #include "raid/raid.hpp" +#include "./frames/config.hpp" #include "./frames/data_table.hpp" namespace raid_test @@ -20,12 +21,17 @@ private: render_fn_t render; }; static constexpr Frame FRAMES[] = { + {.title = CONFIG_TITLE, .render = &renderConfig}, {.title = DATA_TABLE_TITLE, .render = &renderDataTable} }; static constexpr std::size_t NUM_FRAMES = sizeof(FRAMES) / sizeof(FRAMES[0]); + raid::FileConfig mConfig; bool mShowMetrics = false; std::array mFrameOpen{}; +public: + [[nodiscard]] + raid::FileConfig& getConfig() noexcept { return mConfig; } protected: bool init() override; void configureImgui() override; diff --git a/private/raid_test/frames/config.cpp b/private/raid_test/frames/config.cpp new file mode 100644 index 0000000..23ba41c --- /dev/null +++ b/private/raid_test/frames/config.cpp @@ -0,0 +1,26 @@ + +#include "raid_test/frames/config.hpp" + +#include +#include "raid/config.hpp" +#include "raid_test/application.hpp" + +namespace raid_test +{ +void renderConfig(bool& open) +{ + if (!ImGui::Begin(CONFIG_TITLE, &open)) + { + ImGui::End(); + return; + } + + static constexpr const char* TEST_BOOL_PATH = "test/section/bool"; + bool testBool = gApplication.getConfig().getValue(TEST_BOOL_PATH).asBool(); + if (ImGui::Checkbox("Test Bool", &testBool)) { + gApplication.getConfig().setValue(TEST_BOOL_PATH, testBool); + } + + ImGui::End(); +} +} \ No newline at end of file diff --git a/private/raid_test/frames/config.hpp b/private/raid_test/frames/config.hpp new file mode 100644 index 0000000..9496e06 --- /dev/null +++ b/private/raid_test/frames/config.hpp @@ -0,0 +1,14 @@ + +#pragma once + +#if !defined(RAID_TEST_FRAMES_CONFIG_HPP_INCLUDED) +#define RAID_TEST_FRAMES_CONFIG_HPP_INCLUDED 1 + +namespace raid_test +{ +inline constexpr const char* CONFIG_TITLE = "Config"; + +void renderConfig(bool& open); +} // namespace raid_test + +#endif // !defined(RAID_TEST_FRAMES_CONFIG_HPP_INCLUDED) diff --git a/private/raid_test/frames/data_table.cpp b/private/raid_test/frames/data_table.cpp index c8c4f6d..a597e32 100644 --- a/private/raid_test/frames/data_table.cpp +++ b/private/raid_test/frames/data_table.cpp @@ -1,6 +1,7 @@ #include "raid_test/frames/data_table.hpp" +#include #include #include #include diff --git a/public/raid/config.hpp b/public/raid/config.hpp new file mode 100644 index 0000000..f896971 --- /dev/null +++ b/public/raid/config.hpp @@ -0,0 +1,189 @@ + +#pragma once + +#if !defined(RAID_PUBLIC_RAID_CONFIG_HPP_INCLUDED) +#define RAID_PUBLIC_RAID_CONFIG_HPP_INCLUDED 1 + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace raid +{ +class ConfigValue; + +class ConfigArray +{ +public: + using iterator = std::vector::iterator; + using const_iterator = std::vector::const_iterator; +private: + std::vector mValues; +public: + ConfigArray() noexcept = default; + ConfigArray(const ConfigArray&) = default; + ConfigArray(ConfigArray&&) noexcept = default; + explicit ConfigArray(std::vector values) noexcept : mValues(std::move(values)) {} + + ConfigArray& operator=(const ConfigArray&) = default; + ConfigArray& operator=(ConfigArray&&) noexcept = default; + + const ConfigValue& operator[](std::size_t idx) const noexcept { return mValues[idx]; } + + [[nodiscard]] + const std::vector& getValues() const noexcept { return mValues; } + + [[nodiscard]] + std::size_t getSize() const noexcept { return mValues.size(); } + + void append(ConfigValue value); + void setAt(std::size_t idx, ConfigValue value); + void removeAt(std::size_t idx); + + [[nodiscard]] + bool isEmpty() const noexcept { return mValues.empty(); } + + [[nodiscard]] + iterator begin() noexcept { return mValues.begin(); } + + [[nodiscard]] + iterator end() noexcept { return mValues.end(); } + + [[nodiscard]] + const_iterator begin() const noexcept { return mValues.begin(); } + + [[nodiscard]] + const_iterator end() const noexcept { return mValues.end(); } +}; + +class ConfigSection +{ +private: + mijin::VectorMap mValues; +public: + ConfigSection() noexcept = default; + ConfigSection(const ConfigSection&) = default; + ConfigSection(ConfigSection&&) noexcept = default; + explicit ConfigSection(mijin::VectorMap values) noexcept : mValues(std::move(values)) {} + + ConfigSection& operator=(const ConfigSection&) = default; + ConfigSection& operator=(ConfigSection&&) noexcept = default; + + const ConfigValue& operator[](std::string_view key) const noexcept; + + [[nodiscard]] + ConfigValue& getOrAdd(std::string_view key); + + void set(std::string_view key, ConfigValue value); + + [[nodiscard]] + mijin::VectorMap& getValues() noexcept { return mValues; } + + [[nodiscard]] + const mijin::VectorMap& getValues() const noexcept { return mValues; } +}; + +using config_int_t = std::int64_t; +class ConfigValue +{ +private: + std::variant mContent; +public: + ConfigValue() noexcept : mContent(nullptr) {} + ConfigValue(const ConfigValue&) = default; + ConfigValue(ConfigValue&&) noexcept = default; + ConfigValue(bool content) noexcept : mContent(content) {} + ConfigValue(config_int_t content) noexcept : mContent(content) {} + ConfigValue(double content) noexcept : mContent(content) {} + ConfigValue(std::string content) noexcept : mContent(std::move(content)) {} + ConfigValue(const char* content) noexcept : mContent(std::string(content)) {} + ConfigValue(ConfigArray content) noexcept : mContent(std::move(content)) {} + ConfigValue(ConfigSection content) noexcept : mContent(std::move(content)) {} + + ConfigValue& operator=(const ConfigValue&) = default; + ConfigValue& operator=(ConfigValue&&) noexcept = default; + + [[nodiscard]] + bool isUndefined() const noexcept { return std::holds_alternative(mContent); } + + [[nodiscard]] + bool isBool() const noexcept { return std::holds_alternative(mContent); } + + [[nodiscard]] + bool isInt() const noexcept { return std::holds_alternative(mContent); } + + [[nodiscard]] + bool isDouble() const noexcept { return std::holds_alternative(mContent); } + + [[nodiscard]] + bool isString() const noexcept { return std::holds_alternative(mContent); } + + [[nodiscard]] + bool isArray() const noexcept { return std::holds_alternative(mContent); } + + [[nodiscard]] + bool isSection() const noexcept { return std::holds_alternative(mContent); } + + [[nodiscard]] + bool asBool() const noexcept; + + [[nodiscard]] + config_int_t asInt() const noexcept; + + [[nodiscard]] + double asDouble() const noexcept; + + [[nodiscard]] + const std::string& asString() const noexcept; + + [[nodiscard]] + const ConfigArray& asArray() const noexcept; + + [[nodiscard]] + ConfigSection& asMutableSection() noexcept; + + [[nodiscard]] + const ConfigSection& asSection() const noexcept; + + template + decltype(auto) visit(TFunc&& func) const noexcept + { + return std::visit(std::forward(func), mContent); + } +}; + +class FileConfig +{ +private: + ConfigSection mRoot; + mijin::PathReference mPath; + bool mDirty = false; +public: + [[nodiscard]] + const ConfigSection& getRoot() const noexcept { return mRoot; } + + [[nodiscard]] + const ConfigValue& getValue(std::string_view path) const noexcept; + + void setValue(std::string_view path, ConfigValue value) noexcept; + + [[nodiscard]] + mijin::Result<> init(mijin::PathReference path); + + [[nodiscard]] + mijin::Result<> load(); + + [[nodiscard]] + mijin::Result<> save(bool force = false); +}; +} + +#endif // !defined(RAID_PUBLIC_RAID_CONFIG_HPP_INCLUDED) diff --git a/public/raid/imraid.hpp b/public/raid/imraid.hpp index 0d89e4b..5cd46f9 100644 --- a/public/raid/imraid.hpp +++ b/public/raid/imraid.hpp @@ -55,6 +55,21 @@ inline bool ToggleImageButton(const char* strId, ImTextureID textureId, const Im return clicked; } +inline bool BeginPopupButton(const char* label) +{ + char popupId[128] = {"popup##"}; + std::strcat(popupId, label); + + const float popupX = ImGui::GetCursorScreenPos().x; + if (ImGui::Button(label)) { + ImGui::OpenPopup(popupId); + } + + const float popupY = ImGui::GetCursorScreenPos().y; + ImGui::SetNextWindowPos({popupX, popupY}); + return ImGui::BeginPopup(popupId, ImGuiWindowFlags_NoNav); +} + struct DataTableState { std::vector sortedIndices; diff --git a/public/raid/raid.hpp b/public/raid/raid.hpp index f75eb87..af968f1 100644 --- a/public/raid/raid.hpp +++ b/public/raid/raid.hpp @@ -5,5 +5,6 @@ #define RAID_PUBLIC_RAID_RAID_HPP_INCLUDED 1 #include "./application.hpp" +#include "./config.hpp" #endif // !defined(RAID_PUBLIC_RAID_RAID_HPP_INCLUDED)