690 "hasEditor is true but `static const char* editorHtml()` is missing");
693 static constexpr bool kAvailable = (DSPARK_WEBVIEW_BACKEND != 0);
704#if DSPARK_WEBVIEW_BACKEND == 3
705 return gtk_glue::api().ok;
720#if DSPARK_WEBVIEW_BACKEND == 3
739 bool create(
void* parentWindow,
const std::atomic<double>* shadows,
742 if (created_ || parentWindow ==
nullptr)
return false;
745 parent_ = parentWindow;
746 created_ = createPlatform(parentWindow);
753 if (!created_)
return;
761 if (created_ && width > 0 && height > 0)
762 setBoundsPlatform(width, height);
769 setVisiblePlatform(visible);
785 return queryParentSizePlatform(width, height);
788 [[nodiscard]]
bool created() const noexcept {
return created_; }
791 static constexpr size_t kNumParams = P::parameters.size();
793 std::atomic<double>
const* shadows_ =
nullptr;
795 void* parent_ =
nullptr;
796 bool created_ =
false;
798 static int indexOfId(
const char*
id)
noexcept
800 for (
size_t i = 0; i < kNumParams; ++i)
801 if (std::strcmp(P::parameters[i].
id,
id) == 0)
return static_cast<int>(i);
805 [[nodiscard]]
double plainOf(
size_t index)
const noexcept
807 return toPlain(P::parameters[index],
808 shadows_[index].load(std::memory_order_relaxed));
813 void handlePost(
const char* json)
noexcept
817 if (std::strcmp(msg.op,
"poll") == 0)
821 else if (std::strcmp(msg.op,
"set") == 0)
823 const int idx = indexOfId(msg.id);
824 if (idx >= 0 && msg.tokens >= 3 && host_.
setParam !=
nullptr)
827 else if (std::strcmp(msg.op,
"begin") == 0)
829 const int idx = indexOfId(msg.id);
830 if (idx >= 0 && host_.
beginEdit !=
nullptr)
833 else if (std::strcmp(msg.op,
"end") == 0)
835 const int idx = indexOfId(msg.id);
836 if (idx >= 0 && host_.
endEdit !=
nullptr)
839 else if (std::strcmp(msg.op,
"ready") == 0)
844 evalPlatform(
"window.__dsparkPost('metric','innerWidth',window.innerWidth);"
845 "window.__dsparkPost('metric','innerHeight',window.innerHeight);"
846 "window.__dsparkPost('metric','dpr',window.devicePixelRatio||1);");
848 else if (std::strcmp(msg.op,
"metric") == 0)
850 debugLog(
"page metric %s = %.2f", msg.id, msg.value);
856 std::string json =
"{\"type\":\"params\",\"params\":[";
857 for (
size_t i = 0; i < kNumParams; ++i)
859 const Param& p = P::parameters[i];
860 if (i > 0) json +=
',';
863 json +=
",\"name\":";
871 json +=
",\"unit\":";
873 json +=
",\"steps\":";
875 if (p.labels !=
nullptr)
877 json +=
",\"labels\":[";
878 for (
int k = 0; k <= p.steps; ++k)
880 if (k > 0) json +=
',';
885 json +=
",\"value\":";
889 json +=
"],\"design\":{\"width\":";
891 json +=
",\"height\":";
893 json +=
",\"keepAspect\":";
901 std::string json =
"{\"type\":\"values\",\"values\":{";
902 for (
size_t i = 0; i < kNumParams; ++i)
904 if (i > 0) json +=
',';
913 void sendRecv(
const std::string& payloadJson)
915 std::string js =
"window.__dsparkRecv(";
921 static constexpr bool debugFlag() noexcept
923 if constexpr (HasEditorDebug<P>)
return true;
929 static std::string pageHtml()
931#if !defined(DSPARK_NO_FILE_IO)
932 if constexpr (HasEditorDevFile<P>)
934 std::FILE* f =
nullptr;
936 fopen_s(&f, P::editorDevFile(),
"rb");
938 f = std::fopen(P::editorDevFile(),
"rb");
945 while ((got = std::fread(chunk, 1,
sizeof(chunk), f)) > 0)
946 text.append(chunk, got);
950 debugLog(
"page: dev file %s (%u bytes)", P::editorDevFile(),
951 static_cast<unsigned>(text.size()));
955 debugLog(
"page: dev file %s unreadable; embedded page used",
959 return P::editorHtml();
965#if DSPARK_WEBVIEW_BACKEND == 1
967 std::unique_ptr<webview::webview> wv_;
968 void* root_ =
nullptr;
969 mutable SIZE frameChrome_ { -1, -1 };
970 bool comInitialized_ =
false;
972 bool createPlatform(
void* parentWindow)
noexcept
980 comInitialized_ = SUCCEEDED(CoInitializeEx(
nullptr, COINIT_APARTMENTTHREADED));
981 wv_ = std::make_unique<webview::webview>(debugFlag(), parentWindow);
982 wv_->bind(
"__dsparkPost", [
this](std::string request) -> std::string {
983 handlePost(request.c_str());
984 return std::string {};
987 wv_->set_html(pageHtml());
991 SetWindowSubclass(
static_cast<HWND
>(parentWindow), &onParentMessage,
992 reinterpret_cast<UINT_PTR
>(
this),
993 reinterpret_cast<DWORD_PTR
>(
this));
999 root_ = GetAncestor(
static_cast<HWND
>(parentWindow), GA_ROOT);
1000 if (root_ !=
nullptr)
1001 SetWindowSubclass(
static_cast<HWND
>(root_), &onRootMessage,
1002 reinterpret_cast<UINT_PTR
>(
this),
1003 reinterpret_cast<DWORD_PTR
>(
this));
1005 GetClientRect(
static_cast<HWND
>(parentWindow), &parentBox);
1006 debugLog(
"create: parent=%p root=%p client=%ldx%ld", parentWindow,
1007 root_, parentBox.right, parentBox.bottom);
1013 if (comInitialized_)
1016 comInitialized_ =
false;
1022 static LRESULT CALLBACK onParentMessage(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp,
1023 UINT_PTR, DWORD_PTR refData)
noexcept
1027 debugLog(
"parent WM_SIZE %dx%d",
int(LOWORD(lp)),
int(HIWORD(lp)));
1028 auto* editor =
reinterpret_cast<Editor*
>(refData);
1029 editor->setBounds(
static_cast<int>(LOWORD(lp)),
static_cast<int>(HIWORD(lp)));
1031 return DefSubclassProc(hwnd, msg, wp, lp);
1037 double frameDpiScale() const noexcept
1039 const UINT dpi = parent_ !=
nullptr ? GetDpiForWindow(
static_cast<HWND
>(parent_))
1041 return dpi > 0 ? dpi / 96.0 : 1.0;
1057 bool rootGuard(HWND root, SIZE& chrome)
const noexcept
1059 if (parent_ ==
nullptr || root ==
nullptr)
return false;
1060 RECT rootBox {}, parentBox {};
1061 const bool measured = GetWindowRect(root, &rootBox)
1062 && GetClientRect(
static_cast<HWND
>(parent_), &parentBox);
1063 bool coherent =
false;
1070 const double rootArea = double(rootBox.right - rootBox.left)
1071 * double(rootBox.bottom - rootBox.top);
1072 const double parentArea = double(parentBox.right) * double(parentBox.bottom);
1073 coherent = rootArea > 0.0 && parentArea > 0.0
1074 && parentArea / rootArea >= 1.0 / 3.0;
1084 auto clampChrome = [](LONG v) -> LONG {
1085 return v < 0 ? 0 : (v > 1024 ? 1024 : v);
1087 const LONG cx = clampChrome((rootBox.right - rootBox.left) - parentBox.right);
1088 const LONG cy = clampChrome((rootBox.bottom - rootBox.top) - parentBox.bottom);
1089 if (frameChrome_.cx < 0)
1091 frameChrome_ = SIZE { cx, cy };
1092 debugLog(
"frame: steering host window, chrome=%ldx%ld", cx, cy);
1096 if (cx < frameChrome_.cx) frameChrome_.cx = cx;
1097 if (cy < frameChrome_.cy) frameChrome_.cy = cy;
1102 if (frameChrome_.cx < 0)
return false;
1103 chrome = frameChrome_;
1107 void applyMinMax(HWND root, MINMAXINFO* info)
const noexcept
1110 if (info ==
nullptr || !rootGuard(root, chrome))
return;
1111 const EditorSize logical = editorSizeOf<P>();
1112 const double dpi = frameDpiScale();
1116 const LONG minW = LONG(logical.width * dpi * lo + 0.5) + chrome.cx;
1117 const LONG minH = LONG(logical.height * dpi * lo + 0.5) + chrome.cy;
1118 const LONG maxW = LONG(logical.width * dpi * hi + 0.5) + chrome.cx;
1119 const LONG maxH = LONG(logical.height * dpi * hi + 0.5) + chrome.cy;
1120 if (info->ptMinTrackSize.x < minW) info->ptMinTrackSize.x = minW;
1121 if (info->ptMinTrackSize.y < minH) info->ptMinTrackSize.y = minH;
1122 if (info->ptMaxTrackSize.x > maxW) info->ptMaxTrackSize.x = maxW;
1123 if (info->ptMaxTrackSize.y > maxH) info->ptMaxTrackSize.y = maxH;
1124 debugLog(
"frame minmax: track %ldx%ld .. %ldx%ld (chrome %ldx%ld)",
1125 info->ptMinTrackSize.x, info->ptMinTrackSize.y,
1126 info->ptMaxTrackSize.x, info->ptMaxTrackSize.y,
1127 chrome.cx, chrome.cy);
1130 void applySizing(HWND root,
int edge, RECT* box)
const noexcept
1133 if (box ==
nullptr || !rootGuard(root, chrome))
return;
1134 const EditorSize logical = editorSizeOf<P>();
1135 const double dpi = frameDpiScale();
1142 const double minW = logical.width * dpi * lo;
1143 const double maxW = logical.width * dpi * hi;
1144 const double minH = logical.height * dpi * lo;
1145 const double maxH = logical.height * dpi * hi;
1146 double w = double(box->right - box->left) - chrome.cx;
1147 double h = double(box->bottom - box->top) - chrome.cy;
1148 w = w < minW ? minW : (w > maxW ? maxW : w);
1149 h = h < minH ? minH : (h > maxH ? maxH : h);
1154 const double ratio = double(logical.width) / logical.height;
1155 if (edge == WMSZ_TOP || edge == WMSZ_BOTTOM)
1160 const LONG newW = LONG(w + 0.5) + chrome.cx;
1161 const LONG newH = LONG(h + 0.5) + chrome.cy;
1162 const LONG oldW = box->right - box->left;
1163 const LONG oldH = box->bottom - box->top;
1165 if (edge == WMSZ_LEFT || edge == WMSZ_TOPLEFT || edge == WMSZ_BOTTOMLEFT)
1166 box->left = box->right - newW;
1168 box->right = box->left + newW;
1169 if (edge == WMSZ_TOP || edge == WMSZ_TOPLEFT || edge == WMSZ_TOPRIGHT)
1170 box->top = box->bottom - newH;
1172 box->bottom = box->top + newH;
1173 if (newW != oldW || newH != oldH)
1174 debugLog(
"frame sizing(edge %d): %ldx%ld -> %ldx%ld", edge,
1175 oldW, oldH, newW, newH);
1178 static LRESULT CALLBACK onRootMessage(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp,
1179 UINT_PTR, DWORD_PTR refData)
noexcept
1181 const LRESULT result = DefSubclassProc(hwnd, msg, wp, lp);
1182 auto* editor =
reinterpret_cast<Editor*
>(refData);
1183 if (msg == WM_GETMINMAXINFO)
1184 editor->applyMinMax(hwnd,
reinterpret_cast<MINMAXINFO*
>(lp));
1185 else if (msg == WM_SIZING)
1187 editor->applySizing(hwnd,
static_cast<int>(wp),
1188 reinterpret_cast<RECT*
>(lp));
1194 void destroyPlatform() noexcept
1196 if (parent_ !=
nullptr)
1197 RemoveWindowSubclass(
static_cast<HWND
>(parent_), &onParentMessage,
1198 reinterpret_cast<UINT_PTR
>(
this));
1199 if (root_ !=
nullptr)
1201 RemoveWindowSubclass(
static_cast<HWND
>(root_), &onRootMessage,
1202 reinterpret_cast<UINT_PTR
>(
this));
1205 try { wv_.reset(); }
1207 if (comInitialized_)
1210 comInitialized_ =
false;
1214 void setBoundsPlatform(
int width,
int height)
noexcept
1219 const int requestedW = width;
1220 const int requestedH = height;
1222 if (parent_ !=
nullptr && GetClientRect(
static_cast<HWND
>(parent_), &rect)
1223 && rect.right > 0 && rect.bottom > 0)
1225 width =
static_cast<int>(rect.right);
1226 height =
static_cast<int>(rect.bottom);
1228 debugLog(
"setBounds(%d,%d) -> widget %dx%d", requestedW, requestedH,
1232 if (
auto widget = wv_->widget(); widget.ok())
1233 MoveWindow(
static_cast<HWND
>(widget.value()), 0, 0, width, height, TRUE);
1238 void setVisiblePlatform(
bool visible)
noexcept
1242 if (
auto widget = wv_->widget(); widget.ok())
1243 ShowWindow(
static_cast<HWND
>(widget.value()), visible ? SW_SHOW : SW_HIDE);
1248 bool queryParentSizePlatform(
int& width,
int& height)
const noexcept
1251 if (parent_ ==
nullptr || !GetClientRect(
static_cast<HWND
>(parent_), &rect))
1253 if (rect.right <= 0 || rect.bottom <= 0)
return false;
1254 width =
static_cast<int>(rect.right);
1255 height =
static_cast<int>(rect.bottom);
1259 void evalPlatform(
const std::string& js)
noexcept
1263 if (wv_) wv_->eval(js);
1271#elif DSPARK_WEBVIEW_BACKEND == 2
1273 objc_glue::ObjId webView_ =
nullptr;
1274 objc_glue::ObjId configuration_ =
nullptr;
1275 objc_glue::ObjId relay_ =
nullptr;
1276 objc_glue::MessageSink sink_ {};
1278 static void sinkTrampoline(
void* context,
const char* json)
noexcept
1280 static_cast<Editor*
>(context)->handlePost(json);
1283 bool createPlatform(
void* parentWindow)
noexcept
1285 namespace og = objc_glue;
1286 if (!og::loadWebKit())
return false;
1287 Class relayCls = og::relayClass();
1288 if (relayCls ==
nullptr)
return false;
1290 sink_ = { &sinkTrampoline,
this };
1291 relay_ = og::call(og::call(
reinterpret_cast<og::ObjId
>(relayCls),
"alloc"),
"init");
1292 if (relay_ ==
nullptr)
return false;
1293 og::setRelaySink(relay_, &sink_);
1295 configuration_ = og::call(og::cls(
"WKWebViewConfiguration"),
"new");
1296 og::ObjId contentController = og::call(configuration_,
"userContentController");
1297 og::call<void>(contentController,
"addScriptMessageHandler:name:",
1298 relay_, og::nsString(
"dspark"));
1301 std::string bootstrap =
1302 "window.__dsparkPost=function(){"
1303 "window.webkit.messageHandlers.dspark.postMessage("
1304 "JSON.stringify(Array.prototype.slice.call(arguments)));};";
1306 og::ObjId script = og::call(og::call(og::cls(
"WKUserScript"),
"alloc"),
1307 "initWithSource:injectionTime:forMainFrameOnly:",
1308 og::nsString(bootstrap.c_str()),
1309 static_cast<long>(0) ,
1310 static_cast<signed char>(1));
1311 og::call<void>(contentController,
"addUserScript:", script);
1312 og::call<void>(script,
"release");
1314 const og::Rect frame {};
1315 webView_ = og::call(og::call(og::cls(
"WKWebView"),
"alloc"),
1316 "initWithFrame:configuration:", frame, configuration_);
1317 if (webView_ ==
nullptr)
1323 og::call<void>(webView_,
"setAutoresizingMask:",
static_cast<unsigned long>(18));
1324 og::call<void>(
static_cast<og::ObjId
>(parentWindow),
"addSubview:", webView_);
1325 og::call<void>(webView_,
"loadHTMLString:baseURL:",
1326 og::nsString(pageHtml().c_str()),
static_cast<og::ObjId
>(
nullptr));
1330 void destroyPlatform() noexcept
1332 namespace og = objc_glue;
1333 if (configuration_ !=
nullptr)
1335 og::ObjId contentController = og::call(configuration_,
"userContentController");
1336 og::call<void>(contentController,
"removeScriptMessageHandlerForName:",
1337 og::nsString(
"dspark"));
1339 if (webView_ !=
nullptr)
1341 og::call<void>(webView_,
"removeFromSuperview");
1342 og::call<void>(webView_,
"release");
1345 if (configuration_ !=
nullptr)
1347 og::call<void>(configuration_,
"release");
1348 configuration_ =
nullptr;
1350 if (relay_ !=
nullptr)
1352 og::call<void>(relay_,
"release");
1357 void setBoundsPlatform(
int width,
int height)
noexcept
1359 const objc_glue::Rect frame { 0, 0,
static_cast<double>(width),
1360 static_cast<double>(height) };
1361 objc_glue::call<void>(webView_,
"setFrame:", frame);
1364 void setVisiblePlatform(
bool visible)
noexcept
1366 objc_glue::call<void>(webView_,
"setHidden:",
1367 static_cast<signed char>(visible ? 0 : 1));
1370 bool queryParentSizePlatform(
int&,
int&)
const noexcept
1377 void evalPlatform(
const std::string& js)
noexcept
1379 if (webView_ !=
nullptr)
1380 objc_glue::call<void>(webView_,
"evaluateJavaScript:completionHandler:",
1381 objc_glue::nsString(js.c_str()),
1382 static_cast<objc_glue::ObjId
>(
nullptr));
1388#elif DSPARK_WEBVIEW_BACKEND == 3
1390 void* plug_ =
nullptr;
1391 void* webView_ =
nullptr;
1392 void* contentManager_ =
nullptr;
1393 unsigned long messageSignal_ = 0;
1395 static void onScriptMessage(
void*,
void* jsResult,
void* userData)
noexcept
1397 auto* self =
static_cast<Editor*
>(userData);
1398 const auto& gtk = gtk_glue::api();
1399 void* value = gtk.jsResultGetValue(jsResult);
1400 if (value ==
nullptr)
return;
1401 char* text = gtk.jscValueToString(value);
1402 if (text !=
nullptr)
1404 self->handlePost(text);
1409 bool createPlatform(
void* parentWindow)
noexcept
1411 const auto& gtk = gtk_glue::api();
1412 if (!gtk.ok)
return false;
1415 if (!gtk.gtkInitCheck(
nullptr,
nullptr))
1417 debugLog(
"linux create: gtk_init_check failed (no display)");
1420 plug_ = gtk.plugNew(
static_cast<unsigned long>(
1421 reinterpret_cast<std::uintptr_t
>(parentWindow)));
1422 if (plug_ ==
nullptr)
return false;
1423 webView_ = gtk.webViewNew();
1424 if (webView_ ==
nullptr)
1429 contentManager_ = gtk.viewGetContentManager(webView_);
1430 gtk.contentManagerRegisterHandler(contentManager_,
"dspark");
1431 messageSignal_ = gtk.signalConnectData(
1432 contentManager_,
"script-message-received::dspark",
1433 reinterpret_cast<void (*)()
>(&onScriptMessage),
this,
nullptr, 0);
1437 std::string bootstrap =
1438 "window.__dsparkPost=function(){"
1439 "window.webkit.messageHandlers.dspark.postMessage("
1440 "JSON.stringify(Array.prototype.slice.call(arguments)));};";
1442 void* script = gtk.userScriptNew(bootstrap.c_str(),
1446 gtk.contentManagerAddScript(contentManager_, script);
1447 gtk.userScriptUnref(script);
1448 gtk.containerAdd(plug_, webView_);
1449 gtk.loadHtml(webView_, pageHtml().c_str(),
nullptr);
1450 gtk.widgetShowAll(plug_);
1451 debugLog(
"linux create: plug for X11 window %p", parentWindow);
1456 void destroyPlatform() noexcept
1458 const auto& gtk = gtk_glue::api();
1459 if (!gtk.ok)
return;
1460 if (contentManager_ !=
nullptr)
1462 if (messageSignal_ != 0)
1463 gtk.signalHandlerDisconnect(contentManager_, messageSignal_);
1464 gtk.contentManagerUnregisterHandler(contentManager_,
"dspark");
1466 if (plug_ !=
nullptr)
1467 gtk.widgetDestroy(plug_);
1470 contentManager_ =
nullptr;
1475 void setBoundsPlatform(
int width,
int height)
noexcept
1477 const auto& gtk = gtk_glue::api();
1478 if (plug_ ==
nullptr)
return;
1479 gtk.windowSetDefaultSize(plug_, width, height);
1480 gtk.windowResize(plug_, width, height);
1484 void setVisiblePlatform(
bool visible)
noexcept
1486 if (plug_ !=
nullptr)
1487 gtk_glue::api().widgetSetVisible(plug_, visible ? 1 : 0);
1490 bool queryParentSizePlatform(
int&,
int&)
const noexcept
1497 void evalPlatform(
const std::string& js)
noexcept
1499 const auto& gtk = gtk_glue::api();
1500 if (webView_ ==
nullptr)
return;
1501 if (gtk.runJs !=
nullptr)
1502 gtk.runJs(webView_, js.c_str(),
nullptr,
nullptr,
nullptr);
1504 gtk.evalJs(webView_, js.c_str(), -1,
nullptr,
nullptr,
1505 nullptr,
nullptr,
nullptr);
1508 void pumpPlatform() noexcept
1510 const auto& gtk = gtk_glue::api();
1511 if (!gtk.ok)
return;
1515 for (
int i = 0; i < kMaxPumpIterations && gtk.mainContextPending(
nullptr) != 0; ++i)
1516 gtk.mainContextIteration(
nullptr, 0);
1519 static constexpr int kMaxPumpIterations = 256;
1526 bool createPlatform(
void*)
noexcept {
return false; }
1527 void destroyPlatform() noexcept {}
1528 void setBoundsPlatform(
int,
int)
noexcept {}
1529 void setVisiblePlatform(
bool)
noexcept {}
1530 bool queryParentSizePlatform(
int&,
int&)
const noexcept {
return false; }
1531 void evalPlatform(
const std::string&)
noexcept {}