685 "hasEditor is true but `static const char* editorHtml()` is missing");
699#if DSPARK_WEBVIEW_BACKEND == 3
700 return gtk_glue::api().ok;
715#if DSPARK_WEBVIEW_BACKEND == 3
734 bool create(
void* parentWindow,
const std::atomic<double>* shadows,
737 if (created_ || parentWindow ==
nullptr)
return false;
740 parent_ = parentWindow;
741 created_ = createPlatform(parentWindow);
748 if (!created_)
return;
756 if (created_ && width > 0 && height > 0)
757 setBoundsPlatform(width, height);
764 setVisiblePlatform(visible);
780 return queryParentSizePlatform(width, height);
783 [[nodiscard]]
bool created() const noexcept {
return created_; }
786 static constexpr size_t kNumParams = P::parameters.size();
788 std::atomic<double>
const* shadows_ =
nullptr;
790 void* parent_ =
nullptr;
791 bool created_ =
false;
793 static int indexOfId(
const char*
id)
noexcept
795 for (
size_t i = 0; i < kNumParams; ++i)
796 if (std::strcmp(P::parameters[i].
id,
id) == 0)
return static_cast<int>(i);
800 [[nodiscard]]
double plainOf(
size_t index)
const noexcept
802 return toPlain(P::parameters[index],
803 shadows_[index].load(std::memory_order_relaxed));
808 void handlePost(
const char* json)
noexcept
812 if (std::strcmp(msg.op,
"poll") == 0)
816 else if (std::strcmp(msg.op,
"set") == 0)
818 const int idx = indexOfId(msg.id);
819 if (idx >= 0 && msg.tokens >= 3 && host_.
setParam !=
nullptr)
822 else if (std::strcmp(msg.op,
"begin") == 0)
824 const int idx = indexOfId(msg.id);
825 if (idx >= 0 && host_.
beginEdit !=
nullptr)
828 else if (std::strcmp(msg.op,
"end") == 0)
830 const int idx = indexOfId(msg.id);
831 if (idx >= 0 && host_.
endEdit !=
nullptr)
834 else if (std::strcmp(msg.op,
"ready") == 0)
839 evalPlatform(
"window.__dsparkPost('metric','innerWidth',window.innerWidth);"
840 "window.__dsparkPost('metric','innerHeight',window.innerHeight);"
841 "window.__dsparkPost('metric','dpr',window.devicePixelRatio||1);");
843 else if (std::strcmp(msg.op,
"metric") == 0)
845 debugLog(
"page metric %s = %.2f", msg.id, msg.value);
851 std::string json =
"{\"type\":\"params\",\"params\":[";
852 for (
size_t i = 0; i < kNumParams; ++i)
854 const Param& p = P::parameters[i];
855 if (i > 0) json +=
',';
858 json +=
",\"name\":";
866 json +=
",\"unit\":";
868 json +=
",\"steps\":";
870 json +=
",\"value\":";
874 json +=
"],\"design\":{\"width\":";
876 json +=
",\"height\":";
878 json +=
",\"keepAspect\":";
886 std::string json =
"{\"type\":\"values\",\"values\":{";
887 for (
size_t i = 0; i < kNumParams; ++i)
889 if (i > 0) json +=
',';
898 void sendRecv(
const std::string& payloadJson)
900 std::string js =
"window.__dsparkRecv(";
906 static constexpr bool debugFlag() noexcept
908 if constexpr (HasEditorDebug<P>)
return true;
914 static std::string pageHtml()
916#if !defined(DSPARK_NO_FILE_IO)
917 if constexpr (HasEditorDevFile<P>)
919 std::FILE* f =
nullptr;
921 fopen_s(&f, P::editorDevFile(),
"rb");
923 f = std::fopen(P::editorDevFile(),
"rb");
930 while ((got = std::fread(chunk, 1,
sizeof(chunk), f)) > 0)
931 text.append(chunk, got);
935 debugLog(
"page: dev file %s (%u bytes)", P::editorDevFile(),
936 static_cast<unsigned>(text.size()));
940 debugLog(
"page: dev file %s unreadable; embedded page used",
944 return P::editorHtml();
950#if DSPARK_WEBVIEW_BACKEND == 1
952 std::unique_ptr<webview::webview> wv_;
953 void* root_ =
nullptr;
954 mutable SIZE frameChrome_ { -1, -1 };
955 bool comInitialized_ =
false;
957 bool createPlatform(
void* parentWindow)
noexcept
965 comInitialized_ = SUCCEEDED(CoInitializeEx(
nullptr, COINIT_APARTMENTTHREADED));
966 wv_ = std::make_unique<webview::webview>(debugFlag(), parentWindow);
967 wv_->bind(
"__dsparkPost", [
this](std::string request) -> std::string {
968 handlePost(request.c_str());
969 return std::string {};
972 wv_->set_html(pageHtml());
976 SetWindowSubclass(
static_cast<HWND
>(parentWindow), &onParentMessage,
977 reinterpret_cast<UINT_PTR
>(
this),
978 reinterpret_cast<DWORD_PTR
>(
this));
984 root_ = GetAncestor(
static_cast<HWND
>(parentWindow), GA_ROOT);
985 if (root_ !=
nullptr)
986 SetWindowSubclass(
static_cast<HWND
>(root_), &onRootMessage,
987 reinterpret_cast<UINT_PTR
>(
this),
988 reinterpret_cast<DWORD_PTR
>(
this));
990 GetClientRect(
static_cast<HWND
>(parentWindow), &parentBox);
991 debugLog(
"create: parent=%p root=%p client=%ldx%ld", parentWindow,
992 root_, parentBox.right, parentBox.bottom);
1001 comInitialized_ =
false;
1007 static LRESULT CALLBACK onParentMessage(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp,
1008 UINT_PTR, DWORD_PTR refData)
noexcept
1012 debugLog(
"parent WM_SIZE %dx%d",
int(LOWORD(lp)),
int(HIWORD(lp)));
1013 auto* editor =
reinterpret_cast<Editor*
>(refData);
1014 editor->setBounds(
static_cast<int>(LOWORD(lp)),
static_cast<int>(HIWORD(lp)));
1016 return DefSubclassProc(hwnd, msg, wp, lp);
1022 double frameDpiScale() const noexcept
1024 const UINT dpi = parent_ !=
nullptr ? GetDpiForWindow(
static_cast<HWND
>(parent_))
1026 return dpi > 0 ? dpi / 96.0 : 1.0;
1042 bool rootGuard(HWND root, SIZE& chrome)
const noexcept
1044 if (parent_ ==
nullptr || root ==
nullptr)
return false;
1045 RECT rootBox {}, parentBox {};
1046 const bool measured = GetWindowRect(root, &rootBox)
1047 && GetClientRect(
static_cast<HWND
>(parent_), &parentBox);
1048 bool coherent =
false;
1055 const double rootArea = double(rootBox.right - rootBox.left)
1056 * double(rootBox.bottom - rootBox.top);
1057 const double parentArea = double(parentBox.right) * double(parentBox.bottom);
1058 coherent = rootArea > 0.0 && parentArea > 0.0
1059 && parentArea / rootArea >= 1.0 / 3.0;
1069 auto clampChrome = [](LONG v) -> LONG {
1070 return v < 0 ? 0 : (v > 1024 ? 1024 : v);
1072 const LONG cx = clampChrome((rootBox.right - rootBox.left) - parentBox.right);
1073 const LONG cy = clampChrome((rootBox.bottom - rootBox.top) - parentBox.bottom);
1074 if (frameChrome_.cx < 0)
1076 frameChrome_ = SIZE { cx, cy };
1077 debugLog(
"frame: steering host window, chrome=%ldx%ld", cx, cy);
1081 if (cx < frameChrome_.cx) frameChrome_.cx = cx;
1082 if (cy < frameChrome_.cy) frameChrome_.cy = cy;
1087 if (frameChrome_.cx < 0)
return false;
1088 chrome = frameChrome_;
1092 void applyMinMax(HWND root, MINMAXINFO* info)
const noexcept
1095 if (info ==
nullptr || !rootGuard(root, chrome))
return;
1096 const EditorSize logical = editorSizeOf<P>();
1097 const double dpi = frameDpiScale();
1101 const LONG minW = LONG(logical.width * dpi * lo + 0.5) + chrome.cx;
1102 const LONG minH = LONG(logical.height * dpi * lo + 0.5) + chrome.cy;
1103 const LONG maxW = LONG(logical.width * dpi * hi + 0.5) + chrome.cx;
1104 const LONG maxH = LONG(logical.height * dpi * hi + 0.5) + chrome.cy;
1105 if (info->ptMinTrackSize.x < minW) info->ptMinTrackSize.x = minW;
1106 if (info->ptMinTrackSize.y < minH) info->ptMinTrackSize.y = minH;
1107 if (info->ptMaxTrackSize.x > maxW) info->ptMaxTrackSize.x = maxW;
1108 if (info->ptMaxTrackSize.y > maxH) info->ptMaxTrackSize.y = maxH;
1109 debugLog(
"frame minmax: track %ldx%ld .. %ldx%ld (chrome %ldx%ld)",
1110 info->ptMinTrackSize.x, info->ptMinTrackSize.y,
1111 info->ptMaxTrackSize.x, info->ptMaxTrackSize.y,
1112 chrome.cx, chrome.cy);
1115 void applySizing(HWND root,
int edge, RECT* box)
const noexcept
1118 if (box ==
nullptr || !rootGuard(root, chrome))
return;
1119 const EditorSize logical = editorSizeOf<P>();
1120 const double dpi = frameDpiScale();
1127 const double minW = logical.width * dpi * lo;
1128 const double maxW = logical.width * dpi * hi;
1129 const double minH = logical.height * dpi * lo;
1130 const double maxH = logical.height * dpi * hi;
1131 double w = double(box->right - box->left) - chrome.cx;
1132 double h = double(box->bottom - box->top) - chrome.cy;
1133 w = w < minW ? minW : (w > maxW ? maxW : w);
1134 h = h < minH ? minH : (h > maxH ? maxH : h);
1139 const double ratio = double(logical.width) / logical.height;
1140 if (edge == WMSZ_TOP || edge == WMSZ_BOTTOM)
1145 const LONG newW = LONG(w + 0.5) + chrome.cx;
1146 const LONG newH = LONG(h + 0.5) + chrome.cy;
1147 const LONG oldW = box->right - box->left;
1148 const LONG oldH = box->bottom - box->top;
1150 if (edge == WMSZ_LEFT || edge == WMSZ_TOPLEFT || edge == WMSZ_BOTTOMLEFT)
1151 box->left = box->right - newW;
1153 box->right = box->left + newW;
1154 if (edge == WMSZ_TOP || edge == WMSZ_TOPLEFT || edge == WMSZ_TOPRIGHT)
1155 box->top = box->bottom - newH;
1157 box->bottom = box->top + newH;
1158 if (newW != oldW || newH != oldH)
1159 debugLog(
"frame sizing(edge %d): %ldx%ld -> %ldx%ld", edge,
1160 oldW, oldH, newW, newH);
1163 static LRESULT CALLBACK onRootMessage(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp,
1164 UINT_PTR, DWORD_PTR refData)
noexcept
1166 const LRESULT result = DefSubclassProc(hwnd, msg, wp, lp);
1167 auto* editor =
reinterpret_cast<Editor*
>(refData);
1168 if (msg == WM_GETMINMAXINFO)
1169 editor->applyMinMax(hwnd,
reinterpret_cast<MINMAXINFO*
>(lp));
1170 else if (msg == WM_SIZING)
1172 editor->applySizing(hwnd,
static_cast<int>(wp),
1173 reinterpret_cast<RECT*
>(lp));
1179 void destroyPlatform() noexcept
1181 if (parent_ !=
nullptr)
1182 RemoveWindowSubclass(
static_cast<HWND
>(parent_), &onParentMessage,
1183 reinterpret_cast<UINT_PTR
>(
this));
1184 if (root_ !=
nullptr)
1186 RemoveWindowSubclass(
static_cast<HWND
>(root_), &onRootMessage,
1187 reinterpret_cast<UINT_PTR
>(
this));
1190 try { wv_.reset(); }
1192 if (comInitialized_)
1195 comInitialized_ =
false;
1199 void setBoundsPlatform(
int width,
int height)
noexcept
1204 const int requestedW = width;
1205 const int requestedH = height;
1207 if (parent_ !=
nullptr && GetClientRect(
static_cast<HWND
>(parent_), &rect)
1208 && rect.right > 0 && rect.bottom > 0)
1210 width =
static_cast<int>(rect.right);
1211 height =
static_cast<int>(rect.bottom);
1213 debugLog(
"setBounds(%d,%d) -> widget %dx%d", requestedW, requestedH,
1217 if (
auto widget = wv_->widget(); widget.ok())
1218 MoveWindow(
static_cast<HWND
>(widget.value()), 0, 0, width, height, TRUE);
1223 void setVisiblePlatform(
bool visible)
noexcept
1227 if (
auto widget = wv_->widget(); widget.ok())
1228 ShowWindow(
static_cast<HWND
>(widget.value()), visible ? SW_SHOW : SW_HIDE);
1233 bool queryParentSizePlatform(
int& width,
int& height)
const noexcept
1236 if (parent_ ==
nullptr || !GetClientRect(
static_cast<HWND
>(parent_), &rect))
1238 if (rect.right <= 0 || rect.bottom <= 0)
return false;
1239 width =
static_cast<int>(rect.right);
1240 height =
static_cast<int>(rect.bottom);
1244 void evalPlatform(
const std::string& js)
noexcept
1248 if (wv_) wv_->eval(js);
1256#elif DSPARK_WEBVIEW_BACKEND == 2
1258 objc_glue::ObjId webView_ =
nullptr;
1259 objc_glue::ObjId configuration_ =
nullptr;
1260 objc_glue::ObjId relay_ =
nullptr;
1261 objc_glue::MessageSink sink_ {};
1263 static void sinkTrampoline(
void* context,
const char* json)
noexcept
1265 static_cast<Editor*
>(context)->handlePost(json);
1268 bool createPlatform(
void* parentWindow)
noexcept
1270 namespace og = objc_glue;
1271 if (!og::loadWebKit())
return false;
1272 Class relayCls = og::relayClass();
1273 if (relayCls ==
nullptr)
return false;
1275 sink_ = { &sinkTrampoline,
this };
1276 relay_ = og::call(og::call(
reinterpret_cast<og::ObjId
>(relayCls),
"alloc"),
"init");
1277 if (relay_ ==
nullptr)
return false;
1278 og::setRelaySink(relay_, &sink_);
1280 configuration_ = og::call(og::cls(
"WKWebViewConfiguration"),
"new");
1281 og::ObjId contentController = og::call(configuration_,
"userContentController");
1282 og::call<void>(contentController,
"addScriptMessageHandler:name:",
1283 relay_, og::nsString(
"dspark"));
1286 std::string bootstrap =
1287 "window.__dsparkPost=function(){"
1288 "window.webkit.messageHandlers.dspark.postMessage("
1289 "JSON.stringify(Array.prototype.slice.call(arguments)));};";
1291 og::ObjId script = og::call(og::call(og::cls(
"WKUserScript"),
"alloc"),
1292 "initWithSource:injectionTime:forMainFrameOnly:",
1293 og::nsString(bootstrap.c_str()),
1294 static_cast<long>(0) ,
1295 static_cast<signed char>(1));
1296 og::call<void>(contentController,
"addUserScript:", script);
1297 og::call<void>(script,
"release");
1299 const og::Rect frame {};
1300 webView_ = og::call(og::call(og::cls(
"WKWebView"),
"alloc"),
1301 "initWithFrame:configuration:", frame, configuration_);
1302 if (webView_ ==
nullptr)
1308 og::call<void>(webView_,
"setAutoresizingMask:",
static_cast<unsigned long>(18));
1309 og::call<void>(
static_cast<og::ObjId
>(parentWindow),
"addSubview:", webView_);
1310 og::call<void>(webView_,
"loadHTMLString:baseURL:",
1311 og::nsString(pageHtml().c_str()),
static_cast<og::ObjId
>(
nullptr));
1315 void destroyPlatform() noexcept
1317 namespace og = objc_glue;
1318 if (configuration_ !=
nullptr)
1320 og::ObjId contentController = og::call(configuration_,
"userContentController");
1321 og::call<void>(contentController,
"removeScriptMessageHandlerForName:",
1322 og::nsString(
"dspark"));
1324 if (webView_ !=
nullptr)
1326 og::call<void>(webView_,
"removeFromSuperview");
1327 og::call<void>(webView_,
"release");
1330 if (configuration_ !=
nullptr)
1332 og::call<void>(configuration_,
"release");
1333 configuration_ =
nullptr;
1335 if (relay_ !=
nullptr)
1337 og::call<void>(relay_,
"release");
1342 void setBoundsPlatform(
int width,
int height)
noexcept
1344 const objc_glue::Rect frame { 0, 0,
static_cast<double>(width),
1345 static_cast<double>(height) };
1346 objc_glue::call<void>(webView_,
"setFrame:", frame);
1349 void setVisiblePlatform(
bool visible)
noexcept
1351 objc_glue::call<void>(webView_,
"setHidden:",
1352 static_cast<signed char>(visible ? 0 : 1));
1355 bool queryParentSizePlatform(
int&,
int&)
const noexcept
1362 void evalPlatform(
const std::string& js)
noexcept
1364 if (webView_ !=
nullptr)
1365 objc_glue::call<void>(webView_,
"evaluateJavaScript:completionHandler:",
1366 objc_glue::nsString(js.c_str()),
1367 static_cast<objc_glue::ObjId
>(
nullptr));
1373#elif DSPARK_WEBVIEW_BACKEND == 3
1375 void* plug_ =
nullptr;
1376 void* webView_ =
nullptr;
1377 void* contentManager_ =
nullptr;
1378 unsigned long messageSignal_ = 0;
1380 static void onScriptMessage(
void*,
void* jsResult,
void* userData)
noexcept
1382 auto* self =
static_cast<Editor*
>(userData);
1383 const auto& gtk = gtk_glue::api();
1384 void* value = gtk.jsResultGetValue(jsResult);
1385 if (value ==
nullptr)
return;
1386 char* text = gtk.jscValueToString(value);
1387 if (text !=
nullptr)
1389 self->handlePost(text);
1394 bool createPlatform(
void* parentWindow)
noexcept
1396 const auto& gtk = gtk_glue::api();
1397 if (!gtk.ok)
return false;
1400 if (!gtk.gtkInitCheck(
nullptr,
nullptr))
1402 debugLog(
"linux create: gtk_init_check failed (no display)");
1405 plug_ = gtk.plugNew(
static_cast<unsigned long>(
1406 reinterpret_cast<std::uintptr_t
>(parentWindow)));
1407 if (plug_ ==
nullptr)
return false;
1408 webView_ = gtk.webViewNew();
1409 if (webView_ ==
nullptr)
1414 contentManager_ = gtk.viewGetContentManager(webView_);
1415 gtk.contentManagerRegisterHandler(contentManager_,
"dspark");
1416 messageSignal_ = gtk.signalConnectData(
1417 contentManager_,
"script-message-received::dspark",
1418 reinterpret_cast<void (*)()
>(&onScriptMessage),
this,
nullptr, 0);
1422 std::string bootstrap =
1423 "window.__dsparkPost=function(){"
1424 "window.webkit.messageHandlers.dspark.postMessage("
1425 "JSON.stringify(Array.prototype.slice.call(arguments)));};";
1427 void* script = gtk.userScriptNew(bootstrap.c_str(),
1431 gtk.contentManagerAddScript(contentManager_, script);
1432 gtk.userScriptUnref(script);
1433 gtk.containerAdd(plug_, webView_);
1434 gtk.loadHtml(webView_, pageHtml().c_str(),
nullptr);
1435 gtk.widgetShowAll(plug_);
1436 debugLog(
"linux create: plug for X11 window %p", parentWindow);
1441 void destroyPlatform() noexcept
1443 const auto& gtk = gtk_glue::api();
1444 if (!gtk.ok)
return;
1445 if (contentManager_ !=
nullptr)
1447 if (messageSignal_ != 0)
1448 gtk.signalHandlerDisconnect(contentManager_, messageSignal_);
1449 gtk.contentManagerUnregisterHandler(contentManager_,
"dspark");
1451 if (plug_ !=
nullptr)
1452 gtk.widgetDestroy(plug_);
1455 contentManager_ =
nullptr;
1460 void setBoundsPlatform(
int width,
int height)
noexcept
1462 const auto& gtk = gtk_glue::api();
1463 if (plug_ ==
nullptr)
return;
1464 gtk.windowSetDefaultSize(plug_, width, height);
1465 gtk.windowResize(plug_, width, height);
1469 void setVisiblePlatform(
bool visible)
noexcept
1471 if (plug_ !=
nullptr)
1472 gtk_glue::api().widgetSetVisible(plug_, visible ? 1 : 0);
1475 bool queryParentSizePlatform(
int&,
int&)
const noexcept
1482 void evalPlatform(
const std::string& js)
noexcept
1484 const auto& gtk = gtk_glue::api();
1485 if (webView_ ==
nullptr)
return;
1486 if (gtk.runJs !=
nullptr)
1487 gtk.runJs(webView_, js.c_str(),
nullptr,
nullptr,
nullptr);
1489 gtk.evalJs(webView_, js.c_str(), -1,
nullptr,
nullptr,
1490 nullptr,
nullptr,
nullptr);
1493 void pumpPlatform() noexcept
1495 const auto& gtk = gtk_glue::api();
1496 if (!gtk.ok)
return;
1497 while (gtk.mainContextPending(
nullptr) != 0)
1498 gtk.mainContextIteration(
nullptr, 0);
1506 bool createPlatform(
void*)
noexcept {
return false; }
1507 void destroyPlatform() noexcept {}
1508 void setBoundsPlatform(
int,
int)
noexcept {}
1509 void setVisiblePlatform(
bool)
noexcept {}
1510 bool queryParentSizePlatform(
int&,
int&)
const noexcept {
return false; }
1511 void evalPlatform(
const std::string&)
noexcept {}