Added configuration helper types.

This commit is contained in:
Patrick Wuttke 2025-09-22 17:12:35 +02:00
parent 9bc56a2748
commit 20020318e1
11 changed files with 627 additions and 0 deletions

View File

@ -8,6 +8,7 @@ if not hasattr(env, 'Jinja'):
src_files = Split("""
application.cpp
config.cpp
fonts.gen.cpp
stb_image.cpp
""")

365
private/raid/config.cpp Normal file
View File

@ -0,0 +1,365 @@
#include "raid/config.hpp"
#include <mijin/async/coroutine_sleep.hpp>
#include <mijin/io/stlstream.hpp>
#include <mijin/util/string.hpp>
#include <mijin/util/variant.hpp>
#include <yaml-cpp/yaml.h>
template<>
struct YAML::convert<raid::ConfigSection>
{
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<std::string, raid::ConfigValue> values;
for (YAML::const_iterator it = node.begin(); it != node.end(); ++it) {
values.emplace(it->first.as<std::string>(), it->second.as<raid::ConfigValue>());
}
section = raid::ConfigSection(std::move(values));
return true;
}
};
template<>
struct YAML::convert<raid::ConfigArray>
{
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<raid::ConfigValue> values;
for (const YAML::Node& value : node) {
values.push_back(value.as<raid::ConfigValue>());
}
array = raid::ConfigArray(std::move(values));
return true;
}
};
template<>
struct YAML::convert<raid::ConfigValue>
{
static Node encode(const raid::ConfigValue& value)
{
Node node;
value.visit([&]<typename T>(const T& content) {
if constexpr (!std::is_same_v<T, std::nullptr_t>) {
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<raid::ConfigArray>();
break;
case YAML::NodeType::Map:
value = node.as<raid::ConfigSection>();
break;
case YAML::NodeType::Scalar:
try
{
value = node.as<raid::config_int_t>();
break;
}
catch(const YAML::Exception&) {} // NOLINT(bugprone-empty-catch)
try
{
value = node.as<double>();
}
catch(const YAML::Exception&) {} // NOLINT(bugprone-empty-catch)
value = node.as<std::string>();
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<std::ptrdiff_t>(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<config_int_t>(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<double>(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<ConfigArray>(mContent);
}
return EMPTY;
}
ConfigSection& ConfigValue::asMutableSection() noexcept
{
MIJIN_ASSERT(isSection(), "Cannot call this on non-section values!");
return std::get<ConfigSection>(mContent);
}
const ConfigSection& ConfigValue::asSection() const noexcept
{
static const ConfigSection EMPTY;
if (isSection()) {
return std::get<ConfigSection>(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<mijin::Stream> 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<ConfigSection>();
}
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<mijin::Stream> 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 {};
}
}

View File

@ -5,6 +5,7 @@ src_files = Split("""
main.cpp
application.cpp
frames/config.cpp
frames/data_table.cpp
""")

View File

@ -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"))

View File

@ -6,6 +6,7 @@
#include <array>
#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<bool, NUM_FRAMES> mFrameOpen{};
public:
[[nodiscard]]
raid::FileConfig& getConfig() noexcept { return mConfig; }
protected:
bool init() override;
void configureImgui() override;

View File

@ -0,0 +1,26 @@
#include "raid_test/frames/config.hpp"
#include <imgui.h>
#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();
}
}

View File

@ -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)

View File

@ -1,6 +1,7 @@
#include "raid_test/frames/data_table.hpp"
#include <array>
#include <chrono>
#include <random>
#include <imgui.h>

189
public/raid/config.hpp Normal file
View File

@ -0,0 +1,189 @@
#pragma once
#if !defined(RAID_PUBLIC_RAID_CONFIG_HPP_INCLUDED)
#define RAID_PUBLIC_RAID_CONFIG_HPP_INCLUDED 1
#include <cstdint>
#include <string>
#include <string_view>
#include <variant>
#include <vector>
#include <mijin/async/coroutine.hpp>
#include <mijin/container/vector_map.hpp>
#include <mijin/memory/dynamic_pointer.hpp>
#include <mijin/types/result.hpp>
#include <mijin/virtual_filesystem/filesystem.hpp>
namespace raid
{
class ConfigValue;
class ConfigArray
{
public:
using iterator = std::vector<ConfigValue>::iterator;
using const_iterator = std::vector<ConfigValue>::const_iterator;
private:
std::vector<ConfigValue> mValues;
public:
ConfigArray() noexcept = default;
ConfigArray(const ConfigArray&) = default;
ConfigArray(ConfigArray&&) noexcept = default;
explicit ConfigArray(std::vector<ConfigValue> 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<ConfigValue>& 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<std::string, ConfigValue> mValues;
public:
ConfigSection() noexcept = default;
ConfigSection(const ConfigSection&) = default;
ConfigSection(ConfigSection&&) noexcept = default;
explicit ConfigSection(mijin::VectorMap<std::string, ConfigValue> 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<std::string, ConfigValue>& getValues() noexcept { return mValues; }
[[nodiscard]]
const mijin::VectorMap<std::string, ConfigValue>& getValues() const noexcept { return mValues; }
};
using config_int_t = std::int64_t;
class ConfigValue
{
private:
std::variant<std::nullptr_t, bool, config_int_t, double, std::string, ConfigArray, ConfigSection> 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<std::nullptr_t>(mContent); }
[[nodiscard]]
bool isBool() const noexcept { return std::holds_alternative<bool>(mContent); }
[[nodiscard]]
bool isInt() const noexcept { return std::holds_alternative<config_int_t>(mContent); }
[[nodiscard]]
bool isDouble() const noexcept { return std::holds_alternative<double>(mContent); }
[[nodiscard]]
bool isString() const noexcept { return std::holds_alternative<std::string>(mContent); }
[[nodiscard]]
bool isArray() const noexcept { return std::holds_alternative<ConfigArray>(mContent); }
[[nodiscard]]
bool isSection() const noexcept { return std::holds_alternative<ConfigSection>(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<typename TFunc>
decltype(auto) visit(TFunc&& func) const noexcept
{
return std::visit(std::forward<TFunc>(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)

View File

@ -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<std::size_t> sortedIndices;

View File

@ -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)