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

1506 lines
56 KiB
C++

#include "ui/EditorHost.h"
#include "common/config.h"
#if defined(EVENGINE_WEBGPU) && defined(__EMSCRIPTEN__)
// The browser runtime trims Poco / DevTools / the host path. Keep EVUI
// linkable with a no-op implementation; `eve mcp` is desktop-only anyway.
namespace eve::ui {
struct EditorHost::Impl {};
EditorHost& EditorHost::instance() {
static EditorHost* inst = new EditorHost();
return *inst;
}
EditorHost::EditorHost() = default;
EditorHost::~EditorHost() = default;
void EditorHost::start(ssq::VM&, const std::string&, bool) {}
void EditorHost::stop() {}
bool EditorHost::windowOpen() const { return false; }
std::string EditorHost::openWindow(const std::string&, int, int) {
return "error: editor host unavailable on this platform";
}
std::string EditorHost::closeWindow() { return "ok"; }
std::string EditorHost::windowState() const { return "{}"; }
std::string EditorHost::applyEditor(const std::string&) {
return "error: editor host unavailable on this platform";
}
std::string EditorHost::removeEditor(const std::string&) {
return "error: editor host unavailable on this platform";
}
std::string EditorHost::listEditors() const { return "{\"editors\":[]}"; }
std::string EditorHost::editorState(const std::string&) const { return "{\"editors\":[]}"; }
void EditorHost::syncBindings() {}
std::string EditorHost::setEditorValue(const std::string&, const std::string&, const std::string&) {
return "error: editor host unavailable on this platform";
}
std::string EditorHost::consumeEvents(const std::string&) { return "[]"; }
std::string EditorHost::widgetRect(const std::string&, const std::string&) const {
return "{\"x\":0,\"y\":0,\"width\":0,\"height\":0}";
}
std::string EditorHost::registerVM(const std::string&, const std::string&) {
return "error: editor host unavailable on this platform";
}
std::string EditorHost::unregisterVM(const std::string&) {
return "error: editor host unavailable on this platform";
}
std::string EditorHost::saveEditor(const std::string&) {
return "error: editor host unavailable on this platform";
}
std::string EditorHost::unloadEditor(const std::string&) {
return "error: editor host unavailable on this platform";
}
void EditorHost::loadEditorsFromDisk() {}
std::string EditorHost::runScript(const std::string&) {
return "error: editor host unavailable on this platform";
}
std::string EditorHost::capture(const std::string&) {
return "error: editor host unavailable on this platform";
}
std::string EditorHost::status() const {
return "{\"running\":false,\"windowOpen\":false,\"editors\":[],\"viewModels\":[]}";
}
void EditorHost::frame() {}
void EditorHost::renderImGui() {}
void EditorHost::exposeScriptApi(ssq::VM&) {}
} // namespace eve::ui
#else // full implementation
#include "common/Module.h"
#include "event/Event.h"
#include "filesystem/FileData.h"
#include "graphics/Graphics.h"
#include "image/ImageData.h"
#include "timer/Timer.h"
#include "ui/UI.h"
#include "window/Window.h"
#include <Poco/Dynamic/Var.h>
#include <Poco/Exception.h>
#include <Poco/JSON/Array.h>
#include <Poco/JSON/Object.h>
#include <Poco/JSON/Parser.h>
#include <Poco/JSON/Stringifier.h>
#include <imgui.h>
#include <simplesquirrel/simplesquirrel.hpp>
#include <squirrel.h>
#include <algorithm>
#include <cstdlib>
#include <cstring>
#include <cstdio>
#include <fstream>
#include <filesystem>
#include <fstream>
#include <functional>
#include <map>
#include <sstream>
#include <string>
#include <utility>
#include <vector>
namespace eve::ui {
namespace {
using Poco::Dynamic::Var;
using Poco::JSON::Array;
using Poco::JSON::Object;
using Poco::JSON::Parser;
std::string jsonStringify(const Var& v) {
std::ostringstream oss;
Poco::JSON::Stringifier::stringify(v, oss, 0, 0);
return oss.str();
}
Object::Ptr parseObject(const std::string& json) {
Poco::JSON::Parser parser;
return parser.parse(json).extract<Object::Ptr>();
}
std::string strOf(Object::Ptr o, const char* key, const std::string& def = {}) {
if (!o || !o->has(key)) return def;
try {
return o->get(key).convert<std::string>();
} catch (...) {
return def;
}
}
int intOf(Object::Ptr o, const char* key, int def = 0) {
if (!o || !o->has(key)) return def;
try {
return o->get(key).convert<int>();
} catch (...) {
return def;
}
}
float floatOf(Object::Ptr o, const char* key, float def = 0.f) {
if (!o || !o->has(key)) return def;
try {
return static_cast<float>(o->get(key).convert<double>());
} catch (...) {
return def;
}
}
bool boolOf(Object::Ptr o, const char* key, bool def = false) {
if (!o || !o->has(key)) return def;
try {
return o->get(key).convert<bool>();
} catch (...) {
return def;
}
}
float varFloat(const Var& v, float def = 0.f) {
try {
return static_cast<float>(v.convert<double>());
} catch (...) {
return def;
}
}
bool varBool(const Var& v, bool def = false) {
try {
return v.convert<bool>();
} catch (...) {
return def;
}
}
std::string varString(const Var& v, const std::string& def = {}) {
try {
return v.convert<std::string>();
} catch (...) {
return def;
}
}
Array::Ptr varToArray(const Var& v) {
try {
return v.extract<Array::Ptr>();
} catch (...) {
return nullptr;
}
}
std::vector<float> varFloats(const Var& v) {
std::vector<float> out;
Array::Ptr arr = varToArray(v);
if (!arr) return out;
for (size_t i = 0; i < arr->size(); ++i) {
try {
out.push_back(
static_cast<float>(arr->get(static_cast<unsigned int>(i)).convert<double>()));
} catch (...) {
out.push_back(0.f);
}
}
return out;
}
/** Slider init: array (slider2/slider3) or single scalar (slider). */
std::vector<float> widgetFloatInit(const Var& v) {
std::vector<float> init = varFloats(v);
if (!init.empty()) return init;
try {
init.push_back(static_cast<float>(v.convert<double>()));
} catch (...) {
}
return init;
}
std::vector<std::string> varStrings(const Var& v) {
std::vector<std::string> out;
Array::Ptr arr = varToArray(v);
if (!arr) return out;
for (size_t i = 0; i < arr->size(); ++i)
out.push_back(varString(arr->get(static_cast<unsigned int>(i))));
return out;
}
// ---- raw Squirrel helpers -------------------------------------------------
Var stackVar(HSQUIRRELVM vm, SQInteger index) {
switch (sq_gettype(vm, index)) {
case OT_NULL:
return Var();
case OT_BOOL: {
SQBool b = SQFalse;
if (SQ_SUCCEEDED(sq_getbool(vm, index, &b))) return Var(bool(b));
return Var();
}
case OT_INTEGER: {
SQInteger i = 0;
if (SQ_SUCCEEDED(sq_getinteger(vm, index, &i)))
return Var(static_cast<Poco::Int64>(i));
return Var();
}
case OT_FLOAT: {
SQFloat f = 0.f;
if (SQ_SUCCEEDED(sq_getfloat(vm, index, &f)))
return Var(static_cast<double>(f));
return Var();
}
case OT_STRING: {
const SQChar* s = nullptr;
if (SQ_SUCCEEDED(sq_getstring(vm, index, &s)) && s) return Var(std::string(s));
return Var();
}
case OT_ARRAY: {
HSQOBJECT obj;
if (SQ_FAILED(sq_getstackobj(vm, index, &obj))) return Var();
sq_pushobject(vm, obj);
sq_pushnull(vm);
Array::Ptr arr = new Array();
while (SQ_SUCCEEDED(sq_next(vm, -2))) {
arr->add(stackVar(vm, -1));
sq_pop(vm, 2);
}
sq_pop(vm, 1);
return Var(arr);
}
default:
return Var();
}
}
void pushVar(HSQUIRRELVM vm, const Var& v) {
if (v.isEmpty()) {
sq_pushnull(vm);
return;
}
try {
if (v.isBoolean()) {
sq_pushbool(vm, v.convert<bool>() ? SQTrue : SQFalse);
return;
}
if (v.isNumeric()) {
sq_pushfloat(vm, static_cast<SQFloat>(v.convert<double>()));
return;
}
if (v.isString()) {
const std::string s = v.convert<std::string>();
sq_pushstring(vm, s.c_str(), static_cast<SQInteger>(s.size()));
return;
}
if (v.isArray()) {
Array::Ptr arr;
try {
arr = v.extract<Array::Ptr>();
} catch (...) {
sq_pushnull(vm);
return;
}
sq_newarray(vm, 0);
if (arr) {
for (size_t i = 0; i < arr->size(); ++i) {
pushVar(vm, arr->get(static_cast<unsigned int>(i)));
if (SQ_FAILED(sq_arrayappend(vm, -2))) break;
}
}
return;
}
} catch (...) {
}
sq_pushnull(vm);
}
/** Run Squirrel source against the root table; returns "" on success. */
std::string runSquirrel(HSQUIRRELVM vm, const std::string& source, const char* name) {
const SQInteger top = sq_gettop(vm);
if (SQ_FAILED(sq_compilebuffer(vm, source.c_str(), static_cast<SQInteger>(source.size()),
name, SQTrue))) {
sq_settop(vm, top);
return "compile failed";
}
sq_pushroottable(vm);
if (SQ_FAILED(sq_call(vm, 1, SQFalse, SQTrue))) {
sq_settop(vm, top);
return "runtime failed";
}
sq_settop(vm, top);
return {};
}
void callRootFunc(HSQUIRRELVM vm, const std::string& name, const std::vector<Var>& args) {
const SQInteger top = sq_gettop(vm);
sq_pushroottable(vm);
sq_pushstring(vm, name.c_str(), static_cast<SQInteger>(name.size()));
if (SQ_FAILED(sq_get(vm, -2))) {
sq_settop(vm, top);
return; // hook not defined
}
if (sq_gettype(vm, -1) != OT_CLOSURE) {
sq_settop(vm, top);
return;
}
sq_pushroottable(vm);
for (const auto& a : args) pushVar(vm, a);
if (SQ_FAILED(sq_call(vm, static_cast<SQInteger>(args.size()) + 1, SQFalse, SQTrue)))
fprintf(stderr, "eve.host: %s() failed\n", name.c_str());
sq_settop(vm, top);
}
bool callTableFunc(HSQUIRRELVM vm, const ssq::Table& tbl, const std::string& name,
const std::vector<Var>& args, std::string* err) {
if (name.empty()) return true;
const SQInteger top = sq_gettop(vm);
try {
ssq::Function fn = tbl.findFunc(name.c_str());
sq_pushobject(vm, fn.getRaw());
sq_pushobject(vm, tbl.getRaw());
for (const auto& a : args) pushVar(vm, a);
if (SQ_FAILED(sq_call(vm, static_cast<SQInteger>(args.size()) + 1, SQFalse, SQTrue))) {
if (err) *err = "runtime failed: " + name;
sq_settop(vm, top);
return false;
}
sq_settop(vm, top);
return true;
} catch (const ssq::NotFoundException&) {
sq_settop(vm, top);
return true; // optional callback / command absent
} catch (const std::exception& e) {
if (err) *err = e.what();
sq_settop(vm, top);
return false;
}
}
/** Split "vm.slot" / "vm.table.slot" into slot (+ nested + nestedSlot). */
bool splitBindPath(const std::string& bind, std::string& slot, std::string& nested) {
if (bind.rfind("vm.", 0) != 0) return false;
std::string rest = bind.substr(3);
const size_t dot = rest.find('.');
if (dot == std::string::npos) {
slot = rest;
nested.clear();
} else if (rest.find('.', dot + 1) == std::string::npos) {
slot = rest.substr(0, dot);
nested = rest.substr(dot + 1);
} else {
return false; // only one nesting level supported in v1
}
return !slot.empty();
}
Var objectToVar(ssq::VM& vm, const ssq::Object& o) {
// Push the object and convert through the stack so arrays recurse cleanly.
HSQUIRRELVM v = vm.getHandle();
const SQInteger top = sq_gettop(v);
sq_pushobject(v, o.getRaw());
Var out = stackVar(v, -1);
sq_settop(v, top);
return out;
}
Var readVMVar(ssq::VM& vm, const ssq::Table& tbl, const std::string& bind, bool* ok = nullptr) {
std::string slot, nested;
if (ok) *ok = false;
if (!splitBindPath(bind, slot, nested)) return Var();
try {
ssq::Object o = tbl.find(slot.c_str());
if (!nested.empty()) {
if (o.getType() != ssq::Type::TABLE) return Var();
o = o.toTable().find(nested.c_str());
}
if (ok) *ok = true;
return objectToVar(vm, o);
} catch (...) {
return Var();
}
}
void writeVMVar(ssq::VM& vm, const ssq::Table& tbl, const std::string& bind, const Var& value) {
std::string slot, nested;
if (!splitBindPath(bind, slot, nested)) return;
HSQUIRRELVM v = vm.getHandle();
const SQInteger top = sq_gettop(v);
if (nested.empty()) {
sq_pushobject(v, tbl.getRaw());
sq_pushstring(v, slot.c_str(), static_cast<SQInteger>(slot.size()));
pushVar(v, value);
sq_newslot(v, -3, false);
} else {
try {
ssq::Table t2 = tbl.find(slot.c_str()).toTable();
sq_pushobject(v, t2.getRaw());
sq_pushstring(v, nested.c_str(), static_cast<SQInteger>(nested.size()));
pushVar(v, value);
sq_newslot(v, -3, false);
} catch (...) {
}
}
sq_settop(v, top);
}
struct HostEvent {
std::string type; // "click" | "change" | "closed"
std::string widget;
Var value;
};
struct Editor {
std::string id;
std::string title;
std::string vmName;
Object::Ptr view;
float x = 0.f, y = 0.f, width = 0.f, height = 0.f;
bool resizable = true;
bool collapsible = true;
std::string layout = "vertical";
Object::Ptr theme;
std::map<std::string, Var> values;
std::vector<HostEvent> events;
std::map<std::string, ImVec2> rectMin, rectMax;
bool removed = false;
};
void forEachWidget(Object::Ptr parent, const std::function<void(Object::Ptr)>& fn) {
if (!parent) return;
Array::Ptr kids = parent->has("children") ? parent->getArray("children") : nullptr;
if (!kids) return;
for (size_t i = 0; i < kids->size(); ++i) {
Object::Ptr w = kids->getObject(static_cast<unsigned>(i));
if (!w) continue;
fn(w);
forEachWidget(w, fn);
}
}
Object::Ptr findWidget(Object::Ptr parent, const std::string& id) {
if (!parent || id.empty()) return nullptr;
Array::Ptr kids = parent->has("children") ? parent->getArray("children") : nullptr;
if (!kids) return nullptr;
for (size_t i = 0; i < kids->size(); ++i) {
Object::Ptr w = kids->getObject(static_cast<unsigned>(i));
if (!w) continue;
if (strOf(w, "id") == id) return w;
if (Object::Ptr sub = findWidget(w, id)) return sub;
}
return nullptr;
}
std::string widgetIdLabel(Object::Ptr w) {
const std::string id = strOf(w, "id");
const std::string label = strOf(w, "label");
if (label.empty()) return "##" + id;
if (id.empty()) return label;
return label + "##" + id;
}
/** Resolve "vm.slot" paths to a plain ViewModel slot/function name. */
std::string vmFuncName(const std::string& path) {
if (path.rfind("vm.", 0) == 0) return path.substr(3);
return path;
}
void recordRect(Editor& ed, const std::string& id) {
if (id.empty()) return;
ed.rectMin[id] = ImGui::GetItemRectMin();
ed.rectMax[id] = ImGui::GetItemRectMax();
}
void emitEvent(Editor& ed, const std::string& type, const std::string& widget, const Var& value) {
ed.events.push_back({type, widget, value});
}
void renderChildren(EditorHost::Impl& I, Editor& ed, Array::Ptr kids, const std::string& layout);
void syncVMToView(EditorHost::Impl& I, Editor& ed);
} // namespace
// Out-of-line definition of the pimpl. Lives at eve::ui scope (not inside the
// anonymous namespace) so it can be named as eve::ui::EditorHost::Impl.
struct EditorHost::Impl {
ssq::VM* vm = nullptr;
bool allowWindow = true;
std::string rootDir;
eve::window::Window* win = nullptr;
eve::graphics::Graphics* gfx = nullptr;
eve::ui::UI* ui = nullptr;
eve::event::Event* event = nullptr;
eve::timer::Timer* timer = nullptr;
bool windowOpen = false;
bool inFrame = false;
std::string windowTitle;
std::map<std::string, Editor> editors;
std::map<std::string, ssq::Table> vms;
std::map<std::string, std::string> vmSources;
};
// ---------------------------------------------------------------------------
// public surface
// ---------------------------------------------------------------------------
EditorHost& EditorHost::instance() {
// Intentionally leaked: the host VM may be torn down after the singleton.
static EditorHost* inst = new EditorHost();
return *inst;
}
EditorHost::EditorHost() = default;
EditorHost::~EditorHost() = default;
void EditorHost::start(ssq::VM& vm, const std::string& gameRoot, bool allowWindow) {
if (running_) return;
impl_ = std::make_unique<Impl>();
impl_->vm = &vm;
impl_->allowWindow = allowWindow;
allowWindow_ = allowWindow;
gameRoot_ = gameRoot.empty() ? std::filesystem::current_path().string() : gameRoot;
impl_->rootDir = gameRoot_;
running_ = true;
loadEditorsFromDisk();
}
void EditorHost::stop() {
if (!running_) return;
closeWindow();
impl_.reset();
running_ = false;
exitRequested_ = false;
}
std::string EditorHost::openWindow(const std::string& title, int width, int height) {
if (!impl_) return "error: host not started";
auto& I = *impl_;
if (!I.allowWindow) return "error: window creation disabled";
try {
if (!I.win) I.win = ModuleManager::requireInstance<eve::window::Window>("Window");
if (!I.gfx) I.gfx = ModuleManager::requireInstance<eve::graphics::Graphics>("Graphics");
eve::window::WindowSettings s;
s.width = static_cast<uint16_t>(width > 0 ? width : 1280);
s.height = static_cast<uint16_t>(height > 0 ? height : 800);
s.centered = true;
s.resizable = true;
if (!I.win->setWindowSettings(s)) return "error: setWindowSettings failed";
I.win->setWindowTitle(title.empty() ? "EVEngine AI Host" : title);
if (!I.ui) I.ui = ModuleManager::requireInstance<eve::ui::UI>("UI");
I.windowOpen = true;
I.windowTitle = title;
// Keep screen readback on while the host window is open: the AI's whole
// feedback loop is "render -> capture", so every presented frame should
// be available to eve_host_capture immediately (the Vulkan backend
// installs the present-copy hook as soon as readback is enabled).
if (I.gfx) I.gfx->setScreenReadbackEnabled(true);
return "ok";
} catch (const std::exception& e) {
return std::string("error: ") + e.what();
}
}
std::string EditorHost::closeWindow() {
if (!impl_) return "ok";
auto& I = *impl_;
if (I.windowOpen && I.win) {
try {
I.win->close();
} catch (...) {
}
if (I.ui) I.ui->shutdownBackend();
I.windowOpen = false;
}
return "ok";
}
bool EditorHost::windowOpen() const {
return impl_ && impl_->windowOpen;
}
std::string EditorHost::windowState() const {
if (!impl_ || !impl_->windowOpen) return "{\"open\":false}";
Object::Ptr o = new Object();
o->set("open", true);
o->set("title", impl_->windowTitle);
if (impl_->win) {
o->set("width", impl_->win->getWidth());
o->set("height", impl_->win->getHeight());
}
return jsonStringify(Var(o));
}
std::string EditorHost::applyEditor(const std::string& json) {
if (!impl_) return "error: host not started";
auto& I = *impl_;
try {
Object::Ptr root = parseObject(json);
const std::string id = strOf(root, "id");
if (id.empty()) return "error: editor requires id";
Editor ed;
ed.id = id;
ed.title = strOf(root, "title", id);
ed.vmName = strOf(root, "vm");
ed.view = root;
ed.x = static_cast<float>(intOf(root, "x", 0));
ed.y = static_cast<float>(intOf(root, "y", 0));
ed.width = static_cast<float>(intOf(root, "width", 0));
ed.height = static_cast<float>(intOf(root, "height", 0));
ed.resizable = boolOf(root, "resizable", true);
ed.collapsible = boolOf(root, "collapsible", true);
ed.layout = strOf(root, "layout", "vertical");
if (root->has("theme")) ed.theme = root->getObject("theme");
forEachWidget(root, [&](Object::Ptr w) {
const std::string wid = strOf(w, "id");
if (wid.empty()) return;
if (w->has("value")) ed.values[wid] = w->get("value");
});
I.editors[id] = std::move(ed);
if (!I.windowOpen && I.allowWindow) {
std::string err = openWindow(ed.title + " - EVEngine AI Host", 1280, 800);
if (err.rfind("error:", 0) == 0) return err;
}
return editorState(id);
} catch (const std::exception& e) {
return std::string("error: ") + e.what();
}
}
std::string EditorHost::removeEditor(const std::string& id) {
if (!impl_) return "error: host not started";
auto it = impl_->editors.find(id);
if (it == impl_->editors.end()) return "error: editor not found: " + id;
impl_->editors.erase(it);
return "ok";
}
std::string EditorHost::listEditors() const {
if (!impl_) return "{\"editors\":[]}";
Array::Ptr arr = new Array();
for (const auto& [k, ed] : impl_->editors) {
Object::Ptr o = new Object();
o->set("id", k);
o->set("title", ed.title);
o->set("vm", ed.vmName);
arr->add(o);
}
Object::Ptr root = new Object();
root->set("editors", arr);
return jsonStringify(Var(root));
}
namespace {
Poco::JSON::Object::Ptr editorStateObject(const Editor& ed) {
Object::Ptr o = new Object();
o->set("id", ed.id);
o->set("title", ed.title);
o->set("vm", ed.vmName);
Object::Ptr vals = new Object();
for (const auto& [k, v] : ed.values) vals->set(k, v);
o->set("values", vals);
Array::Ptr evs = new Array();
for (const auto& e : ed.events) {
Object::Ptr eo = new Object();
eo->set("editor", ed.id);
eo->set("widget", e.widget);
eo->set("type", e.type);
if (!e.value.isEmpty()) eo->set("value", e.value);
evs->add(eo);
}
o->set("events", evs);
return o;
}
} // namespace
std::string EditorHost::editorState(const std::string& id) const {
if (!impl_) return "{\"editors\":[]}";
const_cast<EditorHost*>(this)->syncBindings();
Array::Ptr arr = new Array();
if (!id.empty()) {
auto it = impl_->editors.find(id);
if (it == impl_->editors.end()) return "error: editor not found: " + id;
arr->add(editorStateObject(it->second));
} else {
for (const auto& [k, ed] : impl_->editors) arr->add(editorStateObject(ed));
}
Object::Ptr root = new Object();
root->set("editors", arr);
return jsonStringify(Var(root));
}
void EditorHost::syncBindings() {
if (!impl_) return;
for (auto& [k, ed] : impl_->editors) syncVMToView(*impl_, ed);
}
namespace {
void pushViewToVM(EditorHost::Impl& I, Editor& ed, const std::string& widgetId, const Var& value) {
if (!I.vm || ed.vmName.empty()) return;
auto it = I.vms.find(ed.vmName);
if (it == I.vms.end()) return;
Object::Ptr w = findWidget(ed.view, widgetId);
if (!w) return;
const std::string bind = strOf(w, "bind");
if (!bind.empty()) writeVMVar(*I.vm, it->second, bind, value);
const std::string onChange = strOf(w, "onChange");
if (!onChange.empty()) {
std::string err;
callTableFunc(I.vm->getHandle(), it->second, vmFuncName(onChange),
{Var(widgetId), value}, &err);
if (!err.empty())
fprintf(stderr, "eve.host: onChange %s: %s\n", onChange.c_str(), err.c_str());
}
}
void setWidgetValue(EditorHost::Impl& I, Editor& ed, const std::string& widgetId,
const Var& value) {
ed.values[widgetId] = value;
emitEvent(ed, "change", widgetId, value);
pushViewToVM(I, ed, widgetId, value);
}
void resolveOneWay(EditorHost::Impl& I, Editor& ed, Object::Ptr w) {
if (!I.vm || ed.vmName.empty()) return;
auto it = I.vms.find(ed.vmName);
if (it == I.vms.end()) return;
const char* oneWay[] = {"bind:label", "bind:visible", "bind:enabled"};
for (const char* key : oneWay) {
const std::string bind = strOf(w, key);
if (bind.empty()) continue;
const Var v = readVMVar(*I.vm, it->second, bind);
if (v.isEmpty()) continue;
const std::string k(key);
if (k == "bind:label") w->set("label", varString(v));
else if (k == "bind:visible") w->set("visible", varBool(v, true));
else if (k == "bind:enabled") w->set("enabled", varBool(v, true));
}
const std::string optBind = strOf(w, "bind:options");
if (!optBind.empty()) {
const Var v = readVMVar(*I.vm, it->second, optBind);
if (!v.isEmpty()) {
Array::Ptr opts = new Array();
for (const auto& s : varStrings(v)) opts->add(s);
w->set("options", opts);
}
}
}
void syncVMToView(EditorHost::Impl& I, Editor& ed) {
if (!I.vm || ed.vmName.empty()) return;
auto it = I.vms.find(ed.vmName);
if (it == I.vms.end()) return;
forEachWidget(ed.view, [&](Object::Ptr w) {
const std::string bind = strOf(w, "bind");
const std::string id = strOf(w, "id");
if (bind.empty() || id.empty()) return;
const Var v = readVMVar(*I.vm, it->second, bind);
if (v.isEmpty()) return;
auto cur = ed.values.find(id);
if (cur == ed.values.end() || jsonStringify(cur->second) != jsonStringify(v))
ed.values[id] = v;
});
}
void renderWidget(EditorHost::Impl& I, Editor& ed, Object::Ptr w);
void renderChildren(EditorHost::Impl& I, Editor& ed, Array::Ptr kids, const std::string& layout) {
if (!kids) return;
for (size_t i = 0; i < kids->size(); ++i) {
Object::Ptr w = kids->getObject(static_cast<unsigned>(i));
if (!w) continue;
resolveOneWay(I, ed, w);
renderWidget(I, ed, w);
if (layout == "horizontal" && i + 1 < kids->size()) ImGui::SameLine();
}
}
void renderWidget(EditorHost::Impl& I, Editor& ed, Object::Ptr w) {
if (!w) return;
if (!boolOf(w, "visible", true)) return;
const std::string type = strOf(w, "type", "label");
const std::string id = strOf(w, "id");
const std::string label = strOf(w, "label");
const bool enabled = boolOf(w, "enabled", true);
if (!enabled) {
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.5f);
}
const auto cur = [&](const char* key, const Var& def) -> Var {
if (!id.empty()) {
auto it = ed.values.find(id);
if (it != ed.values.end()) return it->second;
}
if (w->has(key)) return w->get(key);
return def;
};
if (type == "label" || type == "text") {
const std::string text = strOf(w, "text", label);
if (type == "text") ImGui::TextWrapped("%s", text.c_str());
else ImGui::TextUnformatted(text.c_str());
recordRect(ed, id);
} else if (type == "separator") {
ImGui::Separator();
} else if (type == "spacer") {
ImGui::Dummy(ImVec2(floatOf(w, "width", 0.f), floatOf(w, "height", 8.f)));
} else if (type == "progress") {
float v = std::clamp(varFloat(cur("value", 0.0), 0.f), 0.f, 1.f);
const float pw = floatOf(w, "width", 0.f);
ImGui::ProgressBar(v, ImVec2(pw > 0.f ? pw : -FLT_MIN, 0.f),
label.empty() ? nullptr : label.c_str());
recordRect(ed, id);
} else if (type == "plot") {
const std::vector<float> data = varFloats(cur("data", Var()));
if (!data.empty()) {
ImGui::PlotLines(widgetIdLabel(w).c_str(), data.data(), static_cast<int>(data.size()), 0,
nullptr, floatOf(w, "min", 0.f), floatOf(w, "max", 1.f),
ImVec2(floatOf(w, "width", 0.f), floatOf(w, "height", 80.f)));
recordRect(ed, id);
}
} else if (type == "input") {
std::string buf = varString(cur("value", strOf(w, "value")));
const bool multi = boolOf(w, "multiline", false);
bool changed = false;
if (multi) {
changed = ImGui::InputTextMultiline(widgetIdLabel(w).c_str(), buf.data(),
buf.size() + 64,
ImVec2(floatOf(w, "width", 0.f), floatOf(w, "height", 96.f)));
} else {
buf.resize(512);
changed = ImGui::InputText(widgetIdLabel(w).c_str(), buf.data(), buf.size());
}
if (enabled && changed && !id.empty()) {
buf.resize(strlen(buf.c_str()));
setWidgetValue(I, ed, id, Var(buf));
}
recordRect(ed, id);
} else if (type == "slider" || type == "slider2" || type == "slider3") {
const int n = type == "slider" ? 1 : (type == "slider2" ? 2 : 3);
float v[3] = {0.f, 0.f, 0.f};
const std::vector<float> init = widgetFloatInit(cur("value", Var()));
for (int i = 0; i < n; ++i) v[i] = i < static_cast<int>(init.size()) ? init[i] : 0.f;
const float lo = floatOf(w, "min", 0.f), hi = floatOf(w, "max", 1.f);
const std::string fmt = strOf(w, "format", "%.2f");
bool changed = false;
if (n == 1) changed = ImGui::SliderFloat(widgetIdLabel(w).c_str(), &v[0], lo, hi, fmt.c_str());
else if (n == 2) changed = ImGui::SliderFloat2(widgetIdLabel(w).c_str(), v, lo, hi, fmt.c_str());
else changed = ImGui::SliderFloat3(widgetIdLabel(w).c_str(), v, lo, hi, fmt.c_str());
if (enabled && changed && !id.empty()) {
Array::Ptr arr = new Array();
for (int i = 0; i < n; ++i) arr->add(static_cast<double>(v[i]));
setWidgetValue(I, ed, id, Var(arr));
}
recordRect(ed, id);
} else if (type == "color") {
const bool alpha = boolOf(w, "alpha", false);
const std::vector<float> init = varFloats(cur("value", Var()));
float c[4] = {1.f, 1.f, 1.f, 1.f};
for (size_t i = 0; i < std::min<size_t>(init.size(), 4); ++i) c[i] = init[i];
const bool changed = alpha ? ImGui::ColorEdit4(widgetIdLabel(w).c_str(), c)
: ImGui::ColorEdit3(widgetIdLabel(w).c_str(), c);
if (enabled && changed && !id.empty()) {
Array::Ptr arr = new Array();
const int n = alpha ? 4 : 3;
for (int i = 0; i < n; ++i) arr->add(static_cast<double>(c[i]));
setWidgetValue(I, ed, id, Var(arr));
}
recordRect(ed, id);
} else if (type == "checkbox") {
bool b = varBool(cur("value", false), boolOf(w, "value", false));
if (enabled && ImGui::Checkbox(widgetIdLabel(w).c_str(), &b) && !id.empty())
setWidgetValue(I, ed, id, Var(b));
recordRect(ed, id);
} else if (type == "dropdown" || type == "listbox") {
const std::vector<std::string> opts = varStrings(cur("options", Var()));
std::vector<const char*> items;
for (const auto& s : opts) items.push_back(s.c_str());
std::string sel = varString(cur("value", strOf(w, "value")));
int idx = 0;
for (size_t i = 0; i < opts.size(); ++i)
if (opts[i] == sel) idx = static_cast<int>(i);
bool changed = false;
if (type == "dropdown")
changed = !opts.empty() && ImGui::Combo(widgetIdLabel(w).c_str(), &idx, items.data(),
static_cast<int>(items.size()));
else
changed = !opts.empty() && ImGui::ListBox(widgetIdLabel(w).c_str(), &idx, items.data(),
static_cast<int>(items.size()),
intOf(w, "heightItems", -1));
if (enabled && changed && !id.empty() && idx >= 0 && idx < static_cast<int>(opts.size()))
setWidgetValue(I, ed, id, Var(opts[static_cast<size_t>(idx)]));
recordRect(ed, id);
} else if (type == "button") {
if (enabled && ImGui::Button(widgetIdLabel(w).c_str(), ImVec2(floatOf(w, "width", 0.f), 0.f))) {
emitEvent(ed, "click", id, Var());
const std::string cmd = strOf(w, "command", strOf(w, "action"));
if (!cmd.empty() && !ed.vmName.empty() && I.vm) {
auto it = I.vms.find(ed.vmName);
if (it != I.vms.end()) {
std::string err;
callTableFunc(I.vm->getHandle(), it->second, vmFuncName(cmd),
{Var(ed.id), Var(id)}, &err);
if (!err.empty())
fprintf(stderr, "eve.host: command %s: %s\n", cmd.c_str(), err.c_str());
}
}
}
recordRect(ed, id);
} else if (type == "tree") {
const bool open = boolOf(w, "open", true);
if (ImGui::TreeNodeEx(widgetIdLabel(w).c_str(),
open ? ImGuiTreeNodeFlags_DefaultOpen : 0)) {
renderChildren(I, ed,
w->has("children") ? w->getArray("children") : nullptr, ed.layout);
ImGui::TreePop();
}
recordRect(ed, id);
} else if (type == "group") {
const bool border = boolOf(w, "border", true);
if (border) {
if (ImGui::BeginChild(widgetIdLabel(w).c_str(),
ImVec2(floatOf(w, "width", 0.f), floatOf(w, "height", 0.f)), true)) {
if (!label.empty()) ImGui::TextUnformatted(label.c_str());
renderChildren(I, ed,
w->has("children") ? w->getArray("children") : nullptr,
strOf(w, "layout", "vertical"));
}
ImGui::EndChild();
} else {
ImGui::BeginGroup();
if (!label.empty()) ImGui::TextUnformatted(label.c_str());
renderChildren(I, ed,
w->has("children") ? w->getArray("children") : nullptr,
strOf(w, "layout", "vertical"));
ImGui::EndGroup();
}
recordRect(ed, id);
} else if (type == "tabs") {
Array::Ptr tabs = w->has("children") ? w->getArray("children") : nullptr;
if (tabs && ImGui::BeginTabBar((id.empty() ? "tabs" : id).c_str())) {
for (size_t i = 0; i < tabs->size(); ++i) {
Object::Ptr tab = tabs->getObject(static_cast<unsigned>(i));
if (!tab) continue;
if (ImGui::BeginTabItem(strOf(tab, "label", "Tab").c_str())) {
renderChildren(I, ed,
tab->has("children") ? tab->getArray("children") : nullptr,
strOf(tab, "layout", "vertical"));
ImGui::EndTabItem();
}
}
ImGui::EndTabBar();
}
} else if (type == "tab") {
renderChildren(I, ed, w->has("children") ? w->getArray("children") : nullptr,
strOf(w, "layout", "vertical"));
} else if (type == "table") {
const std::vector<std::string> cols =
w->has("columns") ? varStrings(w->get("columns")) : std::vector<std::string>();
Array::Ptr rows = w->has("rows") ? w->getArray("rows") : nullptr;
if (!cols.empty() && ImGui::BeginTable((id.empty() ? "table" : id).c_str(),
static_cast<int>(cols.size()),
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) {
for (const auto& c : cols) ImGui::TableSetupColumn(c.c_str());
ImGui::TableHeadersRow();
if (rows) {
for (size_t r = 0; r < rows->size(); ++r) {
ImGui::TableNextRow();
Array::Ptr cells = rows->getArray(static_cast<unsigned>(r));
if (!cells) continue;
for (size_t c = 0; c < cells->size(); ++c) {
ImGui::TableSetColumnIndex(static_cast<int>(c));
ImGui::TextUnformatted(
varString(cells->get(static_cast<unsigned int>(c))).c_str());
}
}
}
ImGui::EndTable();
}
recordRect(ed, id);
} else if (type == "viewport") {
const float vw = floatOf(w, "width", 0.f);
const float vh = floatOf(w, "height", 120.f);
const std::vector<float> bg =
w->has("bg") ? varFloats(w->get("bg")) : std::vector<float>();
const ImVec2 pos = ImGui::GetCursorScreenPos();
ImVec2 size(vw > 0.f ? vw : ImGui::GetContentRegionAvail().x, vh);
ImU32 col = IM_COL32(10, 12, 16, 255);
if (bg.size() >= 3)
col = IM_COL32(static_cast<int>(bg[0] * 255.f), static_cast<int>(bg[1] * 255.f),
static_cast<int>(bg[2] * 255.f), 255);
ImGui::GetWindowDrawList()->AddRectFilled(pos, ImVec2(pos.x + size.x, pos.y + size.y), col);
if (ImGui::BeginChild(widgetIdLabel(w).c_str(), size, true)) {
if (!label.empty()) ImGui::TextUnformatted(label.c_str());
// AI scripts draw inside here using eve.host.widgetRect().
}
ImGui::EndChild();
if (!id.empty()) {
ed.rectMin[id] = ImGui::GetItemRectMin();
ed.rectMax[id] = ImGui::GetItemRectMax();
}
}
const std::string tip = strOf(w, "tooltip");
if (!tip.empty() && ImGui::IsItemHovered()) ImGui::SetTooltip("%s", tip.c_str());
if (!enabled) {
ImGui::PopStyleVar();
}
}
void renderEditor(EditorHost::Impl& I, Editor& ed) {
if (ed.removed) return;
if (ed.width > 0.f)
ImGui::SetNextWindowSize(ImVec2(ed.width, ed.height), ImGuiCond_FirstUseEver);
if (ed.x != 0.f || ed.y != 0.f)
ImGui::SetNextWindowPos(ImVec2(ed.x, ed.y), ImGuiCond_FirstUseEver);
ImVec4 accent(0.35f, 0.60f, 1.00f, 1.f), bg(0.10f, 0.11f, 0.13f, 1.f);
ImVec4 panel(0.14f, 0.15f, 0.18f, 1.f), text(0.92f, 0.93f, 0.95f, 1.f);
float radius = 4.f, fontScale = 1.f;
if (ed.theme) {
if (ed.theme->has("accent")) {
const auto c = varFloats(ed.theme->get("accent"));
if (c.size() >= 3) accent = ImVec4(c[0], c[1], c[2], 1.f);
}
if (ed.theme->has("bg")) {
const auto c = varFloats(ed.theme->get("bg"));
if (c.size() >= 3) bg = ImVec4(c[0], c[1], c[2], 1.f);
}
if (ed.theme->has("panel")) {
const auto c = varFloats(ed.theme->get("panel"));
if (c.size() >= 3) panel = ImVec4(c[0], c[1], c[2], 1.f);
}
if (ed.theme->has("text")) {
const auto c = varFloats(ed.theme->get("text"));
if (c.size() >= 3) text = ImVec4(c[0], c[1], c[2], 1.f);
}
radius = floatOf(ed.theme, "radius", 4.f);
fontScale = floatOf(ed.theme, "fontScale", 1.f);
if (strOf(ed.theme, "preset") == "light") {
bg = ImVec4(0.93f, 0.93f, 0.93f, 1.f);
panel = ImVec4(0.84f, 0.84f, 0.86f, 1.f);
text = ImVec4(0.10f, 0.10f, 0.12f, 1.f);
}
}
ImGui::PushStyleColor(ImGuiCol_WindowBg, bg);
ImGui::PushStyleColor(ImGuiCol_ChildBg, panel);
ImGui::PushStyleColor(ImGuiCol_Text, text);
ImGui::PushStyleColor(ImGuiCol_Button, accent);
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
ImVec4(accent.x + 0.08f, accent.y + 0.08f, accent.z + 0.08f, 1.f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive,
ImVec4(accent.x - 0.05f, accent.y - 0.05f, accent.z - 0.05f, 1.f));
ImGui::PushStyleColor(ImGuiCol_Header, accent);
ImGui::PushStyleColor(ImGuiCol_SliderGrab, accent);
ImGui::PushStyleColor(ImGuiCol_CheckMark, accent);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, radius);
ImGui::PushStyleVar(ImGuiStyleVar_GrabRounding, radius);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, radius);
bool open = true;
ImGuiWindowFlags flags = ImGuiWindowFlags_NoSavedSettings;
if (!ed.resizable) flags |= ImGuiWindowFlags_NoResize;
if (!ed.collapsible) flags |= ImGuiWindowFlags_NoCollapse;
if (ImGui::Begin((ed.title + "##" + ed.id).c_str(), &open, flags)) {
if (fontScale != 1.f) ImGui::SetWindowFontScale(fontScale);
renderChildren(I, ed,
(ed.view && ed.view->has("children")) ? ed.view->getArray("children")
: nullptr,
ed.layout);
}
if (!open) {
emitEvent(ed, "closed", "", Var());
ed.removed = true;
}
ImGui::End();
if (fontScale != 1.f) ImGui::SetWindowFontScale(1.f);
ImGui::PopStyleVar(3);
ImGui::PopStyleColor(9);
}
} // namespace
void EditorHost::renderImGui() {
if (!impl_ || !impl_->windowOpen || !impl_->ui) return;
auto& I = *impl_;
for (auto& [id, ed] : I.editors) {
syncVMToView(I, ed);
renderEditor(I, ed);
}
for (auto it = I.editors.begin(); it != I.editors.end();) {
if (it->second.removed) it = I.editors.erase(it);
else ++it;
}
}
void EditorHost::frame() {
if (!impl_ || !impl_->windowOpen || !impl_->vm) return;
auto& I = *impl_;
I.inFrame = true;
if (!I.event) I.event = ModuleManager::requireInstance<eve::event::Event>("Event");
if (!I.timer) I.timer = ModuleManager::requireInstance<eve::timer::Timer>("Timer");
if (!I.gfx) {
I.inFrame = false;
return;
}
I.event->pump();
while (eve::event::Message* m = I.event->poll()) {
const std::string name = m->name;
delete m;
if (name == "quit") {
closeWindow();
requestExit();
I.inFrame = false;
return;
}
}
const float dt = I.timer ? I.timer->step() : 0.f;
callRootFunc(I.vm->getHandle(), "eve_host_update", {Var(static_cast<double>(dt))});
I.gfx->clearScreen();
callRootFunc(I.vm->getHandle(), "eve_host_render", {});
if (I.ui) {
I.ui->beginFrameAndRender();
renderImGui();
}
I.gfx->present();
if (I.ui) I.ui->dispatchEvents();
I.inFrame = false;
}
namespace {
Var parseEditorValue(const std::string& jsonValue) {
Var v;
try {
Poco::JSON::Parser p;
v = p.parse(jsonValue);
} catch (...) {
// Poco's parser wants a top-level JSON value; some clients pass bare
// numbers/booleans without JSON quoting. Handle those explicitly.
if (jsonValue == "true")
v = Var(true);
else if (jsonValue == "false")
v = Var(false);
else if (jsonValue == "null")
v = Var();
else {
char* end = nullptr;
const double d = std::strtod(jsonValue.c_str(), &end);
if (end && end != jsonValue.c_str() && *end == '\0')
v = Var(d);
else
v = Var(jsonValue); // plain text
}
}
return v;
}
} // namespace
std::string EditorHost::setEditorValue(const std::string& editorId, const std::string& widgetId,
const std::string& jsonValue) {
if (!impl_) return "error: host not started";
auto it = impl_->editors.find(editorId);
if (it == impl_->editors.end()) return "error: editor not found: " + editorId;
if (widgetId.empty()) return "error: missing widget";
const Var v = parseEditorValue(jsonValue);
setWidgetValue(*impl_, it->second, widgetId, v);
return "ok";
}
std::string EditorHost::restoreEditorValue(const std::string& editorId, const std::string& widgetId,
const std::string& jsonValue) {
if (!impl_ || !impl_->vm) return "error: host not started";
auto it = impl_->editors.find(editorId);
if (it == impl_->editors.end()) return "error: editor not found: " + editorId;
if (widgetId.empty()) return "error: missing widget";
const Var v = parseEditorValue(jsonValue);
it->second.values[widgetId] = v;
auto vmIt = impl_->vms.find(it->second.vmName);
const auto widget = findWidget(it->second.view, widgetId);
if (vmIt != impl_->vms.end() && widget) {
const std::string bind = strOf(widget, "bind");
if (!bind.empty()) writeVMVar(*impl_->vm, vmIt->second, bind, v);
}
return "ok";
}
std::string EditorHost::consumeEvents(const std::string& editorId) {
if (!impl_) return "[]";
Array::Ptr arr = new Array();
auto drain = [&](Editor& ed) {
for (const auto& e : ed.events) {
Object::Ptr eo = new Object();
eo->set("editor", ed.id);
eo->set("widget", e.widget);
eo->set("type", e.type);
if (!e.value.isEmpty()) eo->set("value", e.value);
arr->add(eo);
}
ed.events.clear();
};
if (!editorId.empty()) {
auto it = impl_->editors.find(editorId);
if (it == impl_->editors.end()) return "error: editor not found: " + editorId;
drain(it->second);
} else {
for (auto& [k, ed] : impl_->editors) drain(ed);
}
return jsonStringify(Var(arr));
}
std::string EditorHost::widgetRect(const std::string& editorId, const std::string& widgetId) const {
if (!impl_) return "{\"x\":0,\"y\":0,\"width\":0,\"height\":0}";
auto it = impl_->editors.find(editorId);
if (it == impl_->editors.end()) return "{\"x\":0,\"y\":0,\"width\":0,\"height\":0}";
const Editor& ed = it->second;
auto mn = ed.rectMin.find(widgetId), mx = ed.rectMax.find(widgetId);
Object::Ptr o = new Object();
if (mn != ed.rectMin.end() && mx != ed.rectMax.end()) {
o->set("x", static_cast<double>(mn->second.x));
o->set("y", static_cast<double>(mn->second.y));
o->set("width", static_cast<double>(mx->second.x - mn->second.x));
o->set("height", static_cast<double>(mx->second.y - mn->second.y));
} else {
o->set("x", 0);
o->set("y", 0);
o->set("width", 0);
o->set("height", 0);
}
return jsonStringify(Var(o));
}
std::string EditorHost::registerVM(const std::string& name, const std::string& source) {
if (!impl_ || !impl_->vm) return "error: host not started";
if (name.empty()) return "error: missing name";
const std::string err = runSquirrel(impl_->vm->getHandle(), source, "host_vm.nut");
if (!err.empty()) return "error: " + err;
try {
ssq::Table tbl = impl_->vm->find(name.c_str()).toTable();
impl_->vms[name] = tbl;
impl_->vmSources[name] = source;
return "ok";
} catch (const std::exception& e) {
return std::string("error: table '") + name + "' not found after run: " + e.what();
}
}
std::string EditorHost::unregisterVM(const std::string& name) {
if (!impl_) return "error: host not started";
if (name.empty()) return "error: missing name";
impl_->vms.erase(name);
impl_->vmSources.erase(name);
if (impl_->vm) {
HSQUIRRELVM v = impl_->vm->getHandle();
const SQInteger top = sq_gettop(v);
sq_pushroottable(v);
sq_pushstring(v, name.c_str(), static_cast<SQInteger>(name.size()));
sq_deleteslot(v, -2, SQFalse);
sq_pop(v, 1);
sq_settop(v, top);
}
return "ok";
}
std::string EditorHost::saveEditor(const std::string& id) {
if (!impl_) return "error: host not started";
auto it = impl_->editors.find(id);
if (it == impl_->editors.end()) return "error: editor not found: " + id;
try {
std::error_code ec;
std::filesystem::path dir = std::filesystem::path(impl_->rootDir) / "editors";
std::filesystem::create_directories(dir, ec);
std::filesystem::path vp = dir / (id + ".editor.json");
{
std::ofstream ofs(vp, std::ios::trunc);
if (!ofs) return "error: cannot write " + vp.string();
ofs << jsonStringify(Var(it->second.view));
}
if (!it->second.vmName.empty()) {
auto vs = impl_->vmSources.find(it->second.vmName);
if (vs != impl_->vmSources.end()) {
std::filesystem::path sp = dir / (id + ".vm.nut");
std::ofstream ofs2(sp, std::ios::trunc);
if (!ofs2) return "error: cannot write " + sp.string();
ofs2 << vs->second;
}
}
return "ok";
} catch (const std::exception& e) {
return std::string("error: ") + e.what();
}
}
std::string EditorHost::unloadEditor(const std::string& id) {
if (!impl_) return "error: host not started";
auto it = impl_->editors.find(id);
if (it == impl_->editors.end()) return "error: editor not found: " + id;
impl_->editors.erase(it);
return "ok";
}
void EditorHost::loadEditorsFromDisk() {
if (!impl_) return;
std::error_code ec;
std::filesystem::path dir = std::filesystem::path(impl_->rootDir) / "editors";
if (!std::filesystem::is_directory(dir, ec)) return;
std::vector<std::pair<std::string, std::string>> pending;
for (auto& entry : std::filesystem::directory_iterator(dir, ec)) {
if (ec) break;
const std::filesystem::path p = entry.path();
const std::string name = p.filename().string();
const std::string suffix = ".editor.json";
if (p.extension() != ".json" || name.size() <= suffix.size() ||
name.compare(name.size() - suffix.size(), suffix.size(), suffix) != 0)
continue;
const std::string id = name.substr(0, name.size() - suffix.size());
std::ifstream ifs(p);
std::string json((std::istreambuf_iterator<char>(ifs)), std::istreambuf_iterator<char>());
if (!json.empty()) pending.emplace_back(id, json);
}
for (const auto& [id, json] : pending) {
try {
Object::Ptr root = parseObject(json);
const std::string vmName = strOf(root, "vm");
if (!vmName.empty()) {
std::ifstream ifs2(dir / (id + ".vm.nut"));
std::string src((std::istreambuf_iterator<char>(ifs2)),
std::istreambuf_iterator<char>());
if (!src.empty()) registerVM(vmName, src);
}
} catch (...) {
}
applyEditor(json);
}
}
std::string EditorHost::runScript(const std::string& source) {
if (!impl_ || !impl_->vm) return "error: host not started";
if (source.empty()) return "error: missing source";
const std::string err = runSquirrel(impl_->vm->getHandle(), source, "host_snippet.nut");
return err.empty() ? "ok" : ("error: " + err);
}
std::string EditorHost::capture(const std::string& path) {
if (!impl_ || !impl_->gfx) return "error: no window";
auto& I = *impl_;
try {
std::string p = path.empty() ? "eve_host_capture.png" : path;
I.gfx->setScreenReadbackEnabled(true);
eve::image::ImageData* img = nullptr;
try {
img = I.gfx->newImageData();
} catch (...) {
// No presented frame yet (e.g. capture right after the window was
// opened). Render one frame with readback enabled, then retry.
if (I.inFrame) throw; // cannot present reentrantly from a script hook
frame();
img = I.gfx->newImageData();
}
if (!img) return "error: readback returned no image";
const int w = I.gfx->getPixelWidth();
const int h = I.gfx->getPixelHeight();
eve::filesystem::FileData* png =
img->encode(eve::image::ImageData::FormatHandler::ENCODED_PNG, p.c_str(), false);
delete img;
if (!png) return "error: PNG encode failed";
std::error_code ec;
std::filesystem::create_directories(std::filesystem::path(p).parent_path(), ec);
std::ofstream out(p, std::ios::binary | std::ios::trunc);
if (!out.good()) {
delete png;
return "error: cannot write " + p;
}
out.write(static_cast<const char*>(png->getData()),
static_cast<std::streamsize>(png->getSize()));
out.close();
const bool wrote = out.good();
delete png;
if (!wrote) return "error: failed writing " + p;
Object::Ptr o = new Object();
o->set("path", p);
o->set("width", w);
o->set("height", h);
return jsonStringify(Var(o));
} catch (const std::exception& e) {
return std::string("error: ") + e.what();
}
}
std::string EditorHost::status() const {
if (!impl_) return "{\"running\":false,\"windowOpen\":false,\"editors\":[],\"viewModels\":[]}";
Object::Ptr o = new Object();
o->set("running", running_);
o->set("windowOpen", impl_->windowOpen);
o->set("windowTitle", impl_->windowTitle);
o->set("gameRoot", gameRoot_);
Array::Ptr eds = new Array();
for (const auto& [k, ed] : impl_->editors) {
Object::Ptr e = new Object();
e->set("id", k);
e->set("title", ed.title);
eds->add(e);
}
o->set("editors", eds);
Array::Ptr vms = new Array();
for (const auto& [k, v] : impl_->vms) vms->add(k);
o->set("viewModels", vms);
return jsonStringify(Var(o));
}
void EditorHost::exposeScriptApi(ssq::VM& vm) {
try {
ssq::Table eveTbl = vm.find("eve").toTable();
ssq::Table host = eveTbl.addTable("host");
host.addFunc("status", []() { return EditorHost::instance().status(); });
host.addFunc("openWindow", [](std::string t, int w, int h) {
return EditorHost::instance().openWindow(t, w, h);
});
host.addFunc("closeWindow", []() { return EditorHost::instance().closeWindow(); });
host.addFunc("windowState", []() { return EditorHost::instance().windowState(); });
host.addFunc("applyEditor", [](std::string json) {
return EditorHost::instance().applyEditor(json);
});
host.addFunc("removeEditor", [](std::string id) {
return EditorHost::instance().removeEditor(id);
});
host.addFunc("setValue", [](std::string editor, std::string widget, std::string value) {
return EditorHost::instance().setEditorValue(editor, widget, value);
});
host.addFunc("events", [](std::string editor) {
return EditorHost::instance().consumeEvents(editor);
});
host.addFunc("registerVM", [](std::string name, std::string source) {
return EditorHost::instance().registerVM(name, source);
});
host.addFunc("unregisterVM", [](std::string name) {
return EditorHost::instance().unregisterVM(name);
});
host.addFunc("widgetRect", [](std::string editor, std::string widget) {
return EditorHost::instance().widgetRect(editor, widget);
});
host.addFunc("capture", [](std::string path) {
return EditorHost::instance().capture(path);
});
host.addFunc("save", [](std::string id) {
return EditorHost::instance().saveEditor(id);
});
host.addFunc("runScript", [](std::string source) {
return EditorHost::instance().runScript(source);
});
host.addFunc("reloadResource", [](std::string path) {
return EditorHost::instance().reloadResource(path);
});
host.addFunc("hotReloadStatus", []() {
return EditorHost::instance().hotReloadStatus();
});
} catch (...) {
// eve table missing — the host surface is optional.
}
}
} // namespace eve::ui
#endif // EVENGINE_WEBGPU && __EMSCRIPTEN__