Files
Destrum/destrum/include/destrum/Serialization/Reflect.h
T

293 lines
11 KiB
C++

#ifndef DESTRUM_REFLECT_H
#define DESTRUM_REFLECT_H
#include <concepts>
#include <cstdint>
#include <functional>
#include <optional>
#include <span>
#include <stdexcept>
#include <string>
#include <string_view>
#include <type_traits>
#include <vector>
#include <glm/glm.hpp>
#include <nlohmann/json.hpp>
#include <destrum/ObjectModel/ObjectRef.h>
#include <destrum/Serialization/GlmJson.h>
class Component;
class GameObject;
class Scene;
// Designated initializers have to follow this order
struct FieldOptions {
float speed = 0.1f;
std::optional<double> min;
std::optional<double> max;
const char* format = nullptr;
bool color = false;
bool angle = false; // radians, shown in degrees
bool multiline = false;
bool acceptAssets = false; // strings take a virtual path dropped from the asset browser
bool readOnly = false;
bool hidden = false; // saved, not shown
bool transient = false; // shown, not saved
const char* label = nullptr;
const char* tooltip = nullptr;
std::span<const char* const> enumNames;
std::function<bool(const GameObject&)> filter;
};
struct FieldOps {
void (*save)(const void* value, nlohmann::json& out, const FieldOptions& options);
void (*load)(void* value, const nlohmann::json& in, const FieldOptions& options);
bool (*draw)(const char* label, void* value, const FieldOptions& options, const Scene* scene);
void (*resolve)(void* value, const ObjectRef::ObjectMap& objects, std::vector<ObjectId>& dropped);
};
namespace reflect_detail {
enum class Scalar { S8, U8, S16, U16, S32, U32, S64, U64, Float, Double };
template <class T>
consteval Scalar ScalarOf()
{
if constexpr (std::same_as<T, float>) return Scalar::Float;
else if constexpr (std::same_as<T, double>) return Scalar::Double;
else if constexpr (std::is_signed_v<T>) {
if constexpr (sizeof(T) == 1) return Scalar::S8;
else if constexpr (sizeof(T) == 2) return Scalar::S16;
else if constexpr (sizeof(T) == 4) return Scalar::S32;
else return Scalar::S64;
} else {
if constexpr (sizeof(T) == 1) return Scalar::U8;
else if constexpr (sizeof(T) == 2) return Scalar::U16;
else if constexpr (sizeof(T) == 4) return Scalar::U32;
else return Scalar::U64;
}
}
bool DrawScalar(const char* label, Scalar type, void* value, int components, const void* min, const void* max, const FieldOptions& options);
bool DrawAngle(const char* label, float& radians, const FieldOptions& options);
bool DrawEnum(const char* label, std::int64_t& value, const FieldOptions& options);
void BeginList(const char* label);
[[nodiscard]] bool ListRemoveButton(std::size_t index);
void EndListRow();
[[nodiscard]] bool ListAddButton();
[[nodiscard]] std::string PrettifyFieldName(std::string_view key);
template <class T>
concept Numeric = std::is_arithmetic_v<T> && !std::same_as<T, bool>;
template <class T>
concept FloatVec = std::same_as<T, glm::vec2> || std::same_as<T, glm::vec3> || std::same_as<T, glm::vec4>;
}
bool DrawField(const char* label, bool& value, const FieldOptions& options, const Scene* scene);
bool DrawField(const char* label, std::string& value, const FieldOptions& options, const Scene* scene);
bool DrawField(const char* label, glm::vec2& value, const FieldOptions& options, const Scene* scene);
bool DrawField(const char* label, glm::vec3& value, const FieldOptions& options, const Scene* scene);
bool DrawField(const char* label, glm::vec4& value, const FieldOptions& options, const Scene* scene);
bool DrawField(const char* label, ObjectRef& value, const FieldOptions& options, const Scene* scene);
bool DrawField(const char* label, std::vector<ObjectRef>& value, const FieldOptions& options, const Scene* scene);
template <reflect_detail::Numeric T>
bool DrawField(const char* label, T& value, const FieldOptions& options, const Scene*)
{
if constexpr (std::same_as<T, float>) {
if (options.angle) {
return reflect_detail::DrawAngle(label, value, options);
}
}
const T min = options.min ? static_cast<T>(*options.min) : T{};
const T max = options.max ? static_cast<T>(*options.max) : T{};
return reflect_detail::DrawScalar(label, reflect_detail::ScalarOf<T>(), &value, 1,
options.min ? &min : nullptr, options.max ? &max : nullptr, options);
}
template <class E>
requires std::is_enum_v<E>
bool DrawField(const char* label, E& value, const FieldOptions& options, const Scene*)
{
auto raw = static_cast<std::int64_t>(value);
if (!reflect_detail::DrawEnum(label, raw, options)) {
return false;
}
value = static_cast<E>(raw);
return true;
}
template <class T>
bool DrawField(const char* label, std::vector<T>& values, const FieldOptions& options, const Scene* scene)
{
reflect_detail::BeginList(label);
bool changed = false;
std::optional<std::size_t> removeIndex;
for (std::size_t i = 0; i < values.size(); ++i) {
if (reflect_detail::ListRemoveButton(i)) {
removeIndex = i;
}
changed |= DrawField("##value", values[i], options, scene);
reflect_detail::EndListRow();
}
if (removeIndex) {
values.erase(values.begin() + static_cast<std::ptrdiff_t>(*removeIndex));
changed = true;
}
if (reflect_detail::ListAddButton()) {
values.emplace_back();
changed = true;
}
return changed;
}
void ResolveField(ObjectRef& value, const ObjectRef::ObjectMap& objects, std::vector<ObjectId>& dropped);
void ResolveField(std::vector<ObjectRef>& value, const ObjectRef::ObjectMap& objects, std::vector<ObjectId>& dropped);
template <class T>
concept Reflectable = std::is_constructible_v<nlohmann::json, const T&> &&
requires(T& value, const FieldOptions& options, const Scene* scene) {
{ DrawField("", value, options, scene) } -> std::same_as<bool>;
};
template <class T>
concept HasReferences = requires(T& value, const ObjectRef::ObjectMap& objects, std::vector<ObjectId>& dropped) {
ResolveField(value, objects, dropped);
};
template <Reflectable T>
struct FieldCodec {
static void Save(const void* value, nlohmann::json& out, const FieldOptions& options)
{
const T& typed = *static_cast<const T*>(value);
if constexpr (std::is_enum_v<T>) {
const auto index = static_cast<std::size_t>(typed);
if (index < options.enumNames.size()) {
out = options.enumNames[index];
return;
}
}
out = typed;
}
static void Load(void* value, const nlohmann::json& in, const FieldOptions& options)
{
T& typed = *static_cast<T*>(value);
if constexpr (std::is_enum_v<T>) {
if (in.is_string()) {
const auto name = in.get<std::string>();
for (std::size_t i = 0; i < options.enumNames.size(); ++i) {
if (name == options.enumNames[i]) {
typed = static_cast<T>(i);
return;
}
}
throw std::runtime_error("unknown value \"" + name + "\"");
}
}
typed = in.get<T>();
if constexpr (reflect_detail::Numeric<T>) {
if (options.min && typed < static_cast<T>(*options.min)) typed = static_cast<T>(*options.min);
if (options.max && typed > static_cast<T>(*options.max)) typed = static_cast<T>(*options.max);
} else if constexpr (reflect_detail::FloatVec<T>) {
if (options.min) typed = glm::max(typed, T{static_cast<float>(*options.min)});
if (options.max) typed = glm::min(typed, T{static_cast<float>(*options.max)});
}
}
static bool Draw(const char* label, void* value, const FieldOptions& options, const Scene* scene)
{
return DrawField(label, *static_cast<T*>(value), options, scene);
}
static void Resolve(void* value, const ObjectRef::ObjectMap& objects, std::vector<ObjectId>& dropped)
{
if constexpr (HasReferences<T>) {
ResolveField(*static_cast<T*>(value), objects, dropped);
}
}
static constexpr FieldOps Ops{&Save, &Load, &Draw, HasReferences<T> ? &Resolve : nullptr};
};
class FieldVisitor {
public:
virtual ~FieldVisitor() = default;
using Commit = std::function<void(void* value)>;
template <Reflectable T>
void Field(std::string_view key, T& value, const FieldOptions& options = {})
{
Visit(key, &value, options, FieldCodec<T>::Ops, {});
}
// Loads and edits go through the setter, so it can validate and react
template <Reflectable T, std::invocable<const T&> Setter>
void Property(std::string_view key, const T& value, Setter&& setter, const FieldOptions& options = {})
{
T copy = value;
Visit(key, &copy, options, FieldCodec<T>::Ops, [&setter](void* changed) {
setter(*static_cast<const T*>(changed));
});
}
protected:
virtual void Visit(std::string_view key, void* value, const FieldOptions& options, const FieldOps& ops, const Commit& commit) = 0;
};
class JsonWriter final : public FieldVisitor {
public:
[[nodiscard]] nlohmann::json& Result() { return m_Result; }
protected:
void Visit(std::string_view key, void* value, const FieldOptions& options, const FieldOps& ops, const Commit& commit) override;
private:
nlohmann::json m_Result = nlohmann::json::object();
};
class JsonReader final : public FieldVisitor {
public:
explicit JsonReader(const nlohmann::json& data) : m_Data(data) {}
protected:
void Visit(std::string_view key, void* value, const FieldOptions& options, const FieldOps& ops, const Commit& commit) override;
private:
const nlohmann::json& m_Data;
};
class ReferenceResolver final : public FieldVisitor {
public:
ReferenceResolver(const Component& owner, const ObjectRef::ObjectMap& objects) : m_Owner(owner), m_Objects(objects) {}
protected:
void Visit(std::string_view key, void* value, const FieldOptions& options, const FieldOps& ops, const Commit& commit) override;
private:
const Component& m_Owner;
const ObjectRef::ObjectMap& m_Objects;
};
class InspectorDrawer final : public FieldVisitor {
public:
explicit InspectorDrawer(const Scene* scene) : m_Scene(scene) {}
[[nodiscard]] bool Changed() const { return m_Changed; }
protected:
void Visit(std::string_view key, void* value, const FieldOptions& options, const FieldOps& ops, const Commit& commit) override;
private:
const Scene* m_Scene;
bool m_Changed{false};
};
#endif // DESTRUM_REFLECT_H