293 lines
11 KiB
C++
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, ©, 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
|