12constexpr const char* kInspectorHostName =
"eve_inspector";
15bool sameValue(
const ReflectedValue&
a,
const ReflectedValue&
b) {
16 if (
a.kind !=
b.kind)
return false;
26std::string memberLabel(
const std::string& ownerClass,
const std::string& memberName) {
28 return memberName +
"##" + ownerClass +
"/" + memberName;
32enum class EditorKind : uint8_t {
40EditorKind editorKind(
const ReflectedMember& member,
const ReflectedValue&
value) {
41 const std::string editor = member.attrString(
"editor");
43 return editor ==
"checkbox" ? EditorKind::Checkbox : EditorKind::Checkbox;
44 if (editor ==
"combo" && !member.attrOptions(
"options").empty())
45 return EditorKind::Combo;
46 if (editor ==
"slider" &&
49 return EditorKind::Slider;
55 return EditorKind::ReadOnly;
56 return EditorKind::Input;
59std::string valueText(
const ReflectedValue&
value) {
62 return value.boolean ?
"true" :
"false";
64 return std::to_string(
value.integer);
83Runtime* Inspector::runtime()
const {
87const ssq::Object* Inspector::currentInstance()
const {
88 if (selectedInstance_ < 0 ||
89 size_t(selectedInstance_) >= instances_.size())
91 return &instances_[size_t(selectedInstance_)].object;
94int Inspector::currentClassIndex()
const {
95 const auto it = std::find(classNames_.begin(), classNames_.end(), selectedClass_);
96 return it == classNames_.end() ? 0 : int(it - classNames_.begin());
104 classNames_.push_back(
cls.name);
107 if (!selectedClass_.empty() &&
108 std::find(classNames_.begin(), classNames_.end(), selectedClass_) ==
110 selectedClass_.clear();
112 selectedInstance_ = -1;
114 if (selectedClass_.empty() && !classNames_.empty())
116 cachedMembers_.clear();
132 return host_ && host_->meta()->visible;
138 std::find(classNames_.begin(), classNames_.end(),
name) == classNames_.end())
141 selectedClass_ =
name;
143 selectedInstance_ = -1;
144 cachedMembers_.clear();
148 entry.label =
name +
" #1";
149 entry.object = instance;
150 instances_.push_back(std::move(entry));
151 selectedInstance_ = 0;
156 return selectedInstance_ >= 0;
161 if (!rt ||
object.getType() != ssq::Type::INSTANCE)
return false;
162 const std::string className = rt->
classNameOf(
object);
163 if (className.empty() ||
164 std::find(classNames_.begin(), classNames_.end(), className) ==
168 selectedClass_ = className;
171 entry.label = className +
" (live)";
172 entry.object = object;
173 instances_.push_back(std::move(entry));
174 selectedInstance_ = 0;
175 cachedMembers_.clear();
182 if (!rt || selectedClass_.empty())
return false;
186 entry.label = selectedClass_ +
" #" + std::to_string(instances_.size() + 1);
187 entry.object = instance;
188 instances_.push_back(std::move(entry));
189 selectedInstance_ = int(instances_.size()) - 1;
190 cachedMembers_.clear();
199 pickScene_ = std::move(pickScene);
203 if (index < 0 || index >=
int(instances_.size()))
return false;
204 selectedInstance_ = index;
205 cachedMembers_.clear();
211 if (
object.getType() != ssq::Type::INSTANCE)
return;
212 if (selectedInstance_ >= 0 &&
213 size_t(selectedInstance_) < instances_.size()) {
215 entry.className = selectedClass_;
216 entry.object = instances_[size_t(selectedInstance_)].object;
217 navStack_.push_back(std::move(entry));
219 selectedClass_ = className;
221 InstanceEntry current;
222 current.label = className +
" (nested)";
223 current.object = object;
224 instances_.push_back(std::move(current));
225 selectedInstance_ = 0;
226 cachedMembers_.clear();
231 if (navStack_.empty())
return;
232 NestedEntry previous = std::move(navStack_.back());
233 navStack_.pop_back();
234 selectedClass_ = previous.className;
236 InstanceEntry current;
237 current.label = previous.className +
" #1";
238 current.object = previous.object;
239 instances_.push_back(std::move(current));
240 selectedInstance_ = 0;
241 cachedMembers_.clear();
247 if (!rt || selectedInstance_ < 0 ||
248 size_t(selectedInstance_) >= instances_.size())
255 if (member.name ==
name) {
256 member.value = std::move(
value);
262WidgetDesc Inspector::propertyWidget(
const std::string& ownerClass,
263 const ReflectedMember& member,
264 const ReflectedValue&
value,
265 const ssq::Object& instance) {
266 const std::string
id =
"prop_" + member.name;
267 const std::string label = memberLabel(ownerClass, member.name);
268 switch (editorKind(member,
value)) {
269 case EditorKind::Checkbox:
271 [
this,
name = member.name](
bool v) {
273 out.kind = ReflectedValueKind::Bool;
275 writeProperty(name, std::move(out));
277 case EditorKind::Slider: {
278 const float minV = member.attrFloat(
"min", 0.f);
279 const float maxV = member.attrFloat(
"max", 1.f);
281 ? float(
value.integer)
282 : float(
value.floating);
283 return slider(label, cur, minV, maxV,
id,
284 [
this,
name = member.name](
float v) {
286 out.kind = ReflectedValueKind::Float;
288 writeProperty(name, std::move(out));
291 case EditorKind::Combo: {
292 const std::vector<std::string> options = member.attrOptions(
"options");
294 const std::string current =
297 const auto it = std::find(options.begin(), options.end(), current);
298 if (it != options.end()) index = int(it - options.begin());
299 return combo(label, options, index,
id,
300 [
this,
name = member.name,
kind =
value.kind, options](
int i) {
302 if (i < 0 || i >= int(options.size())) return;
303 const std::string& option = options[size_t(i)];
304 if (kind == ReflectedValueKind::Integer) {
305 out.kind = ReflectedValueKind::Integer;
306 out.integer = std::strtoll(option.c_str(), nullptr, 10);
309 out.floating = std::strtod(option.c_str(),
nullptr);
314 writeProperty(
name, std::move(out));
317 case EditorKind::Input:
319 [
this,
name = member.name](
const std::string&
text) {
321 out.kind = ReflectedValueKind::String;
323 writeProperty(name, std::move(out));
325 case EditorKind::ReadOnly: {
327 return arrayWidget(ownerClass, member, instance);
329 return tableWidget(ownerClass, member, instance);
331 const std::string openId =
"open_" + ownerClass +
"_" + member.name;
332 return button(
"open " + member.name +
"##" + openId, openId,
333 [
this, memberName = member.name]() {
334 Runtime* rt = runtime();
336 const ssq::Object* inst = currentInstance();
338 const ssq::Object nested =
339 rt->readObjectProperty(*inst, memberName);
340 if (nested.getType() != ssq::Type::INSTANCE) return;
341 const std::string nestedClass =
342 rt->classNameOf(nested);
343 if (nestedClass.empty()) return;
344 openNested(nestedClass, nested);
348 switch (
value.kind) {
353 return text(member.name +
" = " + shown,
id);
356 return text(member.name,
id);
359WidgetDesc Inspector::arrayWidget(
const std::string& ownerClass,
360 const ReflectedMember& member,
361 const ssq::Object& instance) {
362 Runtime* rt = runtime();
363 if (!rt)
return text(member.name +
" = array",
"arr_" + ownerClass +
"_" + member.name);
364 const size_t size = rt->arraySize(instance, member.name);
365 const std::string base =
"arr_" + ownerClass +
"_" + member.name;
366 std::vector<WidgetDesc> rows;
367 for (
size_t i = 0; i < size; ++i) {
368 const ReflectedValue
value = rt->arrayGet(instance, member.name, i);
369 const std::string elementId = base +
"_" + std::to_string(i);
370 const std::string elementLabel =
371 member.name +
"[" + std::to_string(i) +
"]##" + elementId;
375 [
this,
name = member.name, i](
bool v) {
377 out.kind = ReflectedValueKind::Bool;
379 if (Runtime* rt = runtime()) {
380 if (const ssq::Object* inst = currentInstance())
381 rt->arraySet(*inst, name, i, out);
389 cell =
text(member.name +
"[" + std::to_string(i) +
"] = element",
393 [
this,
name = member.name, i](
394 const std::string& text) {
396 out.kind = ReflectedValueKind::String;
398 if (Runtime* rt = runtime()) {
399 if (const ssq::Object* inst = currentInstance())
400 rt->arraySet(*inst, name, i, out);
406 button(
"x##" + elementId +
"_del", elementId +
"_del",
407 [
this,
name = member.name, i]() {
408 if (Runtime* rt = runtime()) {
409 if (const ssq::Object* inst = currentInstance())
410 rt->arrayRemove(*inst, name, i);
414 elementId +
"_row"));
417 row({
button(
"+##" + base +
"_add", base +
"_add",
418 [
this,
name = member.name]() {
419 if (Runtime* rt = runtime()) {
420 if (const ssq::Object* inst = currentInstance()) {
422 out.kind = ReflectedValueKind::String;
423 rt->arrayAppend(*inst, name, out);
430 member.name +
" (array[" + std::to_string(size) +
"])##" + base,
431 std::move(rows), base,
false);
434WidgetDesc Inspector::tableWidget(
const std::string& ownerClass,
435 const ReflectedMember& member,
436 const ssq::Object& instance) {
437 Runtime* rt = runtime();
438 if (!rt)
return text(member.name +
" = table",
"tbl_" + ownerClass +
"_" + member.name);
439 const std::string base =
"tbl_" + ownerClass +
"_" + member.name;
440 const std::vector<std::string> keys = rt->tableKeys(instance, member.name);
441 std::vector<WidgetDesc> rows;
442 for (
const std::string& key : keys) {
443 const ReflectedValue
value = rt->tableGet(instance, member.name, key);
444 const std::string elementId = base +
"_" + key;
445 const std::string elementLabel = key +
"##" + elementId;
449 [
this,
name = member.name, key](
bool v) {
451 out.kind = ReflectedValueKind::Bool;
453 if (Runtime* rt = runtime()) {
454 if (const ssq::Object* inst = currentInstance())
455 rt->tableSet(*inst, name, key, out);
463 cell =
text(key +
" = element", elementId);
466 [
this,
name = member.name, key](
467 const std::string& text) {
469 out.kind = ReflectedValueKind::String;
471 if (Runtime* rt = runtime()) {
472 if (const ssq::Object* inst = currentInstance())
473 rt->tableSet(*inst, name, key, out);
479 button(
"x##" + elementId +
"_del", elementId +
"_del",
480 [
this,
name = member.name, key]() {
481 if (Runtime* rt = runtime()) {
482 if (const ssq::Object* inst = currentInstance())
483 rt->tableRemove(*inst, name, key);
487 elementId +
"_row"));
490 {
button(
"+##" + base +
"_add", base +
"_add",
491 [
this,
name = member.name, keys]() {
492 if (Runtime* rt = runtime()) {
493 if (const ssq::Object* inst = currentInstance()) {
494 std::string key =
"key" + std::to_string(keys.size());
496 while (std::find(keys.begin(), keys.end(), key) !=
498 key =
"key" + std::to_string(keys.size() + (++suffix));
500 out.kind = ReflectedValueKind::String;
501 rt->tableSet(*inst, name, key, out);
507 return collapsingHeader(member.name +
" (table)##" + base, std::move(rows), base,
513 if (classNames_.empty()) {
515 "No reflected script classes. Run a script that defines classes, "
516 "then call ui.inspectRefresh().",
519 std::vector<WidgetDesc> toolbar = {
520 text(
"Class",
"lbl_class"),
522 combo(
"##class", classNames_, currentClassIndex(),
"class",
524 if (index >= 0 && index <
int(classNames_.size()))
525 selectClass(classNames_[
size_t(index)]);
530 button(
"Pick##inspector_pick",
"inspector_pick",
532 if (!pickScene_)
return;
533 const ssq::Object picked = pickScene_();
534 if (picked.getType() == ssq::Type::INSTANCE)
535 inspectObject(picked);
538 if (!navStack_.empty()) {
539 toolbar.insert(toolbar.begin(),
540 button(
"<- Back##inspector_back",
"inspector_back",
541 [
this]() { back(); }));
547 std::vector<std::string> labels;
548 labels.reserve(instances_.size());
549 for (
const InstanceEntry& entry : instances_) labels.push_back(entry.label);
552 text(
"Instance",
"lbl_instance"),
553 spacer(
"instance_spacer"),
554 combo(
"##instance", labels, selectedInstance_,
"instance",
555 [
this](
int index) { selectInstance(index); }),
556 button(
"+",
"add_instance", [
this]() { addInstance(); }),
562 if (rt && selectedInstance_ >= 0 &&
563 size_t(selectedInstance_) < instances_.size()) {
564 const ssq::Object& instance =
565 instances_[size_t(selectedInstance_)].object;
568 std::vector<std::string> chain;
569 std::string current = selectedClass_;
570 while (!current.empty()) {
571 chain.push_back(current);
573 if (!
cls ||
cls->base.empty())
break;
576 for (
const std::string& className : chain) {
579 std::vector<WidgetDesc> props;
581 if (member.method)
continue;
582 props.push_back(propertyWidget(
583 className, member, rt->
readProperty(instance, member.name),
586 const bool own = className == selectedClass_;
587 const std::string headerLabel =
588 className + (own ?
"" :
" (base)") +
"##cls_" + className;
591 text(className +
" (no editable properties)",
"cls_" + className));
594 headerLabel, std::move(props),
"cls_" + className, own));
598 children.push_back(
text(
"Instance creation failed for " + selectedClass_,
605void Inspector::rebuildHost() {
606 if (!host_ || !host_->meta()->visible)
return;
607 host_->setTreeReconcile(build());
610void Inspector::sync() {
611 if (!host_ || !host_->meta()->visible)
return;
613 if (!rt || selectedInstance_ < 0 ||
614 size_t(selectedInstance_) >= instances_.size())
616 const ssq::Object& instance = instances_[size_t(selectedInstance_)].object;
618 if (cachedMembers_.empty())
622 if (sameValue(live, cached.value))
continue;
624 const std::string
id =
"prop_" + cached.name;
625 switch (editorKind(cached, cached.value)) {
626 case EditorKind::Checkbox:
627 host_->setCheckedById(
id, live.
asBool());
629 case EditorKind::Slider:
635 case EditorKind::Combo: {
636 const std::vector<std::string> options =
637 cached.attrOptions(
"options");
638 const std::string current =
641 const auto it = std::find(options.begin(), options.end(), current);
642 host_->setValueById(
id,
643 it == options.end() ? 0.f
644 : float(it - options.begin()));
647 case EditorKind::Input:
648 host_->setValueTextById(
id, valueText(live));
650 case EditorKind::ReadOnly:
static Runtime * runtime()
Active runtime associated with the last expose() call, or nullptr.
std::vector< ReflectedMember > reflectInstance(const ssq::Object &instance) const
Inspects a live instance: own + inherited members with current values.
ssq::Object createInstance(const std::string &name, const std::string &source={})
Creates a live instance of a script class (default constructor).
const ReflectedClass * reflectedClass(const std::string &name) const noexcept
Reflected class by name, or nullptr.
bool writeProperty(const ssq::Object &instance, const std::string &name, const ReflectedValue &value) const
Writes one property of a live instance.
std::string classNameOf(const ssq::Object &instance) const
Name of the script class of a live instance ("" when unknown).
ReflectedValue readProperty(const ssq::Object &instance, const std::string &name) const
Reads one property of a live instance.
bool addInstance()
Creates another instance of the selected class and selects it.
void setPickScene(std::function< ssq::Object()> pickScene)
Registers the scene-pick source used by the Pick button.
void openNested(const std::string &className, const ssq::Object &object)
Navigates into a nested script instance (reference editing).
bool inspectObject(const ssq::Object &object)
Inspects a caller-provided live script instance (the game model).
void refresh()
Re-scans the active Runtime's reflected classes.
bool isOpen() const
True while the inspector host is mounted and visible.
void open()
Mounts (or updates) the inspector host on the UI ECS world.
void back()
Returns to the previously inspected instance.
bool selectClass(const std::string &name)
Selects a class, auto-creating its first instance.
void close()
Hides the inspector host.
bool selectInstance(int index)
Selects an instance by index; false when out of range.
static UIHost * createHost(const std::string &name="")
void setTree(WidgetDesc root)
Full replace.
void setVisible(bool v)
Host visibility / layer / modality.
WidgetDesc spacer(std::string id, float grow)
Flexible empty space; default flexGrow=1 so it absorbs free space in a Flex parent.
WidgetDesc slider(std::string label, float value, float minV, float maxV, std::string id, std::function< void(float)> onValue)
Horizontal slider; fires onValue.
WidgetDesc text(std::string content, std::string id)
Static text label.
WidgetDesc checkbox(std::string label, bool checked, std::string id, std::function< void(bool)> onToggle)
Checkbox with a label; fires onToggle.
WidgetDesc separator(std::string id)
Horizontal separator line.
WidgetDesc combo(std::string label, std::vector< std::string > options, int selected, std::string id, std::function< void(int)> onValue)
WidgetDesc inputText(std::string label, std::string value, std::string id, std::function< void(const std::string &)> onChange)
Editable text field; fires onTextChange.
WidgetDesc window(std::string title, std::vector< WidgetDesc > children, std::string id)
Top-level window widget with a title bar.
WidgetDesc collapsingHeader(std::string label, std::vector< WidgetDesc > children, std::string id, bool defaultOpen)
Collapsible header containing child widgets.
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.
std::string reflectedFloatString(double value)
Shortest round-trip float formatting (portable).
@ Array
OT_ARRAY (not yet editable).
@ Table
OT_TABLE (not yet editable).
@ Other
Any other slot kind.
@ None
Missing / null slot.
@ Instance
Nested script instance (not yet editable).
Typed snapshot of one live instance property.
bool asBool() const noexcept