From 20020318e17ce466e37993e9bb54d11f9823189f Mon Sep 17 00:00:00 2001
From: Patrick Wuttke
Date: Mon, 22 Sep 2025 17:12:35 +0200
Subject: [PATCH] Added configuration helper types.
---
private/raid/SModule | 1 +
private/raid/config.cpp | 365 ++++++++++++++++++++++++
private/raid_test/SModule | 1 +
private/raid_test/application.cpp | 8 +
private/raid_test/application.hpp | 6 +
private/raid_test/frames/config.cpp | 26 ++
private/raid_test/frames/config.hpp | 14 +
private/raid_test/frames/data_table.cpp | 1 +
public/raid/config.hpp | 189 ++++++++++++
public/raid/imraid.hpp | 15 +
public/raid/raid.hpp | 1 +
11 files changed, 627 insertions(+)
create mode 100644 private/raid/config.cpp
create mode 100644 private/raid_test/frames/config.cpp
create mode 100644 private/raid_test/frames/config.hpp
create mode 100644 public/raid/config.hpp
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)