16using property_access::PropertyDescriptor;
20std::string sanitize(std::string
value) {
21 for (
char &character :
value)
22 if (character ==
' ' || character ==
'.' || character ==
'[' || character ==
']' ||
28std::string displayName(
const PropertyDescriptor &property) {
29 if (!property.displayName.empty())
return property.displayName;
30 const std::size_t
separator =
property.path.find_last_of(
"./");
31 return separator == std::string::npos ?
property.path :
property.path.substr(
separator + 1);
34std::string valueText(
const Value &
value) {
35 if (
const auto *
text =
value.getIf<std::string>())
return *
text;
36 if (
const auto *
boolean =
value.getIf<
bool>())
return *
boolean ?
"true" :
"false";
37 if (
const auto *integer =
value.getIf<std::int64_t>())
return std::to_string(*integer);
40 return std::to_string(array->size()) +
" items";
42 return std::to_string(
object->size()) +
" fields";
46double numericValue(
const Value &
value) {
48 if (
const auto *integer =
value.getIf<std::int64_t>())
return static_cast<double>(*integer);
52bool parseNumber(
const std::string &
text,
bool integer, Value &out) {
54 std::int64_t parsed = 0;
55 const auto converted = std::from_chars(
text.data(),
text.data() +
text.size(), parsed);
56 if (converted.ec != std::errc() || converted.ptr !=
text.data() +
text.size())
return false;
62 const double parsed = std::strtod(
text.c_str(), &
end);
63 if (
text.empty() || errno == ERANGE ||
end !=
text.c_str() +
text.size())
return false;
81 case PropertyKind::Integer:
86 case PropertyKind::String:
87 case PropertyKind::AssetRef:
88 case PropertyKind::ObjectRef:
94WidgetDesc scalarElementEditor(
const std::string &elementId,
const std::string &elementLabel,
95 const Value &
element, std::function<
void(Value)> writeElement) {
96 if (
const auto *
boolean =
element.getIf<
bool>()) {
97 return checkbox(elementLabel, *
boolean, elementId,
98 [writeElement = std::move(writeElement)](
bool next) { writeElement(
Value(next)); });
102 [writeElement = std::move(writeElement),
103 integer =
element.getIf<std::int64_t>() !=
nullptr](
const std::string &next) {
105 if (parseNumber(next, integer, parsed)) writeElement(std::move(parsed));
110 [writeElement = std::move(writeElement)](
const std::string &next) {
111 writeElement(
Value(next));
114 return text(elementLabel +
" = " + valueText(
element), elementId);
117WidgetDesc makeCompositeEditor(property_access::IPropertyAccess &
model,
const PropertyViewOptions &
options,
118 const PropertyDescriptor &property,
const Value &
value,
119 const std::vector<std::string> &labels) {
123 children.push_back(
text(displayName(property),
id +
"_label"));
125 const std::string componentId =
id +
"_" + labels[
index];
126 const std::string
path =
property.path;
127 const std::size_t component =
index;
128 const bool useSlider =
property.numeric.minimum &&
property.numeric.maximum;
131 labels[
index],
static_cast<float>(numericValue(items[
index])),
132 static_cast<float>(*property.numeric.minimum),
static_cast<float>(*property.numeric.maximum),
133 componentId, [&
model,
path, component,
count = labels.size()](
float next) {
134 const std::optional<Value> current = model.read(path);
135 Value::Array updated =
136 componentArray(current ? *current : Value(Value::Array{}),
count);
142 labels[
index], valueText(items[
index]), componentId,
145 if (!parseNumber(next,
false, parsed))
return;
149 updated[component] = std::move(parsed);
157WidgetDesc makeArrayEditor(property_access::IPropertyAccess &
model,
const PropertyViewOptions &
options,
158 const PropertyDescriptor &property,
const Value &
value) {
162 std::vector<WidgetDesc>
rows;
164 const std::string elementId =
id +
"_" + std::to_string(
index);
165 const std::string elementLabel = displayName(property) +
"[" + std::to_string(
index) +
"]";
166 const std::string
path =
property.path;
167 const std::size_t elementIndex =
index;
168 WidgetDesc
cell = scalarElementEditor(
169 elementId, elementLabel, items[
index],
170 [&
model,
path, elementIndex](Value next) {
175 if (elementIndex >=
updated.size())
updated.resize(elementIndex + 1);
176 updated[elementIndex] = std::move(next);
179 const auto onStructure =
options.onStructureChange;
182 button(
"x##" + elementId +
"_del", elementId +
"_del",
183 [&
model,
path, elementIndex, onStructure]() {
188 if (elementIndex <
updated.size())
189 updated.erase(
updated.begin() +
static_cast<std::ptrdiff_t
>(elementIndex));
191 if (onStructure) onStructure();
193 elementId +
"_row"));
195 const std::string
path =
property.path;
196 const auto onStructure =
options.onStructureChange;
198 {
button(
"+##" +
id +
"_add",
id +
"_add",
204 updated.emplace_back(std::string{});
206 if (onStructure) onStructure();
209 return collapsingHeader(displayName(property) +
" (array[" + std::to_string(items.size()) +
"])##" +
id,
210 std::move(
rows),
id,
false);
213WidgetDesc makeMapEditor(property_access::IPropertyAccess &
model,
const PropertyViewOptions &
options,
214 const PropertyDescriptor &property,
const Value &
value,
bool allowMutate) {
218 std::vector<WidgetDesc>
rows;
219 for (
const auto &[
key, entry] :
fields) {
220 const std::string elementId =
id +
"_" + sanitize(
key);
221 const std::string
path =
property.path;
222 const std::string fieldKey =
key;
223 WidgetDesc
cell = scalarElementEditor(
224 elementId,
key, entry, [&
model,
path, fieldKey](Value next) {
229 updated[fieldKey] = std::move(next);
233 const auto onStructure =
options.onStructureChange;
236 button(
"x##" + elementId +
"_del", elementId +
"_del",
237 [&
model,
path, fieldKey, onStructure]() {
244 if (onStructure) onStructure();
246 elementId +
"_row"));
252 const std::string
path =
property.path;
253 const auto onStructure =
options.onStructureChange;
255 {
button(
"+##" +
id +
"_add",
id +
"_add",
261 std::string
key =
"key" + std::to_string(
updated.size());
262 std::size_t suffix = 0;
264 key =
"key" + std::to_string(
updated.size() + (++suffix));
267 if (onStructure) onStructure();
271 const std::string kindLabel =
property.kind == PropertyKind::Struct ?
"struct" :
"map";
272 return collapsingHeader(displayName(property) +
" (" + kindLabel +
")##" +
id, std::move(
rows),
id,
false);
275WidgetDesc makePropertyField(property_access::IPropertyAccess &
model,
const PropertyViewOptions &
options,
276 const PropertyDescriptor &property,
const Value &
value) {
278 const std::string
label = displayName(property);
279 const bool readOnly =
284 if (property.kind == PropertyKind::Array || property.kind == PropertyKind::Map ||
285 property.kind == PropertyKind::Struct)
287 {
text(valueText(
value),
id +
"_summary")},
id,
false);
291 switch (property.kind) {
292 case PropertyKind::Bool: {
294 const std::string
path =
property.path;
300 case PropertyKind::Integer:
301 case PropertyKind::Number: {
302 const bool integer =
property.kind == PropertyKind::Integer;
303 const std::string
path =
property.path;
304 if (property.numeric.minimum && property.numeric.maximum) {
306 static_cast<float>(*property.numeric.minimum),
307 static_cast<float>(*property.numeric.maximum),
id,
310 model.write(path, Value(static_cast<std::int64_t>(next)));
312 model.write(path, Value(static_cast<double>(next)));
316 [&
model,
path, integer](
const std::string &next) {
318 if (parseNumber(next, integer, parsed))
324 case PropertyKind::Enum: {
325 const std::string selected = valueText(
value);
326 int selectedIndex = 0;
328 if (property.choices[
index] == selected) selectedIndex =
static_cast<int>(
index);
329 const std::string
path =
property.path;
330 const std::vector<std::string>
choices =
property.choices;
333 const int index =
static_cast<int>(
next);
339 case PropertyKind::Action: {
340 const std::string
path =
property.path;
344 case PropertyKind::String:
345 case PropertyKind::AssetRef:
346 case PropertyKind::ObjectRef:
347 case PropertyKind::Auto: {
348 const std::string
path =
property.path;
350 [&
model,
path](
const std::string &next) {
355 case PropertyKind::Color:
356 result = makeCompositeEditor(
model,
options, property,
value, {
"r",
"g",
"b",
"a"});
358 case PropertyKind::Vec2:
361 case PropertyKind::Vec3:
364 case PropertyKind::Vec4:
365 result = makeCompositeEditor(
model,
options, property,
value, {
"x",
"y",
"z",
"w"});
367 case PropertyKind::Array:
370 case PropertyKind::Map:
373 case PropertyKind::Struct:
376 case PropertyKind::ReadOnlyText:
381 result.tooltip =
property.description;
382 result.accessibilityRole = accessibilityRole(property.kind, readOnly);
383 result.accessibilityName =
label;
384 result.accessibilityDescription =
property.description;
392bool visible(
const PropertyDescriptor &property,
const PropertyViewOptions &
options) {
396 property.kind == PropertyKind::ReadOnlyText))
401void syncComposite(UIHost &
host,
const std::string &
id,
const Value &
value,
402 const std::vector<std::string> &labels,
const PropertyDescriptor &property) {
405 const std::string componentId =
id +
"_" + labels[
index];
406 if (property.numeric.minimum && property.numeric.maximum)
407 host.setValueById(componentId,
static_cast<float>(numericValue(items[
index])));
409 host.setValueTextById(componentId, valueText(items[
index]));
421 auto property =
model.schema().find(
path);
423 if (!property || !
value)
436 for (
const PropertyDescriptor &property :
model.schema().properties) {
438 const std::optional<Value>
value =
model.read(property.path);
439 if (!
value)
continue;
440 if (
options.groupCategories && !property.category.empty() &&
property.category !=
category) {
450 for (
const PropertyDescriptor &property :
model.schema().properties) {
451 const std::optional<Value>
value =
model.read(property.path);
452 if (!
value)
continue;
454 const bool readOnly = property_access::hasFlag(property.flags, PropertyFlag::ReadOnly) ||
455 property.kind == PropertyKind::ReadOnlyText;
457 host.setTextById(
id, displayName(property) +
": " + valueText(*
value));
460 switch (property.kind) {
461 case PropertyKind::Bool:
462 if (
const auto *
boolean =
value->getIf<
bool>())
host.setCheckedById(
id, *
boolean);
464 case PropertyKind::Enum: {
465 const std::string selected = valueText(*
value);
466 const auto found = std::find(property.choices.begin(), property.choices.end(), selected);
467 host.setValueById(
id,
found == property.choices.end()
469 :
static_cast<float>(
found -
property.choices.begin()));
472 case PropertyKind::Integer:
473 case PropertyKind::Number:
474 if (property.numeric.minimum && property.numeric.maximum)
475 host.setValueById(
id,
static_cast<float>(numericValue(*
value)));
477 host.setValueTextById(
id, valueText(*
value));
479 case PropertyKind::Color:
480 syncComposite(
host,
id, *
value, {
"r",
"g",
"b",
"a"}, property);
482 case PropertyKind::Vec2:
483 syncComposite(
host,
id, *
value, {
"x",
"y"}, property);
485 case PropertyKind::Vec3:
486 syncComposite(
host,
id, *
value, {
"x",
"y",
"z"}, property);
488 case PropertyKind::Vec4:
489 syncComposite(
host,
id, *
value, {
"x",
"y",
"z",
"w"}, property);
491 case PropertyKind::Array:
494 const std::string elementId =
id +
"_" + std::to_string(
index);
495 if (
const auto *
boolean = (*items)[
index].getIf<bool>())
496 host.setCheckedById(elementId, *
boolean);
498 host.setValueTextById(elementId, valueText((*items)[
index]));
502 case PropertyKind::Map:
503 case PropertyKind::Struct:
505 for (
const auto &[
key, entry] : *
fields) {
506 const std::string elementId =
id +
"_" + sanitize(
key);
507 if (
const auto *
boolean = entry.getIf<
bool>())
508 host.setCheckedById(elementId, *
boolean);
510 host.setValueTextById(elementId, valueText(entry));
514 case PropertyKind::String:
515 case PropertyKind::AssetRef:
516 case PropertyKind::ObjectRef:
517 case PropertyKind::Auto:
518 if (
const auto *textValue =
value->getIf<std::string>())
519 host.setValueTextById(
id, *textValue);
521 case PropertyKind::Action:
522 case PropertyKind::ReadOnlyText:
550void PropertyComponent::observe() {
std::set< std::string > choices
std::array< std::uint64_t, kPixelChunkSize *kPixelChunkSize > updated
std::vector< std::string > fields
const SquirrelValueOptions & options
void dispose() noexcept
Cancels this registration; safe to call repeatedly.
The canonical owning dynamic value used by data-facing protocols.
std::map< std::string, Value > Object
std::vector< Value > Array
Renderer- and transaction-independent MVVM property surface.
virtual Subscription subscribe(ChangeCallback callback)=0
Observe changes until the returned token is destroyed.
void markDirty()
Mark dirty.
property_access::IPropertyAccess * model() const
Return the currently bound property model, or null when unbound.
void bind(property_access::IPropertyAccess *model)
Replace the model and reconnect change observation.
void setOptions(PropertyViewOptions options)
Replace generation policy and mark the component dirty.
WidgetDesc build() override
Build the current schema-driven tree.
ECS mount point for one UI panel/screen. Subclass to attach UI to game entities, e....
std::variant< std::monostate, std::int64_t, double, std::string, bool > Value
constexpr HexDirection next(HexDirection d) noexcept
The next direction clockwise (NW wraps to NE).
constexpr bool hasFlag(PropertyFlag value, PropertyFlag flag)
True when flag.
PropertyFlag
Cross-host property visibility and editing flags.
PropertyKind
Semantic property kind used by renderer-independent presenters.
int64_t integer(const RuntimeTensor &v, size_t i=0)
Integer.
WidgetDesc collapsingHeader(std::string label, std::vector< WidgetDesc > children, std::string id, bool defaultOpen)
Collapsible header containing child widgets.
WidgetDesc buildPropertyView(property_access::IPropertyAccess &model, const PropertyViewOptions &options)
Generate a declarative, two-way property view from any model schema.
WidgetDesc checkbox(std::string label, bool checked, std::string id, std::function< void(bool)> onToggle)
Checkbox with a label; fires onToggle.
WidgetDesc combo(std::string label, std::vector< std::string > options, int selected, std::string id, std::function< void(float)> onValue)
Combo.
WidgetDesc slider(std::string label, float value, float minV, float maxV, std::string id, std::function< void(float)> onValue)
Horizontal slider; fires onValue.
WidgetDesc inputText(std::string label, std::string value, std::string id, std::function< void(const std::string &)> onChange)
Editable text field; fires onTextChange.
std::string propertyWidgetId(const PropertyViewOptions &options, const std::string &path)
Return the stable widget id generated for a property path.
WidgetDesc buildPropertyField(property_access::IPropertyAccess &model, const std::string &path, const PropertyViewOptions &options)
Generate one field from a model property, or a diagnostic text row.
void syncPropertyView(UIHost &host, const property_access::IPropertyAccess &model, const PropertyViewOptions &options)
Pull current model values into an existing compatible UI tree.
WidgetDesc button(std::string label, std::string id, std::function< void()> onClick)
Clickable button; fires onClick.
WidgetDesc row(std::vector< WidgetDesc > children, std::string id)
Horizontal elastic layout row.
WidgetDesc sectionHeader(std::string label, std::string id)
Non-collapsible editor section heading with an accent marker.
WidgetDesc separator(std::string id)
Horizontal separator line.
AccessibilityRole
Platform-neutral accessibility role exposed by controls.
enum EVENGINE_API_FOUNDATION Array
OT_ARRAY (editable via Runtime array APIs / ReflectedPropertyModel).
Immutable notification emitted after a model property changes.
Policy controlling schema-to-widget generation for a UI shell.