载入中...
搜索中...
未找到
EditorHost.cpp
浏览该文件的文档.
1#include "ui/EditorHost.h"
2
3#include "common/config.h"
4
5#if defined(EVENGINE_WEBGPU) && defined(__EMSCRIPTEN__)
6
7// The browser runtime trims Poco / DevTools / the host path. Keep EVUI
8// linkable with a no-op implementation; `eve mcp` is desktop-only anyway.
9namespace eve::ui {
10struct EditorHost::Impl {};
11
12EditorHost& EditorHost::instance() {
13 static EditorHost* inst = new EditorHost();
14 return *inst;
15}
16EditorHost::EditorHost() = default;
17EditorHost::~EditorHost() = default;
18void EditorHost::start(ssq::VM&, const std::string&, bool) {}
19void EditorHost::stop() {}
20bool EditorHost::windowOpen() const { return false; }
21std::string EditorHost::openWindow(const std::string&, int, int) {
22 return "error: editor host unavailable on this platform";
23}
24std::string EditorHost::closeWindow() { return "ok"; }
25std::string EditorHost::windowState() const { return "{}"; }
26std::string EditorHost::applyEditor(const std::string&) {
27 return "error: editor host unavailable on this platform";
28}
29std::string EditorHost::removeEditor(const std::string&) {
30 return "error: editor host unavailable on this platform";
31}
32std::string EditorHost::listEditors() const { return "{\"editors\":[]}"; }
33std::string EditorHost::editorState(const std::string&) const { return "{\"editors\":[]}"; }
35std::string EditorHost::setEditorValue(const std::string&, const std::string&, const std::string&) {
36 return "error: editor host unavailable on this platform";
37}
38std::string EditorHost::consumeEvents(const std::string&) { return "[]"; }
39std::string EditorHost::widgetRect(const std::string&, const std::string&) const {
40 return "{\"x\":0,\"y\":0,\"width\":0,\"height\":0}";
41}
42std::string EditorHost::registerVM(const std::string&, const std::string&) {
43 return "error: editor host unavailable on this platform";
44}
45std::string EditorHost::unregisterVM(const std::string&) {
46 return "error: editor host unavailable on this platform";
47}
48std::string EditorHost::saveEditor(const std::string&) {
49 return "error: editor host unavailable on this platform";
50}
51std::string EditorHost::unloadEditor(const std::string&) {
52 return "error: editor host unavailable on this platform";
53}
55std::string EditorHost::runScript(const std::string&) {
56 return "error: editor host unavailable on this platform";
57}
58std::string EditorHost::capture(const std::string&) {
59 return "error: editor host unavailable on this platform";
60}
61std::string EditorHost::status() const {
62 return "{\"running\":false,\"windowOpen\":false,\"editors\":[],\"viewModels\":[]}";
63}
64void EditorHost::frame() {}
66void EditorHost::exposeScriptApi(ssq::VM&) {}
67} // namespace eve::ui
68
69#else // full implementation
70
71#include "common/Module.h"
72#include "event/Event.h"
73#include "filesystem/FileData.h"
74#include "graphics/Graphics.h"
75#include "image/ImageData.h"
76#include "timer/Timer.h"
77#include "ui/UI.h"
78#include "window/Window.h"
79
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>
86
87#include <imgui.h>
88#include <simplesquirrel/simplesquirrel.hpp>
89#include <squirrel.h>
90
91#include <algorithm>
92#include <cstdlib>
93#include <cstring>
94#include <cstdio>
95#include <fstream>
96#include <filesystem>
97#include <fstream>
98#include <functional>
99#include <map>
100#include <sstream>
101#include <string>
102#include <utility>
103#include <vector>
104
105namespace eve::ui {
106namespace {
107
108using Poco::Dynamic::Var;
109using Poco::JSON::Array;
110using Poco::JSON::Object;
111using Poco::JSON::Parser;
112
113std::string jsonStringify(const Var& v) {
114 std::ostringstream oss;
115 Poco::JSON::Stringifier::stringify(v, oss, 0, 0);
116 return oss.str();
117}
118
119Object::Ptr parseObject(const std::string& json) {
120 Poco::JSON::Parser parser;
121 return parser.parse(json).extract<Object::Ptr>();
122}
123
124std::string strOf(Object::Ptr o, const char* key, const std::string& def = {}) {
125 if (!o || !o->has(key)) return def;
126 try {
127 return o->get(key).convert<std::string>();
128 } catch (...) {
129 return def;
130 }
131}
132
133int intOf(Object::Ptr o, const char* key, int def = 0) {
134 if (!o || !o->has(key)) return def;
135 try {
136 return o->get(key).convert<int>();
137 } catch (...) {
138 return def;
139 }
140}
141
142float floatOf(Object::Ptr o, const char* key, float def = 0.f) {
143 if (!o || !o->has(key)) return def;
144 try {
145 return static_cast<float>(o->get(key).convert<double>());
146 } catch (...) {
147 return def;
148 }
149}
150
151bool boolOf(Object::Ptr o, const char* key, bool def = false) {
152 if (!o || !o->has(key)) return def;
153 try {
154 return o->get(key).convert<bool>();
155 } catch (...) {
156 return def;
157 }
158}
159
160float varFloat(const Var& v, float def = 0.f) {
161 try {
162 return static_cast<float>(v.convert<double>());
163 } catch (...) {
164 return def;
165 }
166}
167
168bool varBool(const Var& v, bool def = false) {
169 try {
170 return v.convert<bool>();
171 } catch (...) {
172 return def;
173 }
174}
175
176std::string varString(const Var& v, const std::string& def = {}) {
177 try {
178 return v.convert<std::string>();
179 } catch (...) {
180 return def;
181 }
182}
183
184Array::Ptr varToArray(const Var& v) {
185 try {
186 return v.extract<Array::Ptr>();
187 } catch (...) {
188 return nullptr;
189 }
190}
191
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) {
197 try {
198 out.push_back(static_cast<float>(arr->get(i).convert<double>()));
199 } catch (...) {
200 out.push_back(0.f);
201 }
202 }
203 return out;
204}
205
207std::vector<float> widgetFloatInit(const Var& v) {
208 std::vector<float> init = varFloats(v);
209 if (!init.empty()) return init;
210 try {
211 init.push_back(static_cast<float>(v.convert<double>()));
212 } catch (...) {
213 }
214 return init;
215}
216
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)));
222 return out;
223}
224
225// ---- raw Squirrel helpers -------------------------------------------------
226
227Var stackVar(HSQUIRRELVM vm, SQInteger index) {
228 switch (sq_gettype(vm, index)) {
229 case OT_NULL:
230 return Var();
231 case OT_BOOL: {
232 SQBool b = SQFalse;
233 if (SQ_SUCCEEDED(sq_getbool(vm, index, &b))) return Var(bool(b));
234 return Var();
235 }
236 case OT_INTEGER: {
237 SQInteger i = 0;
238 if (SQ_SUCCEEDED(sq_getinteger(vm, index, &i)))
239 return Var(static_cast<Poco::Int64>(i));
240 return Var();
241 }
242 case OT_FLOAT: {
243 SQFloat f = 0.f;
244 if (SQ_SUCCEEDED(sq_getfloat(vm, index, &f)))
245 return Var(static_cast<double>(f));
246 return Var();
247 }
248 case OT_STRING: {
249 const SQChar* s = nullptr;
250 if (SQ_SUCCEEDED(sq_getstring(vm, index, &s)) && s) return Var(std::string(s));
251 return Var();
252 }
253 case OT_ARRAY: {
254 HSQOBJECT obj;
255 if (SQ_FAILED(sq_getstackobj(vm, index, &obj))) return Var();
256 sq_pushobject(vm, obj);
257 sq_pushnull(vm);
258 Array::Ptr arr = new Array();
259 while (SQ_SUCCEEDED(sq_next(vm, -2))) {
260 arr->add(stackVar(vm, -1));
261 sq_pop(vm, 2);
262 }
263 sq_pop(vm, 1);
264 return Var(arr);
265 }
266 default:
267 return Var();
268 }
269}
270
271void pushVar(HSQUIRRELVM vm, const Var& v) {
272 if (v.isEmpty()) {
273 sq_pushnull(vm);
274 return;
275 }
276 try {
277 if (v.isBoolean()) {
278 sq_pushbool(vm, v.convert<bool>() ? SQTrue : SQFalse);
279 return;
280 }
281 if (v.isNumeric()) {
282 sq_pushfloat(vm, static_cast<SQFloat>(v.convert<double>()));
283 return;
284 }
285 if (v.isString()) {
286 const std::string s = v.convert<std::string>();
287 sq_pushstring(vm, s.c_str(), static_cast<SQInteger>(s.size()));
288 return;
289 }
290 if (v.isArray()) {
291 Array::Ptr arr;
292 try {
293 arr = v.extract<Array::Ptr>();
294 } catch (...) {
295 sq_pushnull(vm);
296 return;
297 }
298 sq_newarray(vm, 0);
299 if (arr) {
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;
303 }
304 }
305 return;
306 }
307 } catch (...) {
308 }
309 sq_pushnull(vm);
310}
311
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()),
316 name, SQTrue))) {
317 sq_settop(vm, top);
318 return "compile failed";
319 }
320 sq_pushroottable(vm);
321 if (SQ_FAILED(sq_call(vm, 1, SQFalse, SQTrue))) {
322 sq_settop(vm, top);
323 return "runtime failed";
324 }
325 sq_settop(vm, top);
326 return {};
327}
328
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))) {
334 sq_settop(vm, top);
335 return; // hook not defined
336 }
337 if (sq_gettype(vm, -1) != OT_CLOSURE) {
338 sq_settop(vm, top);
339 return;
340 }
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());
345 sq_settop(vm, top);
346}
347
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);
352 try {
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;
359 sq_settop(vm, top);
360 return false;
361 }
362 sq_settop(vm, top);
363 return true;
364 } catch (const ssq::NotFoundException&) {
365 sq_settop(vm, top);
366 return true; // optional callback / command absent
367 } catch (const std::exception& e) {
368 if (err) *err = e.what();
369 sq_settop(vm, top);
370 return false;
371 }
372}
373
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) {
380 slot = rest;
381 nested.clear();
382 } else if (rest.find('.', dot + 1) == std::string::npos) {
383 slot = rest.substr(0, dot);
384 nested = rest.substr(dot + 1);
385 } else {
386 return false; // only one nesting level supported in v1
387 }
388 return !slot.empty();
389}
390
391Var objectToVar(ssq::VM& vm, const ssq::Object& o) {
392 // Push the object and convert through the stack so arrays recurse cleanly.
393 HSQUIRRELVM v = vm.getHandle();
394 const SQInteger top = sq_gettop(v);
395 sq_pushobject(v, o.getRaw());
396 Var out = stackVar(v, -1);
397 sq_settop(v, top);
398 return out;
399}
400
401Var readVMVar(ssq::VM& vm, const ssq::Table& tbl, const std::string& bind, bool* ok = nullptr) {
402 std::string slot, nested;
403 if (ok) *ok = false;
404 if (!splitBindPath(bind, slot, nested)) return Var();
405 try {
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());
410 }
411 if (ok) *ok = true;
412 return objectToVar(vm, o);
413 } catch (...) {
414 return Var();
415 }
416}
417
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;
421 HSQUIRRELVM v = vm.getHandle();
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()));
426 pushVar(v, value);
427 sq_newslot(v, -3, false);
428 } else {
429 try {
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()));
433 pushVar(v, value);
434 sq_newslot(v, -3, false);
435 } catch (...) {
436 }
437 }
438 sq_settop(v, top);
439}
440
441struct HostEvent {
442 std::string type; // "click" | "change" | "closed"
443 std::string widget;
444 Var value;
445};
446
447struct Editor {
448 std::string id;
449 std::string title;
450 std::string vmName;
451 Object::Ptr view;
452 float x = 0.f, y = 0.f, width = 0.f, height = 0.f;
453 bool resizable = true;
454 bool collapsible = true;
455 std::string layout = "vertical";
456 Object::Ptr theme;
457 std::map<std::string, Var> values;
458 std::vector<HostEvent> events;
459 std::map<std::string, ImVec2> rectMin, rectMax;
460 bool removed = false;
461};
462
463void forEachWidget(Object::Ptr parent, const std::function<void(Object::Ptr)>& fn) {
464 if (!parent) return;
465 Array::Ptr kids = parent->has("children") ? parent->getArray("children") : nullptr;
466 if (!kids) return;
467 for (size_t i = 0; i < kids->size(); ++i) {
468 Object::Ptr w = kids->getObject(static_cast<unsigned>(i));
469 if (!w) continue;
470 fn(w);
471 forEachWidget(w, fn);
472 }
473}
474
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));
481 if (!w) continue;
482 if (strOf(w, "id") == id) return w;
483 if (Object::Ptr sub = findWidget(w, id)) return sub;
484 }
485 return nullptr;
486}
487
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;
494}
495
497std::string vmFuncName(const std::string& path) {
498 if (path.rfind("vm.", 0) == 0) return path.substr(3);
499 return path;
500}
501
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();
506}
507
508void emitEvent(Editor& ed, const std::string& type, const std::string& widget, const Var& value) {
509 ed.events.push_back({type, widget, value});
510}
511
512void renderChildren(EditorHost::Impl& I, Editor& ed, Array::Ptr kids, const std::string& layout);
513void syncVMToView(EditorHost::Impl& I, Editor& ed);
514
515} // namespace
516
517// Out-of-line definition of the pimpl. Lives at eve::ui scope (not inside the
518// anonymous namespace) so it can be named as eve::ui::EditorHost::Impl.
520 ssq::VM* vm = nullptr;
521 bool allowWindow = true;
522 std::string rootDir;
525 eve::ui::UI* ui = nullptr;
526 eve::event::Event* event = nullptr;
528 bool windowOpen = false;
529 bool inFrame = false;
530 std::string windowTitle;
531 std::map<std::string, Editor> editors;
532 std::map<std::string, ssq::Table> vms;
533 std::map<std::string, std::string> vmSources;
534};
535
536// ---------------------------------------------------------------------------
537// public surface
538// ---------------------------------------------------------------------------
539
541 // Intentionally leaked: the host VM may be torn down after the singleton.
542 static EditorHost* inst = new EditorHost();
543 return *inst;
544}
545
546EditorHost::EditorHost() = default;
547EditorHost::~EditorHost() = default;
548
549void EditorHost::start(ssq::VM& vm, const std::string& gameRoot, bool allowWindow) {
550 if (running_) return;
551 impl_ = std::make_unique<Impl>();
552 impl_->vm = &vm;
553 impl_->allowWindow = allowWindow;
554 allowWindow_ = allowWindow;
555 gameRoot_ = gameRoot.empty() ? std::filesystem::current_path().string() : gameRoot;
556 impl_->rootDir = gameRoot_;
557 running_ = true;
559}
560
562 if (!running_) return;
563 closeWindow();
564 impl_.reset();
565 running_ = false;
566 exitRequested_ = false;
567}
568
569std::string EditorHost::openWindow(const std::string& title, int width, int height) {
570 if (!impl_) return "error: host not started";
571 auto& I = *impl_;
572 if (!I.allowWindow) return "error: window creation disabled";
573 try {
574 if (!I.win) I.win = ModuleManager::requireInstance<eve::window::Window>("Window");
575 if (!I.gfx) I.gfx = ModuleManager::requireInstance<eve::graphics::Graphics>("Graphics");
577 s.width = static_cast<uint16_t>(width > 0 ? width : 1280);
578 s.height = static_cast<uint16_t>(height > 0 ? height : 800);
579 s.centered = true;
580 s.resizable = true;
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");
584 I.windowOpen = true;
585 I.windowTitle = title;
586 // Keep screen readback on while the host window is open: the AI's whole
587 // feedback loop is "render -> capture", so every presented frame should
588 // be available to eve_host_capture immediately (the Vulkan backend
589 // installs the present-copy hook as soon as readback is enabled).
590 if (I.gfx) I.gfx->setScreenReadbackEnabled(true);
591 return "ok";
592 } catch (const std::exception& e) {
593 return std::string("error: ") + e.what();
594 }
595}
596
598 if (!impl_) return "ok";
599 auto& I = *impl_;
600 if (I.windowOpen && I.win) {
601 try {
602 I.win->close();
603 } catch (...) {
604 }
605 if (I.ui) I.ui->shutdownBackend();
606 I.windowOpen = false;
607 }
608 return "ok";
609}
610
612 return impl_ && impl_->windowOpen;
613}
614
615std::string EditorHost::windowState() const {
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);
620 if (impl_->win) {
621 o->set("width", impl_->win->getWidth());
622 o->set("height", impl_->win->getHeight());
623 }
624 return jsonStringify(Var(o));
625}
626
627std::string EditorHost::applyEditor(const std::string& json) {
628 if (!impl_) return "error: host not started";
629 auto& I = *impl_;
630 try {
631 Object::Ptr root = parseObject(json);
632 const std::string id = strOf(root, "id");
633 if (id.empty()) return "error: editor requires id";
634 Editor ed;
635 ed.id = id;
636 ed.title = strOf(root, "title", id);
637 ed.vmName = strOf(root, "vm");
638 ed.view = root;
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");
651 });
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;
656 }
657 return editorState(id);
658 } catch (const std::exception& e) {
659 return std::string("error: ") + e.what();
660 }
661}
662
663std::string EditorHost::removeEditor(const std::string& id) {
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);
668 return "ok";
669}
670
671std::string EditorHost::listEditors() const {
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();
676 o->set("id", k);
677 o->set("title", ed.title);
678 o->set("vm", ed.vmName);
679 arr->add(o);
680 }
681 Object::Ptr root = new Object();
682 root->set("editors", arr);
683 return jsonStringify(Var(root));
684}
685
686namespace {
687
688Poco::JSON::Object::Ptr editorStateObject(const Editor& ed) {
689 Object::Ptr o = new Object();
690 o->set("id", ed.id);
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);
703 evs->add(eo);
704 }
705 o->set("events", evs);
706 return o;
707}
708
709} // namespace
710
711std::string EditorHost::editorState(const std::string& id) const {
712 if (!impl_) return "{\"editors\":[]}";
713 const_cast<EditorHost*>(this)->syncBindings();
714 Array::Ptr arr = new Array();
715 if (!id.empty()) {
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));
719 } else {
720 for (const auto& [k, ed] : impl_->editors) arr->add(editorStateObject(ed));
721 }
722 Object::Ptr root = new Object();
723 root->set("editors", arr);
724 return jsonStringify(Var(root));
725}
726
728 if (!impl_) return;
729 for (auto& [k, ed] : impl_->editors) syncVMToView(*impl_, ed);
730}
731
732namespace {
733
734void pushViewToVM(EditorHost::Impl& I, Editor& ed, const std::string& widgetId, const Var& value) {
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);
739 if (!w) return;
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()) {
744 std::string err;
745 callTableFunc(I.vm->getHandle(), it->second, vmFuncName(onChange),
746 {Var(widgetId), value}, &err);
747 if (!err.empty())
748 fprintf(stderr, "eve.host: onChange %s: %s\n", onChange.c_str(), err.c_str());
749 }
750}
751
752void setWidgetValue(EditorHost::Impl& I, Editor& ed, const std::string& widgetId,
753 const Var& value) {
754 ed.values[widgetId] = value;
755 emitEvent(ed, "change", widgetId, value);
756 pushViewToVM(I, ed, widgetId, value);
757}
758
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));
773 }
774 const std::string optBind = strOf(w, "bind:options");
775 if (!optBind.empty()) {
776 const Var v = readVMVar(*I.vm, it->second, optBind);
777 if (!v.isEmpty()) {
778 Array::Ptr opts = new Array();
779 for (const auto& s : varStrings(v)) opts->add(s);
780 w->set("options", opts);
781 }
782 }
783}
784
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))
797 ed.values[id] = v;
798 });
799}
800
801void renderWidget(EditorHost::Impl& I, Editor& ed, Object::Ptr w);
802
803void renderChildren(EditorHost::Impl& I, Editor& ed, Array::Ptr kids, const std::string& layout) {
804 if (!kids) return;
805 for (size_t i = 0; i < kids->size(); ++i) {
806 Object::Ptr w = kids->getObject(static_cast<unsigned>(i));
807 if (!w) continue;
808 resolveOneWay(I, ed, w);
809 renderWidget(I, ed, w);
810 if (layout == "horizontal" && i + 1 < kids->size()) ImGui::SameLine();
811 }
812}
813
814void renderWidget(EditorHost::Impl& I, Editor& ed, Object::Ptr w) {
815 if (!w) return;
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);
821 if (!enabled) {
822 ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.5f);
823 }
824
825 const auto cur = [&](const char* key, const Var& def) -> Var {
826 if (!id.empty()) {
827 auto it = ed.values.find(id);
828 if (it != ed.values.end()) return it->second;
829 }
830 if (w->has(key)) return w->get(key);
831 return def;
832 };
833
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());
838 recordRect(ed, id);
839 } else if (type == "separator") {
840 ImGui::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());
848 recordRect(ed, id);
849 } else if (type == "plot") {
850 const std::vector<float> data = varFloats(cur("data", Var()));
851 if (!data.empty()) {
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)));
855 recordRect(ed, id);
856 }
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;
861 if (multi) {
862 changed = ImGui::InputTextMultiline(widgetIdLabel(w).c_str(), buf.data(),
863 buf.size() + 64,
864 ImVec2(floatOf(w, "width", 0.f), floatOf(w, "height", 96.f)));
865 } else {
866 buf.resize(512);
867 changed = ImGui::InputText(widgetIdLabel(w).c_str(), buf.data(), buf.size());
868 }
869 if (enabled && changed && !id.empty()) {
870 buf.resize(strlen(buf.c_str()));
871 setWidgetValue(I, ed, id, Var(buf));
872 }
873 recordRect(ed, id);
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));
889 }
890 recordRect(ed, id);
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));
903 }
904 recordRect(ed, id);
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));
909 recordRect(ed, id);
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")));
915 int idx = 0;
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()));
922 else
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)]));
928 recordRect(ed, id);
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()) {
936 std::string err;
937 callTableFunc(I.vm->getHandle(), it->second, vmFuncName(cmd),
938 {Var(ed.id), Var(id)}, &err);
939 if (!err.empty())
940 fprintf(stderr, "eve.host: command %s: %s\n", cmd.c_str(), err.c_str());
941 }
942 }
943 }
944 recordRect(ed, id);
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);
951 ImGui::TreePop();
952 }
953 recordRect(ed, id);
954 } else if (type == "group") {
955 const bool border = boolOf(w, "border", true);
956 if (border) {
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"));
963 }
964 ImGui::EndChild();
965 } else {
966 ImGui::BeginGroup();
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"));
971 ImGui::EndGroup();
972 }
973 recordRect(ed, id);
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));
979 if (!tab) continue;
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"));
984 ImGui::EndTabItem();
985 }
986 }
987 ImGui::EndTabBar();
988 }
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();
1001 if (rows) {
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());
1009 }
1010 }
1011 }
1012 ImGui::EndTable();
1013 }
1014 recordRect(ed, id);
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);
1023 if (bg.size() >= 3)
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());
1029 // AI scripts draw inside here using eve.host.widgetRect().
1030 }
1031 ImGui::EndChild();
1032 if (!id.empty()) {
1033 ed.rectMin[id] = ImGui::GetItemRectMin();
1034 ed.rectMax[id] = ImGui::GetItemRectMax();
1035 }
1036 }
1037
1038 const std::string tip = strOf(w, "tooltip");
1039 if (!tip.empty() && ImGui::IsItemHovered()) ImGui::SetTooltip("%s", tip.c_str());
1040 if (!enabled) {
1041 ImGui::PopStyleVar();
1042 }
1043}
1044
1045void renderEditor(EditorHost::Impl& I, Editor& ed) {
1046 if (ed.removed) return;
1047 if (ed.width > 0.f)
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);
1051
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;
1055 if (ed.theme) {
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);
1059 }
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);
1063 }
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);
1067 }
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);
1071 }
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);
1078 }
1079 }
1080
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);
1095
1096 bool open = true;
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")
1104 : nullptr,
1105 ed.layout);
1106 }
1107 if (!open) {
1108 emitEvent(ed, "closed", "", Var());
1109 ed.removed = true;
1110 }
1111 ImGui::End();
1112 if (fontScale != 1.f) ImGui::SetWindowFontScale(1.f);
1113 ImGui::PopStyleVar(3);
1114 ImGui::PopStyleColor(9);
1115}
1116
1117} // namespace
1118
1120 if (!impl_ || !impl_->windowOpen || !impl_->ui) return;
1121 auto& I = *impl_;
1122 for (auto& [id, ed] : I.editors) {
1123 syncVMToView(I, ed);
1124 renderEditor(I, ed);
1125 }
1126 for (auto it = I.editors.begin(); it != I.editors.end();) {
1127 if (it->second.removed) it = I.editors.erase(it);
1128 else ++it;
1129 }
1130}
1131
1133 if (!impl_ || !impl_->windowOpen || !impl_->vm) return;
1134 auto& I = *impl_;
1135 I.inFrame = true;
1136 if (!I.event) I.event = ModuleManager::requireInstance<eve::event::Event>("Event");
1137 if (!I.timer) I.timer = ModuleManager::requireInstance<eve::timer::Timer>("Timer");
1138 if (!I.gfx) {
1139 I.inFrame = false;
1140 return;
1141 }
1142
1143 I.event->pump();
1144 while (eve::event::Message* m = I.event->poll()) {
1145 const std::string name = m->name;
1146 delete m;
1147 if (name == "quit") {
1148 closeWindow();
1149 requestExit();
1150 I.inFrame = false;
1151 return;
1152 }
1153 }
1154
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", {});
1159 if (I.ui) {
1160 I.ui->beginFrameAndRender();
1161 renderImGui();
1162 }
1163 I.gfx->present();
1164 if (I.ui) I.ui->dispatchEvents();
1165 I.inFrame = false;
1166}
1167
1168std::string EditorHost::setEditorValue(const std::string& editorId, const std::string& widgetId,
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";
1174 Var v;
1175 try {
1176 Poco::JSON::Parser p;
1177 v = p.parse(jsonValue);
1178 } catch (...) {
1179 // Poco's parser wants a top-level JSON value; some clients pass bare
1180 // numbers/booleans without JSON quoting. Handle those explicitly.
1181 if (jsonValue == "true") v = Var(true);
1182 else if (jsonValue == "false") v = Var(false);
1183 else if (jsonValue == "null") v = Var();
1184 else {
1185 char* end = nullptr;
1186 const double d = std::strtod(jsonValue.c_str(), &end);
1187 if (end && end != jsonValue.c_str() && *end == '\0')
1188 v = Var(d);
1189 else
1190 v = Var(jsonValue); // plain text
1191 }
1192 }
1193 setWidgetValue(*impl_, it->second, widgetId, v);
1194 return "ok";
1195}
1196
1197std::string EditorHost::consumeEvents(const std::string& editorId) {
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);
1207 arr->add(eo);
1208 }
1209 ed.events.clear();
1210 };
1211 if (!editorId.empty()) {
1212 auto it = impl_->editors.find(editorId);
1213 if (it == impl_->editors.end()) return "error: editor not found: " + editorId;
1214 drain(it->second);
1215 } else {
1216 for (auto& [k, ed] : impl_->editors) drain(ed);
1217 }
1218 return jsonStringify(Var(arr));
1219}
1220
1221std::string EditorHost::widgetRect(const std::string& editorId, const std::string& widgetId) const {
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));
1233 } else {
1234 o->set("x", 0);
1235 o->set("y", 0);
1236 o->set("width", 0);
1237 o->set("height", 0);
1238 }
1239 return jsonStringify(Var(o));
1240}
1241
1242std::string EditorHost::registerVM(const std::string& name, const std::string& source) {
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;
1247 try {
1248 ssq::Table tbl = impl_->vm->find(name.c_str()).toTable();
1249 impl_->vms[name] = tbl;
1250 impl_->vmSources[name] = source;
1251 return "ok";
1252 } catch (const std::exception& e) {
1253 return std::string("error: table '") + name + "' not found after run: " + e.what();
1254 }
1255}
1256
1257std::string EditorHost::unregisterVM(const std::string& name) {
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);
1262 if (impl_->vm) {
1263 HSQUIRRELVM v = impl_->vm->getHandle();
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);
1268 sq_pop(v, 1);
1269 sq_settop(v, top);
1270 }
1271 return "ok";
1272}
1273
1274std::string EditorHost::saveEditor(const std::string& id) {
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;
1278 try {
1279 std::error_code ec;
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");
1283 {
1284 std::ofstream ofs(vp, std::ios::trunc);
1285 if (!ofs) return "error: cannot write " + vp.string();
1286 ofs << jsonStringify(Var(it->second.view));
1287 }
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();
1294 ofs2 << vs->second;
1295 }
1296 }
1297 return "ok";
1298 } catch (const std::exception& e) {
1299 return std::string("error: ") + e.what();
1300 }
1301}
1302
1303std::string EditorHost::unloadEditor(const std::string& id) {
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);
1308 return "ok";
1309}
1310
1312 if (!impl_) return;
1313 std::error_code ec;
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)) {
1318 if (ec) break;
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)
1324 continue;
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);
1329 }
1330 for (const auto& [id, json] : pending) {
1331 try {
1332 Object::Ptr root = parseObject(json);
1333 const std::string vmName = strOf(root, "vm");
1334 if (!vmName.empty()) {
1335 std::ifstream ifs2(dir / (id + ".vm.nut"));
1336 std::string src((std::istreambuf_iterator<char>(ifs2)),
1337 std::istreambuf_iterator<char>());
1338 if (!src.empty()) registerVM(vmName, src);
1339 }
1340 } catch (...) {
1341 }
1342 applyEditor(json);
1343 }
1344}
1345
1346std::string EditorHost::runScript(const std::string& source) {
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);
1351}
1352
1353std::string EditorHost::capture(const std::string& path) {
1354 if (!impl_ || !impl_->gfx) return "error: no window";
1355 auto& I = *impl_;
1356 try {
1357 std::string p = path.empty() ? "eve_host_capture.png" : path;
1358 I.gfx->setScreenReadbackEnabled(true);
1359 eve::image::ImageData* img = nullptr;
1360 try {
1361 img = I.gfx->newImageData();
1362 } catch (...) {
1363 // No presented frame yet (e.g. capture right after the window was
1364 // opened). Render one frame with readback enabled, then retry.
1365 if (I.inFrame) throw; // cannot present reentrantly from a script hook
1366 frame();
1367 img = I.gfx->newImageData();
1368 }
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);
1374 delete img;
1375 if (!png) return "error: PNG encode failed";
1376 std::error_code ec;
1377 std::filesystem::create_directories(std::filesystem::path(p).parent_path(), ec);
1378 std::ofstream out(p, std::ios::binary | std::ios::trunc);
1379 if (!out.good()) {
1380 delete png;
1381 return "error: cannot write " + p;
1382 }
1383 out.write(static_cast<const char*>(png->getData()),
1384 static_cast<std::streamsize>(png->getSize()));
1385 out.close();
1386 const bool wrote = out.good();
1387 delete png;
1388 if (!wrote) return "error: failed writing " + p;
1389 Object::Ptr o = new Object();
1390 o->set("path", p);
1391 o->set("width", w);
1392 o->set("height", h);
1393 return jsonStringify(Var(o));
1394 } catch (const std::exception& e) {
1395 return std::string("error: ") + e.what();
1396 }
1397}
1398
1399std::string EditorHost::status() const {
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();
1409 e->set("id", k);
1410 e->set("title", ed.title);
1411 eds->add(e);
1412 }
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));
1418}
1419
1421 try {
1422 ssq::Table eveTbl = vm.find("eve").toTable();
1423 ssq::Table host = eveTbl.addTable("host");
1424 host.addFunc("status", []() { return EditorHost::instance().status(); });
1425 host.addFunc("openWindow", [](std::string t, int w, int h) {
1426 return EditorHost::instance().openWindow(t, w, h);
1427 });
1428 host.addFunc("closeWindow", []() { return EditorHost::instance().closeWindow(); });
1429 host.addFunc("windowState", []() { return EditorHost::instance().windowState(); });
1430 host.addFunc("applyEditor", [](std::string json) {
1431 return EditorHost::instance().applyEditor(json);
1432 });
1433 host.addFunc("removeEditor", [](std::string id) {
1435 });
1436 host.addFunc("setValue", [](std::string editor, std::string widget, std::string value) {
1438 });
1439 host.addFunc("events", [](std::string editor) {
1440 return EditorHost::instance().consumeEvents(editor);
1441 });
1442 host.addFunc("registerVM", [](std::string name, std::string source) {
1443 return EditorHost::instance().registerVM(name, source);
1444 });
1445 host.addFunc("unregisterVM", [](std::string name) {
1447 });
1448 host.addFunc("widgetRect", [](std::string editor, std::string widget) {
1449 return EditorHost::instance().widgetRect(editor, widget);
1450 });
1451 host.addFunc("capture", [](std::string path) {
1452 return EditorHost::instance().capture(path);
1453 });
1454 host.addFunc("save", [](std::string id) {
1455 return EditorHost::instance().saveEditor(id);
1456 });
1457 host.addFunc("runScript", [](std::string source) {
1458 return EditorHost::instance().runScript(source);
1459 });
1460 } catch (...) {
1461 // eve table missing — the host surface is optional.
1462 }
1463}
1464
1465} // namespace eve::ui
1466
1467#endif // EVENGINE_WEBGPU && __EMSCRIPTEN__
struct SQVM * HSQUIRRELVM
std::string value
HSQUIRRELVM vm
Definition ECS.cpp:20
std::map< std::string, Var > values
std::map< std::string, ImVec2 > rectMin
bool collapsible
bool removed
std::string widget
Object::Ptr theme
std::map< std::string, ImVec2 > rectMax
std::string type
std::string vmName
std::string title
std::vector< HostEvent > events
std::string layout
bool resizable
std::string id
int y
Definition Grass.cpp:135
float height
Definition Grass.cpp:235
int x
Definition Grass.cpp:135
glm::vec3 n
Definition Grass.cpp:64
int h
int w
uint32_t a
uint32_t b
uint32_t c
int width
int idx
float f
glm::vec4 p[6]
glm::mat4 view
Light2D::Data * data
const char * name
Definition RockMesh.cpp:21
SettlementPipeline::Stage fn
bool enabled
int d
int v
int parent
Definition TreeMesh.cpp:175
V3 dir
Definition TreeMesh.cpp:121
float m[16]
uint32_t s
Definition Weather.cpp:28
Object is the base class for all game objects. It provides reference counting and dirty flag for upda...
Definition Object.h:18
A named event carrying an ordered list of Variant payloads. Pushed messages are heap-allocated; the q...
Definition Event.h:56
Data buffer paired with a filename (used for type identification).
Definition FileData.h:12
size_t getSize() const
Gets the size of the Data in bytes.
Definition FileData.h:24
void * getData() const
Gets a pointer to the data. This pointer will obviously not be valid if the Data object is destroyed.
Definition FileData.h:23
Represents raw pixel data.
Definition ImageData.h:26
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....
Definition Timer.h:13
Headless MCP editor host (MVVM).
Definition EditorHost.h:33
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).
bool windowOpen() const
std::string windowState() const
const std::string & gameRoot() const
Definition EditorHost.h:46
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).
Definition UI.h:30
Platform window interface (SDL implementation on desktop/mobile). Script: win <- eve....
Definition Window.h:46
WidgetDesc text(std::string content, std::string id)
Static text label.
Definition Widget.cpp:235
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.
Definition Window.h:17