Files
EVEngine/src/modules/ui/PropertyView.cpp
T

280 lines
13 KiB
C++

#include "ui/PropertyView.h"
#include <algorithm>
#include <cerrno>
#include <charconv>
#include <cstdint>
#include <cstdlib>
#include <string_view>
#include <utility>
#include <vector>
namespace eve::ui {
namespace {
using eve::Value;
using property_access::PropertyDescriptor;
using property_access::PropertyFlag;
using property_access::PropertyKind;
std::string sanitize(std::string value) {
for (char &character : value)
if (character == ' ' || character == '.' || character == '[' || character == ']' ||
character == '/')
character = '_';
return value;
}
std::string displayName(const PropertyDescriptor &property) {
if (!property.displayName.empty()) return property.displayName;
const std::size_t separator = property.path.find_last_of("./");
return separator == std::string::npos ? property.path : property.path.substr(separator + 1);
}
std::string valueText(const Value &value) {
if (const auto *text = value.getIf<std::string>()) return *text;
if (const auto *boolean = value.getIf<bool>()) return *boolean ? "true" : "false";
if (const auto *integer = value.getIf<std::int64_t>()) return std::to_string(*integer);
if (const auto *number = value.getIf<double>()) return std::to_string(*number);
if (const auto *array = value.getIf<Value::Array>())
return std::to_string(array->size()) + " items";
if (const auto *object = value.getIf<Value::Object>())
return std::to_string(object->size()) + " fields";
return "null";
}
double numericValue(const Value &value) {
if (const auto *number = value.getIf<double>()) return *number;
if (const auto *integer = value.getIf<std::int64_t>()) return static_cast<double>(*integer);
return 0.0;
}
AccessibilityRole accessibilityRole(PropertyKind kind, bool readOnly) {
if (readOnly) return AccessibilityRole::Text;
switch (kind) {
case PropertyKind::Bool: return AccessibilityRole::Checkbox;
case PropertyKind::Integer:
case PropertyKind::Number: return AccessibilityRole::Slider;
case PropertyKind::Action: return AccessibilityRole::Button;
case PropertyKind::Enum: return AccessibilityRole::List;
case PropertyKind::String:
case PropertyKind::AssetRef:
case PropertyKind::ObjectRef:
case PropertyKind::Auto: return AccessibilityRole::TextInput;
default: return AccessibilityRole::Text;
}
}
WidgetDesc makePropertyField(property_access::IPropertyAccess &model, const PropertyViewOptions &options,
const PropertyDescriptor &property, const Value &value) {
const std::string id = propertyWidgetId(options, property.path);
const std::string label = displayName(property);
const bool readOnly =
property_access::hasFlag(property.flags, PropertyFlag::ReadOnly) || property.kind == PropertyKind::ReadOnlyText;
WidgetDesc result;
if (readOnly) {
result = text(label + ": " + valueText(value), id);
} else {
switch (property.kind) {
case PropertyKind::Bool: {
const bool current = value.getIf<bool>() ? *value.getIf<bool>() : false;
const std::string path = property.path;
result = checkbox(label, current, id, [&model, path](bool next) {
model.write(path, Value(next));
});
break;
}
case PropertyKind::Integer:
case PropertyKind::Number: {
const bool integer = property.kind == PropertyKind::Integer;
const std::string path = property.path;
if (property.numeric.minimum && property.numeric.maximum) {
result = slider(label, static_cast<float>(numericValue(value)),
static_cast<float>(*property.numeric.minimum),
static_cast<float>(*property.numeric.maximum), id,
[&model, path, integer](float next) {
if (integer)
model.write(path, Value(static_cast<std::int64_t>(next)));
else
model.write(path, Value(static_cast<double>(next)));
});
} else {
result = inputText(label, valueText(value), id,
[&model, path, integer](const std::string &next) {
if (integer) {
std::int64_t parsed = 0;
const auto converted = std::from_chars(
next.data(), next.data() + next.size(), parsed);
if (converted.ec == std::errc() &&
converted.ptr == next.data() + next.size())
model.write(path, Value(parsed));
} else {
char *end = nullptr;
errno = 0;
const double parsed = std::strtod(next.c_str(), &end);
if (!next.empty() && errno != ERANGE &&
end == next.c_str() + next.size())
model.write(path, Value(parsed));
}
});
}
break;
}
case PropertyKind::Enum: {
const std::string selected = valueText(value);
int selectedIndex = 0;
for (std::size_t index = 0; index < property.choices.size(); ++index)
if (property.choices[index] == selected) selectedIndex = static_cast<int>(index);
const std::string path = property.path;
const std::vector<std::string> choices = property.choices;
result = combo(label, choices, selectedIndex, id,
[&model, path, choices](float next) {
const int index = static_cast<int>(next);
if (index >= 0 && static_cast<std::size_t>(index) < choices.size())
model.write(path, Value(choices[static_cast<std::size_t>(index)]));
});
break;
}
case PropertyKind::Action: {
const std::string path = property.path;
result = button(label, id, [&model, path]() { model.write(path, Value(true)); });
break;
}
case PropertyKind::String:
case PropertyKind::AssetRef:
case PropertyKind::ObjectRef:
case PropertyKind::Auto: {
const std::string path = property.path;
result = inputText(label, valueText(value), id,
[&model, path](const std::string &next) {
model.write(path, Value(next));
});
break;
}
case PropertyKind::Color:
case PropertyKind::Vec2:
case PropertyKind::Vec3:
case PropertyKind::Vec4:
case PropertyKind::Struct:
case PropertyKind::Array:
case PropertyKind::Map:
case PropertyKind::ReadOnlyText:
result = text(label + ": " + valueText(value), id);
break;
}
}
result.tooltip = property.description;
result.accessibilityRole = accessibilityRole(property.kind, readOnly);
result.accessibilityName = label;
result.accessibilityDescription = property.description;
if (readOnly) {
result.focusMode = FocusMode::None;
result.mouseFilter = MouseFilter::Ignore;
}
return result;
}
bool visible(const PropertyDescriptor &property, const PropertyViewOptions &options) {
if (!options.showAdvanced && property_access::hasFlag(property.flags, PropertyFlag::Advanced)) return false;
if (!options.showEditorOnly && property_access::hasFlag(property.flags, PropertyFlag::EditorOnly)) return false;
if (!options.showReadOnly && (property_access::hasFlag(property.flags, PropertyFlag::ReadOnly) ||
property.kind == PropertyKind::ReadOnlyText))
return false;
return true;
}
} // namespace
std::string propertyWidgetId(const PropertyViewOptions &options, const std::string &path) {
return options.idPrefix + sanitize(path);
}
WidgetDesc buildPropertyField(property_access::IPropertyAccess &model, const std::string &path,
const PropertyViewOptions &options) {
auto property = model.schema().find(path);
const std::optional<Value> value = model.read(path);
if (!property || !value)
return text(path + ": unavailable", propertyWidgetId(options, path))
.withFocusMode(FocusMode::None)
.withMouseFilter(MouseFilter::Ignore)
.withAccessibility(AccessibilityRole::Text, path, "Property is unavailable");
return makePropertyField(model, options, property->get(), *value);
}
WidgetDesc buildPropertyView(property_access::IPropertyAccess &model, const PropertyViewOptions &options) {
std::vector<WidgetDesc> children;
if (!options.title.empty()) children.push_back(sectionHeader(options.title, options.idPrefix + "title"));
std::string category;
for (const PropertyDescriptor &property : model.schema().properties) {
if (!visible(property, options)) continue;
const std::optional<Value> value = model.read(property.path);
if (!value) continue;
if (options.groupCategories && !property.category.empty() && property.category != category) {
category = property.category;
children.push_back(sectionHeader(category, options.idPrefix + "category/" + sanitize(category)));
}
children.push_back(makePropertyField(model, options, property, *value));
}
return column(std::move(children), options.idPrefix + "root");
}
void syncPropertyView(UIHost &host, const property_access::IPropertyAccess &model, const PropertyViewOptions &options) {
for (const PropertyDescriptor &property : model.schema().properties) {
const std::optional<Value> value = model.read(property.path);
if (!value) continue;
const std::string id = propertyWidgetId(options, property.path);
const bool readOnly = property_access::hasFlag(property.flags, PropertyFlag::ReadOnly) ||
property.kind == PropertyKind::ReadOnlyText;
if (readOnly) {
host.setTextById(id, displayName(property) + ": " + valueText(*value));
} else if (property.kind == PropertyKind::Bool) {
if (const auto *boolean = value->getIf<bool>()) host.setCheckedById(id, *boolean);
} else if (property.kind == PropertyKind::Enum) {
const std::string selected = valueText(*value);
const auto found = std::find(property.choices.begin(), property.choices.end(), selected);
host.setValueById(id, found == property.choices.end()
? 0.f
: static_cast<float>(found - property.choices.begin()));
} else if (property.kind == PropertyKind::Integer ||
property.kind == PropertyKind::Number) {
if (property.numeric.minimum && property.numeric.maximum)
host.setValueById(id, static_cast<float>(numericValue(*value)));
else
host.setValueTextById(id, valueText(*value));
} else if (const auto *textValue = value->getIf<std::string>()) {
host.setValueTextById(id, *textValue);
}
}
}
PropertyComponent::PropertyComponent(property_access::IPropertyAccess *model, PropertyViewOptions options)
: model_(model), options_(std::move(options)) {
observe();
}
void PropertyComponent::bind(property_access::IPropertyAccess *model) {
subscription_.dispose();
model_ = model;
observe();
markDirty();
}
void PropertyComponent::setOptions(PropertyViewOptions options) {
options_ = std::move(options);
markDirty();
}
WidgetDesc PropertyComponent::build() {
if (!model_) return column({}, options_.idPrefix + "root");
return buildPropertyView(*model_, options_);
}
void PropertyComponent::observe() {
if (!model_) return;
subscription_ = model_->subscribe([this](const property_access::PropertyChange &) { markDirty(); });
}
} // namespace eve::ui