3#include "common/config.h"
5#if defined(EVENGINE_WEBGPU) && defined(__EMSCRIPTEN__)
10struct EditorHost::Impl {};
16EditorHost::EditorHost() =
default;
22 return "error: editor host unavailable on this platform";
27 return "error: editor host unavailable on this platform";
30 return "error: editor host unavailable on this platform";
36 return "error: editor host unavailable on this platform";
40 return "{\"x\":0,\"y\":0,\"width\":0,\"height\":0}";
43 return "error: editor host unavailable on this platform";
46 return "error: editor host unavailable on this platform";
49 return "error: editor host unavailable on this platform";
52 return "error: editor host unavailable on this platform";
56 return "error: editor host unavailable on this platform";
59 return "error: editor host unavailable on this platform";
62 return "{\"running\":false,\"windowOpen\":false,\"editors\":[],\"viewModels\":[]}";
80#include <Poco/Dynamic/Var.h>
81#include <Poco/Exception.h>
82#include <Poco/JSON/Array.h>
83#include <Poco/JSON/Object.h>
84#include <Poco/JSON/Parser.h>
85#include <Poco/JSON/Stringifier.h>
88#include <simplesquirrel/simplesquirrel.hpp>
108using Poco::Dynamic::Var;
109using Poco::JSON::Array;
110using Poco::JSON::Object;
111using Poco::JSON::Parser;
113std::string jsonStringify(
const Var&
v) {
114 std::ostringstream oss;
115 Poco::JSON::Stringifier::stringify(
v, oss, 0, 0);
119Object::Ptr parseObject(
const std::string& json) {
120 Poco::JSON::Parser parser;
121 return parser.parse(json).extract<Object::Ptr>();
124std::string strOf(Object::Ptr o,
const char* key,
const std::string& def = {}) {
125 if (!o || !o->has(key))
return def;
127 return o->get(key).convert<std::string>();
133int intOf(Object::Ptr o,
const char* key,
int def = 0) {
134 if (!o || !o->has(key))
return def;
136 return o->get(key).convert<
int>();
142float floatOf(Object::Ptr o,
const char* key,
float def = 0.f) {
143 if (!o || !o->has(key))
return def;
145 return static_cast<float>(o->get(key).convert<
double>());
151bool boolOf(Object::Ptr o,
const char* key,
bool def =
false) {
152 if (!o || !o->has(key))
return def;
154 return o->get(key).convert<
bool>();
160float varFloat(
const Var&
v,
float def = 0.f) {
162 return static_cast<float>(
v.convert<
double>());
168bool varBool(
const Var&
v,
bool def =
false) {
170 return v.convert<
bool>();
176std::string varString(
const Var&
v,
const std::string& def = {}) {
178 return v.convert<std::string>();
184Array::Ptr varToArray(
const Var&
v) {
186 return v.extract<Array::Ptr>();
192std::vector<float> varFloats(
const Var&
v) {
193 std::vector<float> out;
194 Array::Ptr arr = varToArray(
v);
195 if (!arr)
return out;
196 for (
size_t i = 0; i < arr->size(); ++i) {
198 out.push_back(
static_cast<float>(arr->get(i).convert<
double>()));
207std::vector<float> widgetFloatInit(
const Var&
v) {
208 std::vector<float> init = varFloats(
v);
209 if (!init.empty())
return init;
211 init.push_back(
static_cast<float>(
v.convert<
double>()));
217std::vector<std::string> varStrings(
const Var&
v) {
218 std::vector<std::string> out;
219 Array::Ptr arr = varToArray(
v);
220 if (!arr)
return out;
221 for (
size_t i = 0; i < arr->size(); ++i) out.push_back(varString(arr->get(i)));
228 switch (sq_gettype(
vm, index)) {
233 if (SQ_SUCCEEDED(sq_getbool(
vm, index, &
b)))
return Var(
bool(
b));
238 if (SQ_SUCCEEDED(sq_getinteger(
vm, index, &i)))
239 return Var(
static_cast<Poco::Int64
>(i));
244 if (SQ_SUCCEEDED(sq_getfloat(
vm, index, &
f)))
245 return Var(
static_cast<double>(
f));
249 const SQChar*
s =
nullptr;
250 if (SQ_SUCCEEDED(sq_getstring(
vm, index, &
s)) &&
s)
return Var(std::string(
s));
255 if (SQ_FAILED(sq_getstackobj(
vm, index, &obj)))
return Var();
256 sq_pushobject(
vm, obj);
258 Array::Ptr arr =
new Array();
259 while (SQ_SUCCEEDED(sq_next(
vm, -2))) {
260 arr->add(stackVar(
vm, -1));
278 sq_pushbool(
vm,
v.convert<
bool>() ? SQTrue : SQFalse);
282 sq_pushfloat(
vm,
static_cast<SQFloat
>(
v.convert<
double>()));
286 const std::string
s =
v.convert<std::string>();
287 sq_pushstring(
vm,
s.c_str(),
static_cast<SQInteger
>(
s.size()));
293 arr =
v.extract<Array::Ptr>();
300 for (
size_t i = 0; i < arr->size(); ++i) {
301 pushVar(
vm, arr->get(i));
302 if (SQ_FAILED(sq_arrayappend(
vm, -2)))
break;
313std::string runSquirrel(
HSQUIRRELVM vm,
const std::string& source,
const char*
name) {
314 const SQInteger top = sq_gettop(
vm);
315 if (SQ_FAILED(sq_compilebuffer(
vm, source.c_str(),
static_cast<SQInteger
>(source.size()),
318 return "compile failed";
320 sq_pushroottable(
vm);
321 if (SQ_FAILED(sq_call(
vm, 1, SQFalse, SQTrue))) {
323 return "runtime failed";
329void callRootFunc(
HSQUIRRELVM vm,
const std::string&
name,
const std::vector<Var>& args) {
330 const SQInteger top = sq_gettop(
vm);
331 sq_pushroottable(
vm);
332 sq_pushstring(
vm,
name.c_str(),
static_cast<SQInteger
>(
name.size()));
333 if (SQ_FAILED(sq_get(
vm, -2))) {
337 if (sq_gettype(
vm, -1) != OT_CLOSURE) {
341 sq_pushroottable(
vm);
342 for (
const auto&
a : args) pushVar(
vm,
a);
343 if (SQ_FAILED(sq_call(
vm,
static_cast<SQInteger
>(args.size()) + 1, SQFalse, SQTrue)))
344 fprintf(stderr,
"eve.host: %s() failed\n",
name.c_str());
348bool callTableFunc(
HSQUIRRELVM vm,
const ssq::Table& tbl,
const std::string&
name,
349 const std::vector<Var>& args, std::string* err) {
350 if (
name.empty())
return true;
351 const SQInteger top = sq_gettop(
vm);
353 ssq::Function
fn = tbl.findFunc(
name.c_str());
354 sq_pushobject(
vm,
fn.getRaw());
355 sq_pushobject(
vm, tbl.getRaw());
356 for (
const auto&
a : args) pushVar(
vm,
a);
357 if (SQ_FAILED(sq_call(
vm,
static_cast<SQInteger
>(args.size()) + 1, SQFalse, SQTrue))) {
358 if (err) *err =
"runtime failed: " +
name;
364 }
catch (
const ssq::NotFoundException&) {
367 }
catch (
const std::exception& e) {
368 if (err) *err = e.what();
375bool splitBindPath(
const std::string& bind, std::string& slot, std::string& nested) {
376 if (bind.rfind(
"vm.", 0) != 0)
return false;
377 std::string rest = bind.substr(3);
378 const size_t dot = rest.find(
'.');
379 if (dot == std::string::npos) {
382 }
else if (rest.find(
'.', dot + 1) == std::string::npos) {
383 slot = rest.substr(0, dot);
384 nested = rest.substr(dot + 1);
388 return !slot.empty();
391Var objectToVar(ssq::VM&
vm,
const ssq::Object& o) {
394 const SQInteger top = sq_gettop(
v);
395 sq_pushobject(
v, o.getRaw());
396 Var out = stackVar(
v, -1);
401Var readVMVar(ssq::VM&
vm,
const ssq::Table& tbl,
const std::string& bind,
bool*
ok =
nullptr) {
402 std::string slot, nested;
404 if (!splitBindPath(bind, slot, nested))
return Var();
406 ssq::Object o = tbl.find(slot.c_str());
407 if (!nested.empty()) {
408 if (o.getType() != ssq::Type::TABLE)
return Var();
409 o = o.toTable().find(nested.c_str());
412 return objectToVar(
vm, o);
418void writeVMVar(ssq::VM&
vm,
const ssq::Table& tbl,
const std::string& bind,
const Var&
value) {
419 std::string slot, nested;
420 if (!splitBindPath(bind, slot, nested))
return;
422 const SQInteger top = sq_gettop(
v);
423 if (nested.empty()) {
424 sq_pushobject(
v, tbl.getRaw());
425 sq_pushstring(
v, slot.c_str(),
static_cast<SQInteger
>(slot.size()));
427 sq_newslot(
v, -3,
false);
430 ssq::Table t2 = tbl.find(slot.c_str()).toTable();
431 sq_pushobject(
v, t2.getRaw());
432 sq_pushstring(
v, nested.c_str(),
static_cast<SQInteger
>(nested.size()));
434 sq_newslot(
v, -3,
false);
463void forEachWidget(Object::Ptr
parent,
const std::function<
void(Object::Ptr)>&
fn) {
465 Array::Ptr kids =
parent->has(
"children") ?
parent->getArray(
"children") :
nullptr;
467 for (
size_t i = 0; i < kids->size(); ++i) {
468 Object::Ptr
w = kids->getObject(
static_cast<unsigned>(i));
471 forEachWidget(
w,
fn);
475Object::Ptr findWidget(Object::Ptr
parent,
const std::string&
id) {
476 if (!
parent ||
id.empty())
return nullptr;
477 Array::Ptr kids =
parent->has(
"children") ?
parent->getArray(
"children") :
nullptr;
478 if (!kids)
return nullptr;
479 for (
size_t i = 0; i < kids->size(); ++i) {
480 Object::Ptr
w = kids->getObject(
static_cast<unsigned>(i));
482 if (strOf(
w,
"id") ==
id)
return w;
483 if (Object::Ptr sub = findWidget(
w,
id))
return sub;
488std::string widgetIdLabel(Object::Ptr
w) {
489 const std::string
id = strOf(
w,
"id");
490 const std::string label = strOf(
w,
"label");
491 if (label.empty())
return "##" +
id;
492 if (
id.empty())
return label;
493 return label +
"##" +
id;
497std::string vmFuncName(
const std::string& path) {
498 if (path.rfind(
"vm.", 0) == 0)
return path.substr(3);
502void recordRect(Editor& ed,
const std::string&
id) {
503 if (
id.empty())
return;
504 ed.rectMin[
id] = ImGui::GetItemRectMin();
505 ed.rectMax[
id] = ImGui::GetItemRectMax();
508void emitEvent(Editor& ed,
const std::string&
type,
const std::string&
widget,
const Var&
value) {
512void renderChildren(EditorHost::Impl& I, Editor& ed, Array::Ptr kids,
const std::string&
layout);
513void syncVMToView(EditorHost::Impl& I, Editor& ed);
520 ssq::VM*
vm =
nullptr;
532 std::map<std::string, ssq::Table>
vms;
546EditorHost::EditorHost() =
default;
550 if (running_)
return;
551 impl_ = std::make_unique<Impl>();
553 impl_->allowWindow = allowWindow;
554 allowWindow_ = allowWindow;
555 gameRoot_ =
gameRoot.empty() ? std::filesystem::current_path().string() :
gameRoot;
556 impl_->rootDir = gameRoot_;
562 if (!running_)
return;
566 exitRequested_ =
false;
570 if (!impl_)
return "error: host not started";
572 if (!I.allowWindow)
return "error: window creation disabled";
574 if (!I.win) I.win = ModuleManager::requireInstance<eve::window::Window>(
"Window");
575 if (!I.gfx) I.gfx = ModuleManager::requireInstance<eve::graphics::Graphics>(
"Graphics");
581 if (!I.win->setWindowSettings(
s))
return "error: setWindowSettings failed";
582 I.win->setWindowTitle(
title.empty() ?
"EVEngine AI Host" :
title);
583 if (!I.ui) I.ui = ModuleManager::requireInstance<eve::ui::UI>(
"UI");
585 I.windowTitle =
title;
590 if (I.gfx) I.gfx->setScreenReadbackEnabled(
true);
592 }
catch (
const std::exception& e) {
593 return std::string(
"error: ") + e.what();
598 if (!impl_)
return "ok";
600 if (I.windowOpen && I.win) {
605 if (I.ui) I.ui->shutdownBackend();
606 I.windowOpen =
false;
612 return impl_ && impl_->windowOpen;
616 if (!impl_ || !impl_->windowOpen)
return "{\"open\":false}";
617 Object::Ptr o =
new Object();
618 o->set(
"open",
true);
619 o->set(
"title", impl_->windowTitle);
621 o->set(
"width", impl_->win->getWidth());
622 o->set(
"height", impl_->win->getHeight());
624 return jsonStringify(Var(o));
628 if (!impl_)
return "error: host not started";
631 Object::Ptr root = parseObject(json);
632 const std::string
id = strOf(root,
"id");
633 if (
id.empty())
return "error: editor requires id";
636 ed.title = strOf(root,
"title",
id);
637 ed.vmName = strOf(root,
"vm");
639 ed.x =
static_cast<float>(intOf(root,
"x", 0));
640 ed.y =
static_cast<float>(intOf(root,
"y", 0));
641 ed.width =
static_cast<float>(intOf(root,
"width", 0));
642 ed.height =
static_cast<float>(intOf(root,
"height", 0));
643 ed.resizable = boolOf(root,
"resizable",
true);
644 ed.collapsible = boolOf(root,
"collapsible",
true);
645 ed.layout = strOf(root,
"layout",
"vertical");
646 if (root->has(
"theme")) ed.theme = root->getObject(
"theme");
647 forEachWidget(root, [&](Object::Ptr
w) {
648 const std::string wid = strOf(
w,
"id");
649 if (wid.empty())
return;
650 if (
w->has(
"value")) ed.values[wid] =
w->get(
"value");
652 I.editors[
id] = std::move(ed);
653 if (!I.windowOpen && I.allowWindow) {
654 std::string err =
openWindow(ed.title +
" - EVEngine AI Host", 1280, 800);
655 if (err.rfind(
"error:", 0) == 0)
return err;
658 }
catch (
const std::exception& e) {
659 return std::string(
"error: ") + e.what();
664 if (!impl_)
return "error: host not started";
665 auto it = impl_->editors.find(
id);
666 if (it == impl_->editors.end())
return "error: editor not found: " +
id;
667 impl_->editors.erase(it);
672 if (!impl_)
return "{\"editors\":[]}";
673 Array::Ptr arr =
new Array();
674 for (
const auto& [k, ed] : impl_->editors) {
675 Object::Ptr o =
new Object();
677 o->set(
"title", ed.title);
678 o->set(
"vm", ed.vmName);
681 Object::Ptr root =
new Object();
682 root->set(
"editors", arr);
683 return jsonStringify(Var(root));
688Poco::JSON::Object::Ptr editorStateObject(
const Editor& ed) {
689 Object::Ptr o =
new Object();
691 o->set(
"title", ed.title);
692 o->set(
"vm", ed.vmName);
693 Object::Ptr vals =
new Object();
694 for (
const auto& [k,
v] : ed.
values) vals->set(k,
v);
695 o->set(
"values", vals);
696 Array::Ptr evs =
new Array();
697 for (
const auto& e : ed.
events) {
698 Object::Ptr eo =
new Object();
699 eo->set(
"editor", ed.id);
700 eo->set(
"widget", e.widget);
701 eo->set(
"type", e.type);
702 if (!e.value.isEmpty()) eo->set(
"value", e.value);
705 o->set(
"events", evs);
712 if (!impl_)
return "{\"editors\":[]}";
714 Array::Ptr arr =
new Array();
716 auto it = impl_->editors.find(
id);
717 if (it == impl_->editors.end())
return "error: editor not found: " +
id;
718 arr->add(editorStateObject(it->second));
720 for (
const auto& [k, ed] : impl_->editors) arr->add(editorStateObject(ed));
722 Object::Ptr root =
new Object();
723 root->set(
"editors", arr);
724 return jsonStringify(Var(root));
729 for (
auto& [k, ed] : impl_->editors) syncVMToView(*impl_, ed);
735 if (!I.vm || ed.vmName.empty())
return;
736 auto it = I.vms.find(ed.vmName);
737 if (it == I.vms.end())
return;
738 Object::Ptr
w = findWidget(ed.view, widgetId);
740 const std::string bind = strOf(
w,
"bind");
741 if (!bind.empty()) writeVMVar(*I.vm, it->second, bind,
value);
742 const std::string onChange = strOf(
w,
"onChange");
743 if (!onChange.empty()) {
745 callTableFunc(I.vm->getHandle(), it->second, vmFuncName(onChange),
746 {Var(widgetId),
value}, &err);
748 fprintf(stderr,
"eve.host: onChange %s: %s\n", onChange.c_str(), err.c_str());
752void setWidgetValue(EditorHost::Impl& I, Editor& ed,
const std::string& widgetId,
754 ed.values[widgetId] =
value;
755 emitEvent(ed,
"change", widgetId,
value);
756 pushViewToVM(I, ed, widgetId,
value);
759void resolveOneWay(EditorHost::Impl& I, Editor& ed, Object::Ptr
w) {
760 if (!I.vm || ed.vmName.empty())
return;
761 auto it = I.vms.find(ed.vmName);
762 if (it == I.vms.end())
return;
763 const char* oneWay[] = {
"bind:label",
"bind:visible",
"bind:enabled"};
764 for (
const char* key : oneWay) {
765 const std::string bind = strOf(
w, key);
766 if (bind.empty())
continue;
767 const Var
v = readVMVar(*I.vm, it->second, bind);
768 if (
v.isEmpty())
continue;
769 const std::string k(key);
770 if (k ==
"bind:label")
w->set(
"label", varString(
v));
771 else if (k ==
"bind:visible")
w->set(
"visible", varBool(
v,
true));
772 else if (k ==
"bind:enabled")
w->set(
"enabled", varBool(
v,
true));
774 const std::string optBind = strOf(
w,
"bind:options");
775 if (!optBind.empty()) {
776 const Var
v = readVMVar(*I.vm, it->second, optBind);
778 Array::Ptr opts =
new Array();
779 for (
const auto&
s : varStrings(
v)) opts->add(
s);
780 w->set(
"options", opts);
785void syncVMToView(EditorHost::Impl& I, Editor& ed) {
786 if (!I.vm || ed.vmName.empty())
return;
787 auto it = I.vms.find(ed.vmName);
788 if (it == I.vms.end())
return;
789 forEachWidget(ed.view, [&](Object::Ptr
w) {
790 const std::string bind = strOf(w,
"bind");
791 const std::string id = strOf(w,
"id");
792 if (bind.empty() || id.empty()) return;
793 const Var v = readVMVar(*I.vm, it->second, bind);
794 if (v.isEmpty()) return;
795 auto cur = ed.values.find(id);
796 if (cur == ed.values.end() || jsonStringify(cur->second) != jsonStringify(v))
801void renderWidget(EditorHost::Impl& I, Editor& ed, Object::Ptr
w);
803void renderChildren(EditorHost::Impl& I, Editor& ed, Array::Ptr kids,
const std::string&
layout) {
805 for (
size_t i = 0; i < kids->size(); ++i) {
806 Object::Ptr
w = kids->getObject(
static_cast<unsigned>(i));
808 resolveOneWay(I, ed,
w);
809 renderWidget(I, ed,
w);
810 if (
layout ==
"horizontal" && i + 1 < kids->size()) ImGui::SameLine();
814void renderWidget(EditorHost::Impl& I, Editor& ed, Object::Ptr
w) {
816 if (!boolOf(
w,
"visible",
true))
return;
817 const std::string
type = strOf(
w,
"type",
"label");
818 const std::string
id = strOf(
w,
"id");
819 const std::string label = strOf(
w,
"label");
820 const bool enabled = boolOf(
w,
"enabled",
true);
822 ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.5f);
825 const auto cur = [&](
const char* key,
const Var& def) -> Var {
827 auto it = ed.values.find(
id);
828 if (it != ed.values.end())
return it->second;
830 if (
w->has(key))
return w->get(key);
834 if (
type ==
"label" ||
type ==
"text") {
835 const std::string
text = strOf(
w,
"text", label);
836 if (
type ==
"text") ImGui::TextWrapped(
"%s",
text.c_str());
837 else ImGui::TextUnformatted(
text.c_str());
839 }
else if (
type ==
"separator") {
841 }
else if (
type ==
"spacer") {
842 ImGui::Dummy(ImVec2(floatOf(
w,
"width", 0.f), floatOf(
w,
"height", 8.f)));
843 }
else if (
type ==
"progress") {
844 float v = std::clamp(varFloat(cur(
"value", 0.0), 0.f), 0.f, 1.f);
845 const float pw = floatOf(
w,
"width", 0.f);
846 ImGui::ProgressBar(
v, ImVec2(pw > 0.f ? pw : -FLT_MIN, 0.f),
847 label.empty() ? nullptr : label.c_str());
849 }
else if (
type ==
"plot") {
850 const std::vector<float>
data = varFloats(cur(
"data", Var()));
852 ImGui::PlotLines(widgetIdLabel(
w).c_str(),
data.data(),
static_cast<int>(
data.size()), 0,
853 nullptr, floatOf(
w,
"min", 0.f), floatOf(
w,
"max", 1.f),
854 ImVec2(floatOf(
w,
"width", 0.f), floatOf(
w,
"height", 80.f)));
857 }
else if (
type ==
"input") {
858 std::string buf = varString(cur(
"value", strOf(
w,
"value")));
859 const bool multi = boolOf(
w,
"multiline",
false);
860 bool changed =
false;
862 changed = ImGui::InputTextMultiline(widgetIdLabel(
w).c_str(), buf.data(),
864 ImVec2(floatOf(
w,
"width", 0.f), floatOf(
w,
"height", 96.f)));
867 changed = ImGui::InputText(widgetIdLabel(
w).c_str(), buf.data(), buf.size());
869 if (
enabled && changed && !
id.empty()) {
870 buf.resize(strlen(buf.c_str()));
871 setWidgetValue(I, ed,
id, Var(buf));
874 }
else if (
type ==
"slider" ||
type ==
"slider2" ||
type ==
"slider3") {
875 const int n =
type ==
"slider" ? 1 : (
type ==
"slider2" ? 2 : 3);
876 float v[3] = {0.f, 0.f, 0.f};
877 const std::vector<float> init = widgetFloatInit(cur(
"value", Var()));
878 for (
int i = 0; i < n; ++i) v[i] = i < static_cast<int>(init.size()) ? init[i] : 0.f;
879 const float lo = floatOf(
w,
"min", 0.f), hi = floatOf(
w,
"max", 1.f);
880 const std::string fmt = strOf(
w,
"format",
"%.2f");
881 bool changed =
false;
882 if (
n == 1) changed = ImGui::SliderFloat(widgetIdLabel(
w).c_str(), &
v[0], lo, hi, fmt.c_str());
883 else if (
n == 2) changed = ImGui::SliderFloat2(widgetIdLabel(
w).c_str(),
v, lo, hi, fmt.c_str());
884 else changed = ImGui::SliderFloat3(widgetIdLabel(
w).c_str(),
v, lo, hi, fmt.c_str());
885 if (
enabled && changed && !
id.empty()) {
886 Array::Ptr arr =
new Array();
887 for (
int i = 0; i <
n; ++i) arr->add(
static_cast<double>(
v[i]));
888 setWidgetValue(I, ed,
id, Var(arr));
891 }
else if (
type ==
"color") {
892 const bool alpha = boolOf(
w,
"alpha",
false);
893 const std::vector<float> init = varFloats(cur(
"value", Var()));
894 float c[4] = {1.f, 1.f, 1.f, 1.f};
895 for (
size_t i = 0; i < std::min<size_t>(init.size(), 4); ++i)
c[i] = init[i];
896 const bool changed = alpha ? ImGui::ColorEdit4(widgetIdLabel(
w).c_str(),
c)
897 :
ImGui::ColorEdit3(widgetIdLabel(
w).c_str(),
c);
898 if (
enabled && changed && !
id.empty()) {
899 Array::Ptr arr =
new Array();
900 const int n = alpha ? 4 : 3;
901 for (
int i = 0; i <
n; ++i) arr->add(
static_cast<double>(
c[i]));
902 setWidgetValue(I, ed,
id, Var(arr));
905 }
else if (
type ==
"checkbox") {
906 bool b = varBool(cur(
"value",
false), boolOf(
w,
"value",
false));
907 if (
enabled && ImGui::Checkbox(widgetIdLabel(
w).c_str(), &
b) && !
id.empty())
908 setWidgetValue(I, ed,
id, Var(
b));
910 }
else if (
type ==
"dropdown" ||
type ==
"listbox") {
911 const std::vector<std::string> opts = varStrings(cur(
"options", Var()));
912 std::vector<const char*> items;
913 for (
const auto&
s : opts) items.push_back(
s.c_str());
914 std::string sel = varString(cur(
"value", strOf(
w,
"value")));
916 for (
size_t i = 0; i < opts.size(); ++i)
917 if (opts[i] == sel)
idx =
static_cast<int>(i);
918 bool changed =
false;
919 if (
type ==
"dropdown")
920 changed = !opts.empty() && ImGui::Combo(widgetIdLabel(
w).c_str(), &
idx, items.data(),
921 static_cast<int>(items.size()));
923 changed = !opts.empty() && ImGui::ListBox(widgetIdLabel(
w).c_str(), &
idx, items.data(),
924 static_cast<int>(items.size()),
925 intOf(
w,
"heightItems", -1));
926 if (
enabled && changed && !
id.empty() &&
idx >= 0 &&
idx <
static_cast<int>(opts.size()))
927 setWidgetValue(I, ed,
id, Var(opts[
static_cast<size_t>(
idx)]));
929 }
else if (
type ==
"button") {
930 if (
enabled && ImGui::Button(widgetIdLabel(
w).c_str(), ImVec2(floatOf(
w,
"width", 0.f), 0.f))) {
931 emitEvent(ed,
"click",
id, Var());
932 const std::string cmd = strOf(
w,
"command", strOf(
w,
"action"));
933 if (!cmd.empty() && !ed.vmName.empty() && I.vm) {
934 auto it = I.vms.find(ed.vmName);
935 if (it != I.vms.end()) {
937 callTableFunc(I.vm->getHandle(), it->second, vmFuncName(cmd),
938 {Var(ed.id), Var(
id)}, &err);
940 fprintf(stderr,
"eve.host: command %s: %s\n", cmd.c_str(), err.c_str());
945 }
else if (
type ==
"tree") {
946 const bool open = boolOf(
w,
"open",
true);
947 if (ImGui::TreeNodeEx(widgetIdLabel(
w).c_str(),
948 open ? ImGuiTreeNodeFlags_DefaultOpen : 0)) {
949 renderChildren(I, ed,
950 w->has(
"children") ?
w->getArray(
"children") : nullptr, ed.
layout);
954 }
else if (
type ==
"group") {
955 const bool border = boolOf(
w,
"border",
true);
957 if (ImGui::BeginChild(widgetIdLabel(
w).c_str(),
958 ImVec2(floatOf(
w,
"width", 0.f), floatOf(
w,
"height", 0.f)),
true)) {
959 if (!label.empty()) ImGui::TextUnformatted(label.c_str());
960 renderChildren(I, ed,
961 w->has(
"children") ?
w->getArray(
"children") : nullptr,
962 strOf(
w,
"layout",
"vertical"));
967 if (!label.empty()) ImGui::TextUnformatted(label.c_str());
968 renderChildren(I, ed,
969 w->has(
"children") ?
w->getArray(
"children") : nullptr,
970 strOf(
w,
"layout",
"vertical"));
974 }
else if (
type ==
"tabs") {
975 Array::Ptr tabs =
w->has(
"children") ?
w->getArray(
"children") :
nullptr;
976 if (tabs && ImGui::BeginTabBar((
id.empty() ?
"tabs" :
id).c_str())) {
977 for (
size_t i = 0; i < tabs->size(); ++i) {
978 Object::Ptr tab = tabs->getObject(
static_cast<unsigned>(i));
980 if (ImGui::BeginTabItem(strOf(tab,
"label",
"Tab").c_str())) {
981 renderChildren(I, ed,
982 tab->has(
"children") ? tab->getArray(
"children") : nullptr,
983 strOf(tab,
"layout",
"vertical"));
989 }
else if (
type ==
"tab") {
990 renderChildren(I, ed,
w->has(
"children") ?
w->getArray(
"children") : nullptr,
991 strOf(
w,
"layout",
"vertical"));
992 }
else if (
type ==
"table") {
993 const std::vector<std::string> cols =
994 w->has(
"columns") ? varStrings(
w->get(
"columns")) : std::vector<std::string>();
995 Array::Ptr rows =
w->has(
"rows") ?
w->getArray(
"rows") :
nullptr;
996 if (!cols.empty() && ImGui::BeginTable((
id.empty() ?
"table" :
id).c_str(),
997 static_cast<int>(cols.size()),
998 ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) {
999 for (
const auto&
c : cols)
ImGui::TableSetupColumn(
c.c_str());
1000 ImGui::TableHeadersRow();
1002 for (
size_t r = 0; r < rows->size(); ++r) {
1003 ImGui::TableNextRow();
1004 Array::Ptr cells = rows->getArray(
static_cast<unsigned>(r));
1005 if (!cells)
continue;
1006 for (
size_t c = 0;
c < cells->size(); ++
c) {
1007 ImGui::TableSetColumnIndex(
static_cast<int>(
c));
1008 ImGui::TextUnformatted(varString(cells->get(
c)).c_str());
1015 }
else if (
type ==
"viewport") {
1016 const float vw = floatOf(
w,
"width", 0.f);
1017 const float vh = floatOf(
w,
"height", 120.f);
1018 const std::vector<float> bg =
1019 w->has(
"bg") ? varFloats(
w->get(
"bg")) : std::vector<float>();
1020 const ImVec2
pos = ImGui::GetCursorScreenPos();
1021 ImVec2 size(vw > 0.f ? vw :
ImGui::GetContentRegionAvail().
x, vh);
1022 ImU32 col = IM_COL32(10, 12, 16, 255);
1024 col = IM_COL32(
static_cast<int>(bg[0] * 255.f),
static_cast<int>(bg[1] * 255.f),
1025 static_cast<int>(bg[2] * 255.f), 255);
1026 ImGui::GetWindowDrawList()->AddRectFilled(
pos, ImVec2(
pos.x + size.x,
pos.y + size.y), col);
1027 if (ImGui::BeginChild(widgetIdLabel(
w).c_str(), size,
true)) {
1028 if (!label.empty()) ImGui::TextUnformatted(label.c_str());
1033 ed.rectMin[
id] = ImGui::GetItemRectMin();
1034 ed.rectMax[
id] = ImGui::GetItemRectMax();
1038 const std::string tip = strOf(
w,
"tooltip");
1039 if (!tip.empty() && ImGui::IsItemHovered()) ImGui::SetTooltip(
"%s", tip.c_str());
1041 ImGui::PopStyleVar();
1045void renderEditor(EditorHost::Impl& I, Editor& ed) {
1046 if (ed.removed)
return;
1048 ImGui::SetNextWindowSize(ImVec2(ed.width, ed.height), ImGuiCond_FirstUseEver);
1049 if (ed.x != 0.f || ed.y != 0.f)
1050 ImGui::SetNextWindowPos(ImVec2(ed.x, ed.y), ImGuiCond_FirstUseEver);
1052 ImVec4 accent(0.35f, 0.60f, 1.00f, 1.f), bg(0.10f, 0.11f, 0.13f, 1.f);
1053 ImVec4 panel(0.14f, 0.15f, 0.18f, 1.f),
text(0.92f, 0.93f, 0.95f, 1.f);
1054 float radius = 4.f, fontScale = 1.f;
1056 if (ed.theme->has(
"accent")) {
1057 const auto c = varFloats(ed.theme->get(
"accent"));
1058 if (
c.size() >= 3) accent = ImVec4(
c[0],
c[1],
c[2], 1.f);
1060 if (ed.theme->has(
"bg")) {
1061 const auto c = varFloats(ed.theme->get(
"bg"));
1062 if (
c.size() >= 3) bg = ImVec4(
c[0],
c[1],
c[2], 1.f);
1064 if (ed.theme->has(
"panel")) {
1065 const auto c = varFloats(ed.theme->get(
"panel"));
1066 if (
c.size() >= 3) panel = ImVec4(
c[0],
c[1],
c[2], 1.f);
1068 if (ed.theme->has(
"text")) {
1069 const auto c = varFloats(ed.theme->get(
"text"));
1070 if (
c.size() >= 3)
text = ImVec4(
c[0],
c[1],
c[2], 1.f);
1072 radius = floatOf(ed.theme,
"radius", 4.f);
1073 fontScale = floatOf(ed.theme,
"fontScale", 1.f);
1074 if (strOf(ed.theme,
"preset") ==
"light") {
1075 bg = ImVec4(0.93f, 0.93f, 0.93f, 1.f);
1076 panel = ImVec4(0.84f, 0.84f, 0.86f, 1.f);
1077 text = ImVec4(0.10f, 0.10f, 0.12f, 1.f);
1081 ImGui::PushStyleColor(ImGuiCol_WindowBg, bg);
1082 ImGui::PushStyleColor(ImGuiCol_ChildBg, panel);
1083 ImGui::PushStyleColor(ImGuiCol_Text,
text);
1084 ImGui::PushStyleColor(ImGuiCol_Button, accent);
1085 ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
1086 ImVec4(accent.x + 0.08f, accent.y + 0.08f, accent.z + 0.08f, 1.f));
1087 ImGui::PushStyleColor(ImGuiCol_ButtonActive,
1088 ImVec4(accent.x - 0.05f, accent.y - 0.05f, accent.z - 0.05f, 1.f));
1089 ImGui::PushStyleColor(ImGuiCol_Header, accent);
1090 ImGui::PushStyleColor(ImGuiCol_SliderGrab, accent);
1091 ImGui::PushStyleColor(ImGuiCol_CheckMark, accent);
1092 ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, radius);
1093 ImGui::PushStyleVar(ImGuiStyleVar_GrabRounding, radius);
1094 ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, radius);
1097 ImGuiWindowFlags flags = ImGuiWindowFlags_NoSavedSettings;
1098 if (!ed.resizable) flags |= ImGuiWindowFlags_NoResize;
1099 if (!ed.collapsible) flags |= ImGuiWindowFlags_NoCollapse;
1100 if (ImGui::Begin((ed.title +
"##" + ed.id).c_str(), &open, flags)) {
1101 if (fontScale != 1.f) ImGui::SetWindowFontScale(fontScale);
1102 renderChildren(I, ed,
1103 (ed.view && ed.view->has(
"children")) ? ed.view->getArray(
"children")
1108 emitEvent(ed,
"closed",
"", Var());
1112 if (fontScale != 1.f) ImGui::SetWindowFontScale(1.f);
1113 ImGui::PopStyleVar(3);
1114 ImGui::PopStyleColor(9);
1120 if (!impl_ || !impl_->windowOpen || !impl_->ui)
return;
1122 for (
auto& [
id, ed] : I.editors) {
1123 syncVMToView(I, ed);
1124 renderEditor(I, ed);
1126 for (
auto it = I.editors.begin(); it != I.editors.end();) {
1127 if (it->second.removed) it = I.editors.erase(it);
1133 if (!impl_ || !impl_->windowOpen || !impl_->vm)
return;
1136 if (!I.event) I.event = ModuleManager::requireInstance<eve::event::Event>(
"Event");
1137 if (!I.timer) I.timer = ModuleManager::requireInstance<eve::timer::Timer>(
"Timer");
1145 const std::string
name =
m->name;
1147 if (
name ==
"quit") {
1155 const float dt = I.timer ? I.timer->step() : 0.f;
1156 callRootFunc(I.vm->getHandle(),
"eve_host_update", {Var(static_cast<double>(dt))});
1157 I.gfx->clearScreen();
1158 callRootFunc(I.vm->getHandle(),
"eve_host_render", {});
1160 I.ui->beginFrameAndRender();
1164 if (I.ui) I.ui->dispatchEvents();
1169 const std::string& jsonValue) {
1170 if (!impl_)
return "error: host not started";
1171 auto it = impl_->editors.find(editorId);
1172 if (it == impl_->editors.end())
return "error: editor not found: " + editorId;
1173 if (widgetId.empty())
return "error: missing widget";
1176 Poco::JSON::Parser
p;
1177 v =
p.parse(jsonValue);
1181 if (jsonValue ==
"true")
v = Var(
true);
1182 else if (jsonValue ==
"false")
v = Var(
false);
1183 else if (jsonValue ==
"null")
v = Var();
1185 char* end =
nullptr;
1186 const double d = std::strtod(jsonValue.c_str(), &end);
1187 if (end && end != jsonValue.c_str() && *end ==
'\0')
1193 setWidgetValue(*impl_, it->second, widgetId,
v);
1198 if (!impl_)
return "[]";
1199 Array::Ptr arr =
new Array();
1200 auto drain = [&](Editor& ed) {
1201 for (
const auto& e : ed.events) {
1202 Object::Ptr eo =
new Object();
1203 eo->set(
"editor", ed.id);
1204 eo->set(
"widget", e.widget);
1205 eo->set(
"type", e.type);
1206 if (!e.value.isEmpty()) eo->set(
"value", e.value);
1211 if (!editorId.empty()) {
1212 auto it = impl_->editors.find(editorId);
1213 if (it == impl_->editors.end())
return "error: editor not found: " + editorId;
1216 for (
auto& [k, ed] : impl_->editors) drain(ed);
1218 return jsonStringify(Var(arr));
1222 if (!impl_)
return "{\"x\":0,\"y\":0,\"width\":0,\"height\":0}";
1223 auto it = impl_->editors.find(editorId);
1224 if (it == impl_->editors.end())
return "{\"x\":0,\"y\":0,\"width\":0,\"height\":0}";
1225 const Editor& ed = it->second;
1226 auto mn = ed.rectMin.find(widgetId), mx = ed.rectMax.find(widgetId);
1227 Object::Ptr o =
new Object();
1228 if (mn != ed.rectMin.end() && mx != ed.rectMax.end()) {
1229 o->set(
"x",
static_cast<double>(mn->second.x));
1230 o->set(
"y",
static_cast<double>(mn->second.y));
1231 o->set(
"width",
static_cast<double>(mx->second.x - mn->second.x));
1232 o->set(
"height",
static_cast<double>(mx->second.y - mn->second.y));
1237 o->set(
"height", 0);
1239 return jsonStringify(Var(o));
1243 if (!impl_ || !impl_->vm)
return "error: host not started";
1244 if (
name.empty())
return "error: missing name";
1245 const std::string err = runSquirrel(impl_->vm->getHandle(), source,
"host_vm.nut");
1246 if (!err.empty())
return "error: " + err;
1248 ssq::Table tbl = impl_->vm->find(
name.c_str()).toTable();
1249 impl_->vms[
name] = tbl;
1250 impl_->vmSources[
name] = source;
1252 }
catch (
const std::exception& e) {
1253 return std::string(
"error: table '") +
name +
"' not found after run: " + e.what();
1258 if (!impl_)
return "error: host not started";
1259 if (
name.empty())
return "error: missing name";
1260 impl_->vms.erase(
name);
1261 impl_->vmSources.erase(
name);
1264 const SQInteger top = sq_gettop(
v);
1265 sq_pushroottable(
v);
1266 sq_pushstring(
v,
name.c_str(),
static_cast<SQInteger
>(
name.size()));
1267 sq_deleteslot(
v, -2, SQFalse);
1275 if (!impl_)
return "error: host not started";
1276 auto it = impl_->editors.find(
id);
1277 if (it == impl_->editors.end())
return "error: editor not found: " +
id;
1280 std::filesystem::path
dir = std::filesystem::path(impl_->rootDir) /
"editors";
1281 std::filesystem::create_directories(
dir, ec);
1282 std::filesystem::path vp =
dir / (
id +
".editor.json");
1284 std::ofstream ofs(vp, std::ios::trunc);
1285 if (!ofs)
return "error: cannot write " + vp.string();
1286 ofs << jsonStringify(Var(it->second.view));
1288 if (!it->second.vmName.empty()) {
1289 auto vs = impl_->vmSources.find(it->second.vmName);
1290 if (vs != impl_->vmSources.end()) {
1291 std::filesystem::path sp =
dir / (
id +
".vm.nut");
1292 std::ofstream ofs2(sp, std::ios::trunc);
1293 if (!ofs2)
return "error: cannot write " + sp.string();
1298 }
catch (
const std::exception& e) {
1299 return std::string(
"error: ") + e.what();
1304 if (!impl_)
return "error: host not started";
1305 auto it = impl_->editors.find(
id);
1306 if (it == impl_->editors.end())
return "error: editor not found: " +
id;
1307 impl_->editors.erase(it);
1314 std::filesystem::path
dir = std::filesystem::path(impl_->rootDir) /
"editors";
1315 if (!std::filesystem::is_directory(
dir, ec))
return;
1316 std::vector<std::pair<std::string, std::string>> pending;
1317 for (
auto& entry : std::filesystem::directory_iterator(
dir, ec)) {
1319 const std::filesystem::path
p = entry.path();
1320 const std::string
name =
p.filename().string();
1321 const std::string suffix =
".editor.json";
1322 if (
p.extension() !=
".json" ||
name.size() <= suffix.size() ||
1323 name.compare(
name.size() - suffix.size(), suffix.size(), suffix) != 0)
1325 const std::string
id =
name.substr(0,
name.size() - suffix.size());
1326 std::ifstream ifs(
p);
1327 std::string json((std::istreambuf_iterator<char>(ifs)), std::istreambuf_iterator<char>());
1328 if (!json.empty()) pending.emplace_back(
id, json);
1330 for (
const auto& [
id, json] : pending) {
1332 Object::Ptr root = parseObject(json);
1333 const std::string
vmName = strOf(root,
"vm");
1335 std::ifstream ifs2(
dir / (
id +
".vm.nut"));
1336 std::string src((std::istreambuf_iterator<char>(ifs2)),
1337 std::istreambuf_iterator<char>());
1347 if (!impl_ || !impl_->vm)
return "error: host not started";
1348 if (source.empty())
return "error: missing source";
1349 const std::string err = runSquirrel(impl_->vm->getHandle(), source,
"host_snippet.nut");
1350 return err.empty() ?
"ok" : (
"error: " + err);
1354 if (!impl_ || !impl_->gfx)
return "error: no window";
1357 std::string
p = path.empty() ?
"eve_host_capture.png" : path;
1358 I.gfx->setScreenReadbackEnabled(
true);
1361 img = I.gfx->newImageData();
1365 if (I.inFrame)
throw;
1367 img = I.gfx->newImageData();
1369 if (!img)
return "error: readback returned no image";
1370 const int w = I.gfx->getPixelWidth();
1371 const int h = I.gfx->getPixelHeight();
1373 img->
encode(eve::image::ImageData::FormatHandler::ENCODED_PNG,
p.c_str(),
false);
1375 if (!png)
return "error: PNG encode failed";
1377 std::filesystem::create_directories(std::filesystem::path(
p).parent_path(), ec);
1378 std::ofstream out(
p, std::ios::binary | std::ios::trunc);
1381 return "error: cannot write " +
p;
1383 out.write(
static_cast<const char*
>(png->
getData()),
1384 static_cast<std::streamsize
>(png->
getSize()));
1386 const bool wrote = out.good();
1388 if (!wrote)
return "error: failed writing " +
p;
1389 Object::Ptr o =
new Object();
1392 o->set(
"height",
h);
1393 return jsonStringify(Var(o));
1394 }
catch (
const std::exception& e) {
1395 return std::string(
"error: ") + e.what();
1400 if (!impl_)
return "{\"running\":false,\"windowOpen\":false,\"editors\":[],\"viewModels\":[]}";
1401 Object::Ptr o =
new Object();
1402 o->set(
"running", running_);
1403 o->set(
"windowOpen", impl_->windowOpen);
1404 o->set(
"windowTitle", impl_->windowTitle);
1405 o->set(
"gameRoot", gameRoot_);
1406 Array::Ptr eds =
new Array();
1407 for (
const auto& [k, ed] : impl_->editors) {
1408 Object::Ptr e =
new Object();
1410 e->set(
"title", ed.title);
1413 o->set(
"editors", eds);
1414 Array::Ptr vms =
new Array();
1415 for (
const auto& [k,
v] : impl_->vms) vms->add(k);
1416 o->set(
"viewModels", vms);
1417 return jsonStringify(Var(o));
1422 ssq::Table eveTbl =
vm.find(
"eve").toTable();
1423 ssq::Table host = eveTbl.addTable(
"host");
1425 host.addFunc(
"openWindow", [](std::string t,
int w,
int h) {
1430 host.addFunc(
"applyEditor", [](std::string json) {
1433 host.addFunc(
"removeEditor", [](std::string
id) {
1436 host.addFunc(
"setValue", [](std::string editor, std::string
widget, std::string
value) {
1439 host.addFunc(
"events", [](std::string editor) {
1442 host.addFunc(
"registerVM", [](std::string
name, std::string source) {
1445 host.addFunc(
"unregisterVM", [](std::string
name) {
1448 host.addFunc(
"widgetRect", [](std::string editor, std::string
widget) {
1451 host.addFunc(
"capture", [](std::string path) {
1454 host.addFunc(
"save", [](std::string
id) {
1457 host.addFunc(
"runScript", [](std::string source) {
struct SQVM * HSQUIRRELVM
std::map< std::string, Var > values
std::map< std::string, ImVec2 > rectMin
std::map< std::string, ImVec2 > rectMax
std::vector< HostEvent > events
SettlementPipeline::Stage fn
Object is the base class for all game objects. It provides reference counting and dirty flag for upda...
A named event carrying an ordered list of Variant payloads. Pushed messages are heap-allocated; the q...
Data buffer paired with a filename (used for type identification).
size_t getSize() const
Gets the size of the Data in bytes.
void * getData() const
Gets a pointer to the data. This pointer will obviously not be valid if the Data object is destroyed.
Represents raw pixel data.
filesystem::FileData * encode(FormatHandler::EncodedFormat format, const char *filename, bool writefile) const
Encodes raw pixel data into a given format.
High-resolution frame/elapsed timer backed by SDL_GetPerformanceCounter(). Script: timer <- eve....
Headless MCP editor host (MVVM).
std::string status() const
std::string consumeEvents(const std::string &editorId)
Read and clear pending interaction events for one editor ("" = all).
std::string runScript(const std::string &source)
std::string closeWindow()
std::string listEditors() const
std::string unregisterVM(const std::string &name)
EditorHost(const EditorHost &)=delete
void syncBindings()
Pull bound ViewModel values into widget state (per-frame + on read).
void renderImGui()
Draw editor ImGui windows; call between beginFrameAndRender/present.
void loadEditorsFromDisk()
Load editors/<id>.editor.json (+ matching .vm.nut) on host startup.
std::string setEditorValue(const std::string &editorId, const std::string &widgetId, const std::string &jsonValue)
std::string capture(const std::string &path)
std::string applyEditor(const std::string &json)
JSON object/string: {id,title,vm,x,y,width,height,theme,children[]}.
std::string unloadEditor(const std::string &id)
Remove an editor from the session (files stay on disk).
std::string windowState() const
const std::string & gameRoot() const
std::string editorState(const std::string &id) const
Full state JSON: editors + values + (non-destructive) events.
void frame()
Pump events + update/render hooks + present (no-op without window).
std::string removeEditor(const std::string &id)
static EditorHost & instance()
std::string openWindow(const std::string &title, int width, int height)
std::string widgetRect(const std::string &editorId, const std::string &widgetId) const
Last-frame widget rect JSON (for script drawing inside viewports).
std::string saveEditor(const std::string &id)
Write editors/<id>.editor.json + editors/<id>.vm.nut under gameRoot.
void start(ssq::VM &vm, const std::string &gameRoot={}, bool allowWindow=true)
Attach to the host VM. gameRoot is the project dir for editors/.
void exposeScriptApi(ssq::VM &vm)
Expose eve.host table (registerVM/unregisterVM/widgetRect/...).
std::string registerVM(const std::string &name, const std::string &source)
Compile Squirrel source in the host VM, then register table name.
Declarative UI module (eve.UI).
Platform window interface (SDL implementation on desktop/mobile). Script: win <- eve....
WidgetDesc text(std::string content, std::string id)
Static text label.
eve::window::Window * win
eve::graphics::Graphics * gfx
std::map< std::string, ssq::Table > vms
std::map< std::string, Editor > editors
eve::timer::Timer * timer
std::map< std::string, std::string > vmSources
Display settings for window creation/resize. width/height == 0 selects the desktop display mode size.