diff --git a/Kore/Tools/macos_arm64/kmake b/Kore/Tools/macos_arm64/kmake index 2334a3a..891f181 100755 Binary files a/Kore/Tools/macos_arm64/kmake and b/Kore/Tools/macos_arm64/kmake differ diff --git a/Sources/main.cpp b/Sources/main.cpp index 1856b0c..264b991 100644 --- a/Sources/main.cpp +++ b/Sources/main.cpp @@ -46,6 +46,10 @@ extern "C" void websocket_run_groupchat_test(int rate, int duration); #endif +#ifdef WITH_WEBVIEW +#include "webview.h" +#endif + //#include "socket_connection_test.cpp" #include #include @@ -3010,6 +3014,50 @@ namespace { SET_FUNCTION(runt, "windowY", runt_window_y); SET_FUNCTION(runt, "language", runt_language); + #ifdef WITH_WEBVIEW + SET_FUNCTION(runt, "webviewCreate", runt_webview_create); + SET_FUNCTION(runt, "webviewLoadHTML", runt_webview_load_html); + SET_FUNCTION(runt, "webviewLoadURL", runt_webview_load_url); + SET_FUNCTION(runt, "webviewEvalJS", runt_webview_eval_js); + SET_FUNCTION(runt, "webviewShow", runt_webview_show); + SET_FUNCTION(runt, "webviewHide", runt_webview_hide); + SET_FUNCTION(runt, "webviewDestroy", runt_webview_destroy); + SET_FUNCTION(runt, "webviewResize", runt_webview_resize); + SET_FUNCTION(runt, "webviewMove", runt_webview_move); + SET_FUNCTION(runt, "webviewSetBounds", runt_webview_set_bounds); + SET_FUNCTION(runt, "webviewGetX", runt_webview_get_x); + SET_FUNCTION(runt, "webviewGetY", runt_webview_get_y); + SET_FUNCTION(runt, "webviewGetWidth", runt_webview_get_width); + SET_FUNCTION(runt, "webviewGetHeight", runt_webview_get_height); + SET_FUNCTION(runt, "webviewSetTransparent", runt_webview_set_transparent); + SET_FUNCTION(runt, "webviewSetClickThrough", runt_webview_set_click_through); + SET_FUNCTION(runt, "webviewSetTitle", runt_webview_set_title); + SET_FUNCTION(runt, "webviewSend", runt_webview_send); + SET_FUNCTION(runt, "webviewSetOnMessage", runt_webview_set_on_message); + SET_FUNCTION(runt, "webviewSetOnLoad", runt_webview_set_on_load); + SET_FUNCTION(runt, "webviewSetOnError", runt_webview_set_on_error); + SET_FUNCTION(runt, "webviewSetOnClose", runt_webview_set_on_close); + SET_FUNCTION(runt, "webviewCount", runt_webview_count); + SET_FUNCTION(runt, "webviewIsValid", runt_webview_is_valid); + SET_FUNCTION(runt, "webviewSetActiveDOM", runt_webview_set_active_dom); + SET_FUNCTION(runt, "webviewGetActiveDOM", runt_webview_get_active_dom); + SET_FUNCTION(runt, "webviewEvalJSAsync", runt_webview_eval_js_async); + SET_FUNCTION(runt, "webviewGoBack", runt_webview_go_back); + SET_FUNCTION(runt, "webviewGoForward", runt_webview_go_forward); + SET_FUNCTION(runt, "webviewReload", runt_webview_reload); + SET_FUNCTION(runt, "webviewCanGoBack", runt_webview_can_go_back); + SET_FUNCTION(runt, "webviewCanGoForward", runt_webview_can_go_forward); + SET_FUNCTION(runt, "webviewGetURL", runt_webview_get_url); + SET_FUNCTION(runt, "webviewGetPageTitle", runt_webview_get_page_title); + SET_FUNCTION(runt, "webviewMinimize", runt_webview_minimize); + SET_FUNCTION(runt, "webviewMaximize", runt_webview_maximize); + SET_FUNCTION(runt, "webviewRestore", runt_webview_restore); + SET_FUNCTION(runt, "webviewSetFullscreen", runt_webview_set_fullscreen); + SET_FUNCTION(runt, "webviewIsFullscreen", runt_webview_is_fullscreen); + SET_FUNCTION(runt, "webviewEnableDevTools", runt_webview_enable_devtools); + SET_FUNCTION(runt, "webviewSetContextMenu", runt_webview_set_context_menu); + #endif + Local global = ObjectTemplate::New(isolate); global->Set(String::NewFromUtf8(isolate, "RunT").ToLocalChecked(), runt); global->Set(String::NewFromUtf8(isolate, "runt").ToLocalChecked(), runt); // lowercase alias for JavaScript compatibility @@ -4072,6 +4120,7 @@ int kickstart(int argc, char **argv) { // TODO: find and fix while (update_func.IsEmpty()) { + kinc_internal_handle_messages(); handle_worker_messages(isolate, global_context); EngineManager::AsyncEngine::instance().process_events(); MicrotasksScope::PerformCheckpoint(isolate); diff --git a/Sources/webview.cpp b/Sources/webview.cpp new file mode 100644 index 0000000..e988c3a --- /dev/null +++ b/Sources/webview.cpp @@ -0,0 +1,975 @@ +#ifdef WITH_WEBVIEW + +#include "webview.h" +#include +#include + +WebViewManager& WebViewManager::instance() { + static WebViewManager inst; + return inst; +} + +WebViewManager::~WebViewManager() { + for (auto& [id, wv] : webviews) { + if (wv->platformHandle) { + platformDestroy(wv.get()); + } + } + webviews.clear(); +} + +int WebViewManager::create(v8::Isolate* isolate, v8::Local context, int parentWindow, int x, int y, int width, int height, bool transparent, const std::string& title) { + auto wv = std::make_unique(); + wv->id = nextId++; + wv->parentWindow = parentWindow; + wv->x = x; + wv->y = y; + wv->width = width; + wv->height = height; + wv->autoResize = (parentWindow >= 0 && (width == -1 || height == -1)); + wv->transparent = transparent; + wv->clickThrough = false; + wv->visible = false; + wv->title = title; + wv->isolate = isolate; + wv->context.Reset(isolate, context); + + if (wv->autoResize && parentWindow >= 0) { + if (wv->width == -1) wv->width = kinc_window_width(parentWindow); + if (wv->height == -1) wv->height = kinc_window_height(parentWindow); + } + + int id = wv->id; + int result = platformCreate(wv.get()); + if (result < 0) { + kinc_log(KINC_LOG_LEVEL_ERROR, "[WebView] Failed to create WebView (platform returned %d)", result); + return -1; + } + + webviews[id] = std::move(wv); + kinc_log(KINC_LOG_LEVEL_INFO, "[WebView] Created WebView id=%d parent=%d %dx%d", id, parentWindow, width, height); + + if (activeDomId < 0) { + setActiveDOM(id, isolate); + } + + return id; +} + +void WebViewManager::destroy(int id) { + auto it = webviews.find(id); + if (it == webviews.end()) return; + + if (it->second->platformHandle) { + platformDestroy(it->second.get()); + } + webviews.erase(it); + kinc_log(KINC_LOG_LEVEL_INFO, "[WebView] Destroyed WebView id=%d", id); +} + +bool WebViewManager::isValid(int id) { + return webviews.find(id) != webviews.end(); +} + +int WebViewManager::count() { + return (int)webviews.size(); +} + +void WebViewManager::loadHTML(int id, const std::string& html) { + auto it = webviews.find(id); + if (it == webviews.end()) return; + platformLoadHTML(it->second.get(), html); +} + +void WebViewManager::loadURL(int id, const std::string& url) { + auto it = webviews.find(id); + if (it == webviews.end()) return; + platformLoadURL(it->second.get(), url); +} + +void WebViewManager::evalJS(int id, const std::string& js) { + auto it = webviews.find(id); + if (it == webviews.end()) return; + platformEvalJS(it->second.get(), js); +} + +void WebViewManager::evalJSAsync(int id, v8::Isolate* isolate, v8::Local callback, const std::string& js) { + auto it = webviews.find(id); + if (it == webviews.end()) return; + it->second->onEvalResult.Reset(isolate, callback); + platformEvalJSAsync(it->second.get(), js); +} + +void WebViewManager::setActiveDOM(int id, v8::Isolate* isolate) { + if (id >= 0 && webviews.find(id) == webviews.end()) return; + activeDomId = id; + + if (id < 0) return; + + auto it = webviews.find(id); + if (it == webviews.end()) return; + auto* wv = it->second.get(); + + v8::HandleScope handleScope(isolate); + v8::Local ctx = wv->context.Get(isolate); + v8::Context::Scope ctxScope(ctx); + + // This is the proxy interception to support the web apis. TODO replace + std::string proxyJs = std::string("(function(){var _id=") + std::to_string(id) + ";" R"JS( +var _r=0; +function _eval(j){runt.webviewEvalJS(_id,j);} +function _evalAsync(j,cb){runt.webviewEvalJSAsync(_id,j,cb);} +function _arg(x){ + if(x!==null&&(typeof x==='object'||typeof x==='function')&&x.__jsExpr)return x.__jsExpr; + return JSON.stringify(x); +} +function _proxy(expr){ + var fn=function(){}; + var _loaded=false,_data=null,_fired=false; + function _fire(){ + if(_fired)return;_fired=true; + _evalAsync(expr,function(r){_loaded=true;try{_data=JSON.parse(r);}catch(e){_data=r;}}); + } + return new Proxy(fn,{ + get:function(t,p){ + if(p==='__jsExpr')return expr; + if(p==='loaded'){_fire();return _loaded;} + if(p==='data'){_fire();return _data;} + if(typeof p==='symbol'||p==='then')return undefined; + return _proxy(expr+"."+p); + }, + apply:function(t,thisArg,args){ + var r='__c'+(++_r); + _eval("window['"+r+"']="+expr+"("+args.map(_arg).join(',')+")"); + return _proxy("window['"+r+"']"); + }, + set:function(t,p,v){_eval(expr+"."+p+"="+_arg(v));return true;} + }); +} +return [_proxy("window"),_proxy("window.document")]; +})(); +)JS"; + + v8::Local src = v8::String::NewFromUtf8(isolate, proxyJs.c_str(), v8::NewStringType::kNormal, (int)proxyJs.length()).ToLocalChecked(); + v8::TryCatch tryCatch(isolate); + v8::MaybeLocal compileResult = v8::Script::Compile(ctx, src); + if (compileResult.IsEmpty()) { + v8::Local msg = tryCatch.Message(); + if (!msg.IsEmpty()) { + v8::String::Utf8Value errorStr(isolate, msg->Get()); + kinc_log(KINC_LOG_LEVEL_ERROR, "WebView proxy compile error: %s", *errorStr); + } + return; + } + v8::Local script = compileResult.ToLocalChecked(); + v8::MaybeLocal result = script->Run(ctx); + if (tryCatch.HasCaught()) { + v8::Local msg = tryCatch.Message(); + if (!msg.IsEmpty()) { + v8::String::Utf8Value errorStr(isolate, msg->Get()); + kinc_log(KINC_LOG_LEVEL_ERROR, "Webview proxy run error: %s", *errorStr); + } else { + kinc_log(KINC_LOG_LEVEL_ERROR, "Webview proxy: no message"); + } + return; + } + if (!result.IsEmpty()) { + v8::Local val = result.ToLocalChecked(); + if (val->IsArray()) { + v8::Local arr = val.As(); + v8::Local globalObj = ctx->Global(); + globalObj->Set(ctx, v8::String::NewFromUtf8(isolate, "window").ToLocalChecked(), arr->Get(ctx, 0).ToLocalChecked()).Check(); + globalObj->Set(ctx, v8::String::NewFromUtf8(isolate, "document").ToLocalChecked(), arr->Get(ctx, 1).ToLocalChecked()).Check(); + kinc_log(KINC_LOG_LEVEL_INFO, "DOM proxy injected successfully for webview id=%d", id); + } else { + kinc_log(KINC_LOG_LEVEL_ERROR, "Proxy returned non array type"); + } + } +} + +int WebViewManager::getActiveDOM() { + return activeDomId; +} + +void WebViewManager::show(int id) { + auto it = webviews.find(id); + if (it == webviews.end()) return; + it->second->visible = true; + platformShow(it->second.get()); +} + +void WebViewManager::hide(int id) { + auto it = webviews.find(id); + if (it == webviews.end()) return; + it->second->visible = false; + platformHide(it->second.get()); +} + +void WebViewManager::resize(int id, int width, int height) { + auto it = webviews.find(id); + if (it == webviews.end()) return; + it->second->width = width; + it->second->height = height; + it->second->autoResize = false; + platformResize(it->second.get(), width, height); +} + +void WebViewManager::move(int id, int x, int y) { + auto it = webviews.find(id); + if (it == webviews.end()) return; + it->second->x = x; + it->second->y = y; + platformMove(it->second.get(), x, y); +} + +void WebViewManager::setBounds(int id, int x, int y, int width, int height) { + auto it = webviews.find(id); + if (it == webviews.end()) return; + it->second->x = x; + it->second->y = y; + it->second->width = width; + it->second->height = height; + it->second->autoResize = false; + platformResize(it->second.get(), width, height); + platformMove(it->second.get(), x, y); +} + +int WebViewManager::getX(int id) { + auto it = webviews.find(id); + if (it == webviews.end()) return 0; + return it->second->x; +} + +int WebViewManager::getY(int id) { + auto it = webviews.find(id); + if (it == webviews.end()) return 0; + return it->second->y; +} + +int WebViewManager::getWidth(int id) { + auto it = webviews.find(id); + if (it == webviews.end()) return 0; + return it->second->width; +} + +int WebViewManager::getHeight(int id) { + auto it = webviews.find(id); + if (it == webviews.end()) return 0; + return it->second->height; +} + +void WebViewManager::setTransparent(int id, bool transparent) { + auto it = webviews.find(id); + if (it == webviews.end()) return; + it->second->transparent = transparent; + platformSetTransparent(it->second.get(), transparent); +} + +void WebViewManager::setClickThrough(int id, bool enabled) { + auto it = webviews.find(id); + if (it == webviews.end()) return; + it->second->clickThrough = enabled; + platformSetClickThrough(it->second.get(), enabled); +} + +void WebViewManager::setTitle(int id, const std::string& title) { + auto it = webviews.find(id); + if (it == webviews.end()) return; + it->second->title = title; + platformSetTitle(it->second.get(), title); +} + +void WebViewManager::send(int id, const std::string& message) { + auto it = webviews.find(id); + if (it == webviews.end()) return; + platformSend(it->second.get(), message); +} + +void WebViewManager::goBack(int id) { + auto it = webviews.find(id); + if (it == webviews.end()) return; + platformGoBack(it->second.get()); +} + +void WebViewManager::goForward(int id) { + auto it = webviews.find(id); + if (it == webviews.end()) return; + platformGoForward(it->second.get()); +} + +void WebViewManager::reload(int id) { + auto it = webviews.find(id); + if (it == webviews.end()) return; + platformReload(it->second.get()); +} + +bool WebViewManager::canGoBack(int id) { + auto it = webviews.find(id); + if (it == webviews.end()) return false; + return platformCanGoBack(it->second.get()); +} + +bool WebViewManager::canGoForward(int id) { + auto it = webviews.find(id); + if (it == webviews.end()) return false; + return platformCanGoForward(it->second.get()); +} + +std::string WebViewManager::getURL(int id) { + auto it = webviews.find(id); + if (it == webviews.end()) return ""; + return platformGetURL(it->second.get()); +} + +std::string WebViewManager::getPageTitle(int id) { + auto it = webviews.find(id); + if (it == webviews.end()) return ""; + return platformGetTitle(it->second.get()); +} + +void WebViewManager::minimize(int id) { + auto it = webviews.find(id); + if (it == webviews.end()) return; + platformMinimize(it->second.get()); +} + +void WebViewManager::maximize(int id) { + auto it = webviews.find(id); + if (it == webviews.end()) return; + platformMaximize(it->second.get()); +} + +void WebViewManager::restore(int id) { + auto it = webviews.find(id); + if (it == webviews.end()) return; + platformRestore(it->second.get()); +} + +void WebViewManager::setFullscreen(int id, bool fullscreen) { + auto it = webviews.find(id); + if (it == webviews.end()) return; + platformSetFullscreen(it->second.get(), fullscreen); +} + +bool WebViewManager::isFullscreen(int id) { + auto it = webviews.find(id); + if (it == webviews.end()) return false; + return platformIsFullscreen(it->second.get()); +} + +void WebViewManager::enableDevTools(int id, bool enabled) { + auto it = webviews.find(id); + if (it == webviews.end()) return; + platformEnableDevTools(it->second.get(), enabled); +} + +void WebViewManager::setContextMenuEnabled(int id, bool enabled) { + auto it = webviews.find(id); + if (it == webviews.end()) return; + platformSetContextMenuEnabled(it->second.get(), enabled); +} + +void WebViewManager::setOnMessage(int id, v8::Isolate* isolate, v8::Local cb) { + auto it = webviews.find(id); + if (it == webviews.end()) return; + it->second->onMessage.Reset(isolate, cb); +} + +void WebViewManager::setOnLoad(int id, v8::Isolate* isolate, v8::Local cb) { + auto it = webviews.find(id); + if (it == webviews.end()) return; + it->second->onLoad.Reset(isolate, cb); +} + +void WebViewManager::setOnError(int id, v8::Isolate* isolate, v8::Local cb) { + auto it = webviews.find(id); + if (it == webviews.end()) return; + it->second->onError.Reset(isolate, cb); +} + +void WebViewManager::setOnClose(int id, v8::Isolate* isolate, v8::Local cb) { + auto it = webviews.find(id); + if (it == webviews.end()) return; + it->second->onClose.Reset(isolate, cb); +} + +void WebViewManager::dispatchMessage(int id, const std::string& message) { + auto it = webviews.find(id); + if (it == webviews.end()) return; + auto* wv = it->second.get(); + if (wv->onMessage.IsEmpty()) return; + + v8::Isolate* isolate = wv->isolate; + v8::Locker locker(isolate); + v8::Isolate::Scope isolateScope(isolate); + v8::HandleScope handleScope(isolate); + v8::Local ctx = wv->context.Get(isolate); + v8::Context::Scope ctxScope(ctx); + + v8::Local cb = wv->onMessage.Get(isolate); + v8::Local arg = v8::String::NewFromUtf8(isolate, message.c_str(), v8::NewStringType::kNormal, (int)message.length()).ToLocalChecked(); + cb->Call(ctx, v8::Undefined(isolate), 1, &arg).IsEmpty(); +} + +void WebViewManager::dispatchLoad(int id) { + auto it = webviews.find(id); + if (it == webviews.end()) return; + auto* wv = it->second.get(); + if (wv->onLoad.IsEmpty()) return; + + v8::Isolate* isolate = wv->isolate; + v8::Locker locker(isolate); + v8::Isolate::Scope isolateScope(isolate); + v8::HandleScope handleScope(isolate); + v8::Local ctx = wv->context.Get(isolate); + v8::Context::Scope ctxScope(ctx); + + v8::Local cb = wv->onLoad.Get(isolate); + v8::Local arg = v8::Undefined(isolate); + cb->Call(ctx, v8::Undefined(isolate), 1, &arg).IsEmpty(); +} + +void WebViewManager::dispatchError(int id, const std::string& error) { + auto it = webviews.find(id); + if (it == webviews.end()) return; + auto* wv = it->second.get(); + if (wv->onError.IsEmpty()) return; + + v8::Isolate* isolate = wv->isolate; + v8::Locker locker(isolate); + v8::Isolate::Scope isolateScope(isolate); + v8::HandleScope handleScope(isolate); + v8::Local ctx = wv->context.Get(isolate); + v8::Context::Scope ctxScope(ctx); + + v8::Local cb = wv->onError.Get(isolate); + v8::Local arg = v8::String::NewFromUtf8(isolate, error.c_str(), v8::NewStringType::kNormal, (int)error.length()).ToLocalChecked(); + cb->Call(ctx, v8::Undefined(isolate), 1, &arg).IsEmpty(); +} + +void WebViewManager::dispatchClose(int id) { + auto it = webviews.find(id); + if (it == webviews.end()) return; + auto* wv = it->second.get(); + if (wv->onClose.IsEmpty()) return; + + v8::Isolate* isolate = wv->isolate; + v8::Locker locker(isolate); + v8::Isolate::Scope isolateScope(isolate); + v8::HandleScope handleScope(isolate); + v8::Local ctx = wv->context.Get(isolate); + v8::Context::Scope ctxScope(ctx); + + v8::Local cb = wv->onClose.Get(isolate); + v8::Local arg = v8::Undefined(isolate); + cb->Call(ctx, v8::Undefined(isolate), 1, &arg).IsEmpty(); +} + +void WebViewManager::dispatchEvalResult(int id, const std::string& result) { + auto it = webviews.find(id); + if (it == webviews.end()) return; + auto* wv = it->second.get(); + if (wv->onEvalResult.IsEmpty()) return; + + v8::Isolate* isolate = wv->isolate; + v8::Locker locker(isolate); + v8::Isolate::Scope isolateScope(isolate); + v8::HandleScope handleScope(isolate); + v8::Local ctx = wv->context.Get(isolate); + v8::Context::Scope ctxScope(ctx); + + v8::Local cb = wv->onEvalResult.Get(isolate); + v8::Local arg = v8::String::NewFromUtf8(isolate, result.c_str(), v8::NewStringType::kNormal, (int)result.length()).ToLocalChecked(); + cb->Call(ctx, v8::Undefined(isolate), 1, &arg).IsEmpty(); +} + +void WebViewManager::tick() { + platformTick(); +} + +void WebViewManager::onWindowResize(int windowId, int width, int height) { + for (auto& [id, wv] : webviews) { + if (wv->parentWindow == windowId && wv->autoResize) { + wv->width = width; + wv->height = height; + platformResize(wv.get(), width, height); + } + } +} + +static int getIntOption(v8::Isolate* isolate, v8::Local ctx, + v8::Local opts, const char* key, int defaultVal) { + v8::Local val; + v8::Local keyStr = v8::String::NewFromUtf8(isolate, key).ToLocalChecked(); + if (opts->Get(ctx, keyStr).ToLocal(&val) && val->IsNumber()) { + return val->Int32Value(ctx).FromJust(); + } + return defaultVal; +} + +static bool getBoolOption(v8::Isolate* isolate, v8::Local ctx, + v8::Local opts, const char* key, bool defaultVal) { + v8::Local val; + v8::Local keyStr = v8::String::NewFromUtf8(isolate, key).ToLocalChecked(); + if (opts->Get(ctx, keyStr).ToLocal(&val) && val->IsBoolean()) { + return val->BooleanValue(isolate); + } + return defaultVal; +} + +static std::string getStringOption(v8::Isolate* isolate, v8::Local ctx, + v8::Local opts, const char* key, const char* defaultVal) { + v8::Local val; + v8::Local keyStr = v8::String::NewFromUtf8(isolate, key).ToLocalChecked(); + if (opts->Get(ctx, keyStr).ToLocal(&val) && val->IsString()) { + v8::String::Utf8Value str(isolate, val); + return std::string(*str); + } + return std::string(defaultVal); +} + +void runt_webview_create(const v8::FunctionCallbackInfo& args) { + v8::Isolate* isolate = args.GetIsolate(); + v8::HandleScope scope(isolate); + v8::Local ctx = isolate->GetCurrentContext(); + + if (args.Length() < 1 || !args[0]->IsObject()) { + isolate->ThrowException(v8::Exception::Error( + v8::String::NewFromUtf8(isolate, "Options object required").ToLocalChecked())); + return; + } + + v8::Local opts = args[0]->ToObject(ctx).ToLocalChecked(); + + int parentWindow = getIntOption(isolate, ctx, opts, "parentWindow", -1); + int x = getIntOption(isolate, ctx, opts, "x", 0); + int y = getIntOption(isolate, ctx, opts, "y", 0); + int width = getIntOption(isolate, ctx, opts, "width", 800); + int height = getIntOption(isolate, ctx, opts, "height", 600); + bool transparent = getBoolOption(isolate, ctx, opts, "transparent", true); + std::string title = getStringOption(isolate, ctx, opts, "title", "WebView"); + + int id = WebViewManager::instance().create(isolate, ctx, parentWindow, x, y, + width, height, transparent, title); + args.GetReturnValue().Set(v8::Number::New(isolate, id)); + + if (id < 0) return; + + v8::Local val; + v8::Local htmlKey = v8::String::NewFromUtf8(isolate, "html").ToLocalChecked(); + if (opts->Get(ctx, htmlKey).ToLocal(&val) && val->IsString()) { + v8::String::Utf8Value str(isolate, val); + WebViewManager::instance().loadHTML(id, *str); + } + + v8::Local urlKey = v8::String::NewFromUtf8(isolate, "url").ToLocalChecked(); + if (opts->Get(ctx, urlKey).ToLocal(&val) && val->IsString()) { + v8::String::Utf8Value str(isolate, val); + WebViewManager::instance().loadURL(id, *str); + } +} + +void runt_webview_load_html(const v8::FunctionCallbackInfo& args) { + v8::Isolate* isolate = args.GetIsolate(); + v8::HandleScope scope(isolate); + + if (args.Length() < 2 || !args[0]->IsNumber() || !args[1]->IsString()) { + isolate->ThrowException(v8::Exception::Error( + v8::String::NewFromUtf8(isolate, "ID and HTML string required").ToLocalChecked())); + return; + } + + int id = args[0]->Int32Value(isolate->GetCurrentContext()).FromJust(); + v8::String::Utf8Value str(isolate, args[1]); + WebViewManager::instance().loadHTML(id, *str); +} + +void runt_webview_load_url(const v8::FunctionCallbackInfo& args) { + v8::Isolate* isolate = args.GetIsolate(); + v8::HandleScope scope(isolate); + + if (args.Length() < 2 || !args[0]->IsNumber() || !args[1]->IsString()) { + isolate->ThrowException(v8::Exception::Error( + v8::String::NewFromUtf8(isolate, "ID and URL string required").ToLocalChecked())); + return; + } + + int id = args[0]->Int32Value(isolate->GetCurrentContext()).FromJust(); + v8::String::Utf8Value str(isolate, args[1]); + WebViewManager::instance().loadURL(id, *str); +} + +void runt_webview_eval_js(const v8::FunctionCallbackInfo& args) { + v8::Isolate* isolate = args.GetIsolate(); + v8::HandleScope scope(isolate); + + if (args.Length() < 2 || !args[0]->IsNumber() || !args[1]->IsString()) { + isolate->ThrowException(v8::Exception::Error( + v8::String::NewFromUtf8(isolate, "ID and JS string required").ToLocalChecked())); + return; + } + + int id = args[0]->Int32Value(isolate->GetCurrentContext()).FromJust(); + v8::String::Utf8Value str(isolate, args[1]); + WebViewManager::instance().evalJS(id, *str); +} + +void runt_webview_show(const v8::FunctionCallbackInfo& args) { + v8::Isolate* isolate = args.GetIsolate(); + v8::HandleScope scope(isolate); + + if (args.Length() < 1 || !args[0]->IsNumber()) return; + int id = args[0]->Int32Value(isolate->GetCurrentContext()).FromJust(); + WebViewManager::instance().show(id); +} + +void runt_webview_hide(const v8::FunctionCallbackInfo& args) { + v8::Isolate* isolate = args.GetIsolate(); + v8::HandleScope scope(isolate); + + if (args.Length() < 1 || !args[0]->IsNumber()) return; + int id = args[0]->Int32Value(isolate->GetCurrentContext()).FromJust(); + WebViewManager::instance().hide(id); +} + +void runt_webview_destroy(const v8::FunctionCallbackInfo& args) { + v8::Isolate* isolate = args.GetIsolate(); + v8::HandleScope scope(isolate); + + if (args.Length() < 1 || !args[0]->IsNumber()) return; + int id = args[0]->Int32Value(isolate->GetCurrentContext()).FromJust(); + WebViewManager::instance().destroy(id); +} + +void runt_webview_resize(const v8::FunctionCallbackInfo& args) { + v8::Isolate* isolate = args.GetIsolate(); + v8::HandleScope scope(isolate); + + if (args.Length() < 3 || !args[0]->IsNumber() || !args[1]->IsNumber() || !args[2]->IsNumber()) return; + int id = args[0]->Int32Value(isolate->GetCurrentContext()).FromJust(); + int w = args[1]->Int32Value(isolate->GetCurrentContext()).FromJust(); + int h = args[2]->Int32Value(isolate->GetCurrentContext()).FromJust(); + WebViewManager::instance().resize(id, w, h); +} + +void runt_webview_move(const v8::FunctionCallbackInfo& args) { + v8::Isolate* isolate = args.GetIsolate(); + v8::HandleScope scope(isolate); + + if (args.Length() < 3 || !args[0]->IsNumber() || !args[1]->IsNumber() || !args[2]->IsNumber()) return; + int id = args[0]->Int32Value(isolate->GetCurrentContext()).FromJust(); + int x = args[1]->Int32Value(isolate->GetCurrentContext()).FromJust(); + int y = args[2]->Int32Value(isolate->GetCurrentContext()).FromJust(); + WebViewManager::instance().move(id, x, y); +} + +void runt_webview_set_bounds(const v8::FunctionCallbackInfo& args) { + v8::Isolate* isolate = args.GetIsolate(); + v8::HandleScope scope(isolate); + + if (args.Length() < 5) return; + int id = args[0]->Int32Value(isolate->GetCurrentContext()).FromJust(); + int x = args[1]->Int32Value(isolate->GetCurrentContext()).FromJust(); + int y = args[2]->Int32Value(isolate->GetCurrentContext()).FromJust(); + int w = args[3]->Int32Value(isolate->GetCurrentContext()).FromJust(); + int h = args[4]->Int32Value(isolate->GetCurrentContext()).FromJust(); + WebViewManager::instance().setBounds(id, x, y, w, h); +} + +void runt_webview_get_x(const v8::FunctionCallbackInfo& args) { + v8::Isolate* isolate = args.GetIsolate(); + v8::HandleScope scope(isolate); + if (args.Length() < 1 || !args[0]->IsNumber()) return; + int id = args[0]->Int32Value(isolate->GetCurrentContext()).FromJust(); + args.GetReturnValue().Set(v8::Int32::New(isolate, WebViewManager::instance().getX(id))); +} + +void runt_webview_get_y(const v8::FunctionCallbackInfo& args) { + v8::Isolate* isolate = args.GetIsolate(); + v8::HandleScope scope(isolate); + if (args.Length() < 1 || !args[0]->IsNumber()) return; + int id = args[0]->Int32Value(isolate->GetCurrentContext()).FromJust(); + args.GetReturnValue().Set(v8::Int32::New(isolate, WebViewManager::instance().getY(id))); +} + +void runt_webview_get_width(const v8::FunctionCallbackInfo& args) { + v8::Isolate* isolate = args.GetIsolate(); + v8::HandleScope scope(isolate); + if (args.Length() < 1 || !args[0]->IsNumber()) return; + int id = args[0]->Int32Value(isolate->GetCurrentContext()).FromJust(); + args.GetReturnValue().Set(v8::Int32::New(isolate, WebViewManager::instance().getWidth(id))); +} + +void runt_webview_get_height(const v8::FunctionCallbackInfo& args) { + v8::Isolate* isolate = args.GetIsolate(); + v8::HandleScope scope(isolate); + if (args.Length() < 1 || !args[0]->IsNumber()) return; + int id = args[0]->Int32Value(isolate->GetCurrentContext()).FromJust(); + args.GetReturnValue().Set(v8::Int32::New(isolate, WebViewManager::instance().getHeight(id))); +} + +void runt_webview_set_transparent(const v8::FunctionCallbackInfo& args) { + v8::Isolate* isolate = args.GetIsolate(); + v8::HandleScope scope(isolate); + if (args.Length() < 2 || !args[0]->IsNumber() || !args[1]->IsBoolean()) return; + int id = args[0]->Int32Value(isolate->GetCurrentContext()).FromJust(); + bool val = args[1]->BooleanValue(isolate); + WebViewManager::instance().setTransparent(id, val); +} + +void runt_webview_set_click_through(const v8::FunctionCallbackInfo& args) { + v8::Isolate* isolate = args.GetIsolate(); + v8::HandleScope scope(isolate); + if (args.Length() < 2 || !args[0]->IsNumber() || !args[1]->IsBoolean()) return; + int id = args[0]->Int32Value(isolate->GetCurrentContext()).FromJust(); + bool val = args[1]->BooleanValue(isolate); + WebViewManager::instance().setClickThrough(id, val); +} + +void runt_webview_set_title(const v8::FunctionCallbackInfo& args) { + v8::Isolate* isolate = args.GetIsolate(); + v8::HandleScope scope(isolate); + if (args.Length() < 2 || !args[0]->IsNumber() || !args[1]->IsString()) return; + int id = args[0]->Int32Value(isolate->GetCurrentContext()).FromJust(); + v8::String::Utf8Value str(isolate, args[1]); + WebViewManager::instance().setTitle(id, *str); +} + +void runt_webview_send(const v8::FunctionCallbackInfo& args) { + v8::Isolate* isolate = args.GetIsolate(); + v8::HandleScope scope(isolate); + + if (args.Length() < 2 || !args[0]->IsNumber()) { + isolate->ThrowException(v8::Exception::Error( + v8::String::NewFromUtf8(isolate, "ID and message required").ToLocalChecked())); + return; + } + + int id = args[0]->Int32Value(isolate->GetCurrentContext()).FromJust(); + + std::string message; + if (args[1]->IsString()) { + v8::String::Utf8Value str(isolate, args[1]); + message = *str; + } else { + v8::Local ctx = isolate->GetCurrentContext(); + v8::Local json = v8::JSON::Stringify(ctx, args[1]).ToLocalChecked(); + v8::String::Utf8Value str(isolate, json); + message = *str; + } + + WebViewManager::instance().send(id, message); +} + +void runt_webview_set_on_message(const v8::FunctionCallbackInfo& args) { + v8::Isolate* isolate = args.GetIsolate(); + v8::HandleScope scope(isolate); + if (args.Length() < 2 || !args[0]->IsNumber() || !args[1]->IsFunction()) return; + int id = args[0]->Int32Value(isolate->GetCurrentContext()).FromJust(); + WebViewManager::instance().setOnMessage(id, isolate, v8::Local::Cast(args[1])); +} + +void runt_webview_set_on_load(const v8::FunctionCallbackInfo& args) { + v8::Isolate* isolate = args.GetIsolate(); + v8::HandleScope scope(isolate); + if (args.Length() < 2 || !args[0]->IsNumber() || !args[1]->IsFunction()) return; + int id = args[0]->Int32Value(isolate->GetCurrentContext()).FromJust(); + WebViewManager::instance().setOnLoad(id, isolate, v8::Local::Cast(args[1])); +} + +void runt_webview_set_on_error(const v8::FunctionCallbackInfo& args) { + v8::Isolate* isolate = args.GetIsolate(); + v8::HandleScope scope(isolate); + if (args.Length() < 2 || !args[0]->IsNumber() || !args[1]->IsFunction()) return; + int id = args[0]->Int32Value(isolate->GetCurrentContext()).FromJust(); + WebViewManager::instance().setOnError(id, isolate, v8::Local::Cast(args[1])); +} + +void runt_webview_set_on_close(const v8::FunctionCallbackInfo& args) { + v8::Isolate* isolate = args.GetIsolate(); + v8::HandleScope scope(isolate); + if (args.Length() < 2 || !args[0]->IsNumber() || !args[1]->IsFunction()) return; + int id = args[0]->Int32Value(isolate->GetCurrentContext()).FromJust(); + WebViewManager::instance().setOnClose(id, isolate, v8::Local::Cast(args[1])); +} + +void runt_webview_count(const v8::FunctionCallbackInfo& args) { + v8::Isolate* isolate = args.GetIsolate(); + v8::HandleScope scope(isolate); + args.GetReturnValue().Set(v8::Int32::New(isolate, WebViewManager::instance().count())); +} + +void runt_webview_is_valid(const v8::FunctionCallbackInfo& args) { + v8::Isolate* isolate = args.GetIsolate(); + v8::HandleScope scope(isolate); + if (args.Length() < 1 || !args[0]->IsNumber()) { + args.GetReturnValue().Set(v8::Boolean::New(isolate, false)); + return; + } + int id = args[0]->Int32Value(isolate->GetCurrentContext()).FromJust(); + args.GetReturnValue().Set(v8::Boolean::New(isolate, WebViewManager::instance().isValid(id))); +} + +void runt_webview_set_active_dom(const v8::FunctionCallbackInfo& args) { + v8::Isolate* isolate = args.GetIsolate(); + v8::HandleScope scope(isolate); + if (args.Length() < 1 || !args[0]->IsNumber()) return; + int id = args[0]->Int32Value(isolate->GetCurrentContext()).FromJust(); + WebViewManager::instance().setActiveDOM(id, isolate); +} + +void runt_webview_get_active_dom(const v8::FunctionCallbackInfo& args) { + v8::Isolate* isolate = args.GetIsolate(); + v8::HandleScope scope(isolate); + args.GetReturnValue().Set(v8::Int32::New(isolate, WebViewManager::instance().getActiveDOM())); +} + +void runt_webview_eval_js_async(const v8::FunctionCallbackInfo& args) { + v8::Isolate* isolate = args.GetIsolate(); + v8::HandleScope scope(isolate); + + if (args.Length() < 3 || !args[0]->IsNumber() || !args[1]->IsString() || !args[2]->IsFunction()) { + isolate->ThrowException(v8::Exception::Error( + v8::String::NewFromUtf8(isolate, "ID, JS string, and callback required").ToLocalChecked())); + return; + } + + int id = args[0]->Int32Value(isolate->GetCurrentContext()).FromJust(); + v8::String::Utf8Value str(isolate, args[1]); + v8::Local cb = v8::Local::Cast(args[2]); + WebViewManager::instance().evalJSAsync(id, isolate, cb, *str); +} + +void runt_webview_go_back(const v8::FunctionCallbackInfo& args) { + v8::Isolate* isolate = args.GetIsolate(); + v8::HandleScope scope(isolate); + if (args.Length() < 1 || !args[0]->IsNumber()) return; + int id = args[0]->Int32Value(isolate->GetCurrentContext()).FromJust(); + WebViewManager::instance().goBack(id); +} + +void runt_webview_go_forward(const v8::FunctionCallbackInfo& args) { + v8::Isolate* isolate = args.GetIsolate(); + v8::HandleScope scope(isolate); + if (args.Length() < 1 || !args[0]->IsNumber()) return; + int id = args[0]->Int32Value(isolate->GetCurrentContext()).FromJust(); + WebViewManager::instance().goForward(id); +} + +void runt_webview_reload(const v8::FunctionCallbackInfo& args) { + v8::Isolate* isolate = args.GetIsolate(); + v8::HandleScope scope(isolate); + if (args.Length() < 1 || !args[0]->IsNumber()) return; + int id = args[0]->Int32Value(isolate->GetCurrentContext()).FromJust(); + WebViewManager::instance().reload(id); +} + +void runt_webview_can_go_back(const v8::FunctionCallbackInfo& args) { + v8::Isolate* isolate = args.GetIsolate(); + v8::HandleScope scope(isolate); + if (args.Length() < 1 || !args[0]->IsNumber()) { + args.GetReturnValue().Set(v8::Boolean::New(isolate, false)); + return; + } + int id = args[0]->Int32Value(isolate->GetCurrentContext()).FromJust(); + args.GetReturnValue().Set(v8::Boolean::New(isolate, WebViewManager::instance().canGoBack(id))); +} + +void runt_webview_can_go_forward(const v8::FunctionCallbackInfo& args) { + v8::Isolate* isolate = args.GetIsolate(); + v8::HandleScope scope(isolate); + if (args.Length() < 1 || !args[0]->IsNumber()) { + args.GetReturnValue().Set(v8::Boolean::New(isolate, false)); + return; + } + int id = args[0]->Int32Value(isolate->GetCurrentContext()).FromJust(); + args.GetReturnValue().Set(v8::Boolean::New(isolate, WebViewManager::instance().canGoForward(id))); +} + +void runt_webview_get_url(const v8::FunctionCallbackInfo& args) { + v8::Isolate* isolate = args.GetIsolate(); + v8::HandleScope scope(isolate); + if (args.Length() < 1 || !args[0]->IsNumber()) { + args.GetReturnValue().Set(v8::String::NewFromUtf8(isolate, "").ToLocalChecked()); + return; + } + int id = args[0]->Int32Value(isolate->GetCurrentContext()).FromJust(); + std::string url = WebViewManager::instance().getURL(id); + args.GetReturnValue().Set(v8::String::NewFromUtf8(isolate, url.c_str(), v8::NewStringType::kNormal, (int)url.length()).ToLocalChecked()); +} + +void runt_webview_get_page_title(const v8::FunctionCallbackInfo& args) { + v8::Isolate* isolate = args.GetIsolate(); + v8::HandleScope scope(isolate); + if (args.Length() < 1 || !args[0]->IsNumber()) { + args.GetReturnValue().Set(v8::String::NewFromUtf8(isolate, "").ToLocalChecked()); + return; + } + int id = args[0]->Int32Value(isolate->GetCurrentContext()).FromJust(); + std::string title = WebViewManager::instance().getPageTitle(id); + args.GetReturnValue().Set(v8::String::NewFromUtf8(isolate, title.c_str(), v8::NewStringType::kNormal, (int)title.length()).ToLocalChecked()); +} + +void runt_webview_minimize(const v8::FunctionCallbackInfo& args) { + v8::Isolate* isolate = args.GetIsolate(); + v8::HandleScope scope(isolate); + if (args.Length() < 1 || !args[0]->IsNumber()) return; + int id = args[0]->Int32Value(isolate->GetCurrentContext()).FromJust(); + WebViewManager::instance().minimize(id); +} + +void runt_webview_maximize(const v8::FunctionCallbackInfo& args) { + v8::Isolate* isolate = args.GetIsolate(); + v8::HandleScope scope(isolate); + if (args.Length() < 1 || !args[0]->IsNumber()) return; + int id = args[0]->Int32Value(isolate->GetCurrentContext()).FromJust(); + WebViewManager::instance().maximize(id); +} + +void runt_webview_restore(const v8::FunctionCallbackInfo& args) { + v8::Isolate* isolate = args.GetIsolate(); + v8::HandleScope scope(isolate); + if (args.Length() < 1 || !args[0]->IsNumber()) return; + int id = args[0]->Int32Value(isolate->GetCurrentContext()).FromJust(); + WebViewManager::instance().restore(id); +} + +void runt_webview_set_fullscreen(const v8::FunctionCallbackInfo& args) { + v8::Isolate* isolate = args.GetIsolate(); + v8::HandleScope scope(isolate); + if (args.Length() < 2 || !args[0]->IsNumber() || !args[1]->IsBoolean()) return; + int id = args[0]->Int32Value(isolate->GetCurrentContext()).FromJust(); + bool fs = args[1]->BooleanValue(isolate); + WebViewManager::instance().setFullscreen(id, fs); +} + +void runt_webview_is_fullscreen(const v8::FunctionCallbackInfo& args) { + v8::Isolate* isolate = args.GetIsolate(); + v8::HandleScope scope(isolate); + if (args.Length() < 1 || !args[0]->IsNumber()) { + args.GetReturnValue().Set(v8::Boolean::New(isolate, false)); + return; + } + int id = args[0]->Int32Value(isolate->GetCurrentContext()).FromJust(); + args.GetReturnValue().Set(v8::Boolean::New(isolate, WebViewManager::instance().isFullscreen(id))); +} + +void runt_webview_enable_devtools(const v8::FunctionCallbackInfo& args) { + v8::Isolate* isolate = args.GetIsolate(); + v8::HandleScope scope(isolate); + if (args.Length() < 2 || !args[0]->IsNumber() || !args[1]->IsBoolean()) return; + int id = args[0]->Int32Value(isolate->GetCurrentContext()).FromJust(); + bool enabled = args[1]->BooleanValue(isolate); + WebViewManager::instance().enableDevTools(id, enabled); +} + +void runt_webview_set_context_menu(const v8::FunctionCallbackInfo& args) { + v8::Isolate* isolate = args.GetIsolate(); + v8::HandleScope scope(isolate); + if (args.Length() < 2 || !args[0]->IsNumber() || !args[1]->IsBoolean()) return; + int id = args[0]->Int32Value(isolate->GetCurrentContext()).FromJust(); + bool enabled = args[1]->BooleanValue(isolate); + WebViewManager::instance().setContextMenuEnabled(id, enabled); +} + +#endif \ No newline at end of file diff --git a/Sources/webview.h b/Sources/webview.h new file mode 100644 index 0000000..fa7c46e --- /dev/null +++ b/Sources/webview.h @@ -0,0 +1,191 @@ +#pragma once + +#ifdef WITH_WEBVIEW + +#include +#include +#include +#include + +struct WebViewInstance { + int id; + int parentWindow; // -1 means detached as its own OS window, >= 0 = child of Kinc window + int x, y; + int width, height; // -1 means fill parent window for embedded mode only + bool autoResize; // true if width or height was -1 at creation + bool transparent; + bool clickThrough; + bool visible; + std::string title; + + void* platformHandle; + void* platformWindow; + + v8::Isolate* isolate; + v8::Global context; + v8::Global onMessage; + v8::Global onLoad; + v8::Global onError; + v8::Global onClose; + v8::Global onEvalResult; + + WebViewInstance() : id(-1), parentWindow(-1), x(0), y(0), + width(800), height(600), autoResize(false), transparent(true), + clickThrough(false), visible(false), platformHandle(nullptr), + platformWindow(nullptr), isolate(nullptr) {} +}; + +class WebViewManager { +public: + static WebViewManager& instance(); + + int create(v8::Isolate* isolate, v8::Local context, + int parentWindow, int x, int y, int width, int height, + bool transparent, const std::string& title); + void destroy(int id); + bool isValid(int id); + int count(); + + void loadHTML(int id, const std::string& html); + void loadURL(int id, const std::string& url); + void evalJS(int id, const std::string& js); + void evalJSAsync(int id, v8::Isolate* isolate, v8::Local callback, const std::string& js); + + void setActiveDOM(int id, v8::Isolate* isolate); + int getActiveDOM(); + + void show(int id); + void hide(int id); + + void resize(int id, int width, int height); + void move(int id, int x, int y); + void setBounds(int id, int x, int y, int width, int height); + int getX(int id); + int getY(int id); + int getWidth(int id); + int getHeight(int id); + + void setTransparent(int id, bool transparent); + void setClickThrough(int id, bool enabled); + void setTitle(int id, const std::string& title); + + void send(int id, const std::string& message); + + void goBack(int id); + void goForward(int id); + void reload(int id); + bool canGoBack(int id); + bool canGoForward(int id); + std::string getURL(int id); + std::string getPageTitle(int id); + + void minimize(int id); + void maximize(int id); + void restore(int id); + void setFullscreen(int id, bool fullscreen); + bool isFullscreen(int id); + + void enableDevTools(int id, bool enabled); + void setContextMenuEnabled(int id, bool enabled); + + void setOnMessage(int id, v8::Isolate* isolate, v8::Local cb); + void setOnLoad(int id, v8::Isolate* isolate, v8::Local cb); + void setOnError(int id, v8::Isolate* isolate, v8::Local cb); + void setOnClose(int id, v8::Isolate* isolate, v8::Local cb); + + void dispatchMessage(int id, const std::string& message); + void dispatchLoad(int id); + void dispatchError(int id, const std::string& error); + void dispatchClose(int id); + void dispatchEvalResult(int id, const std::string& result); + + void tick(); + + void onWindowResize(int windowId, int width, int height); + +private: + WebViewManager() = default; + ~WebViewManager(); + + std::map> webviews; + int nextId = 0; + int activeDomId = -1; +}; + +int platformCreate(WebViewInstance* wv); +void platformDestroy(WebViewInstance* wv); +void platformLoadHTML(WebViewInstance* wv, const std::string& html); +void platformLoadURL(WebViewInstance* wv, const std::string& url); +void platformEvalJS(WebViewInstance* wv, const std::string& js); +void platformEvalJSAsync(WebViewInstance* wv, const std::string& js); +void platformShow(WebViewInstance* wv); +void platformHide(WebViewInstance* wv); +void platformResize(WebViewInstance* wv, int width, int height); +void platformMove(WebViewInstance* wv, int x, int y); +void platformSetTransparent(WebViewInstance* wv, bool transparent); +void platformSetClickThrough(WebViewInstance* wv, bool enabled); +void platformSetTitle(WebViewInstance* wv, const std::string& title); +void platformSend(WebViewInstance* wv, const std::string& message); + +void platformGoBack(WebViewInstance* wv); +void platformGoForward(WebViewInstance* wv); +void platformReload(WebViewInstance* wv); +bool platformCanGoBack(WebViewInstance* wv); +bool platformCanGoForward(WebViewInstance* wv); +std::string platformGetURL(WebViewInstance* wv); +std::string platformGetTitle(WebViewInstance* wv); + +void platformMinimize(WebViewInstance* wv); +void platformMaximize(WebViewInstance* wv); +void platformRestore(WebViewInstance* wv); +void platformSetFullscreen(WebViewInstance* wv, bool fullscreen); +bool platformIsFullscreen(WebViewInstance* wv); + +void platformEnableDevTools(WebViewInstance* wv, bool enabled); +void platformSetContextMenuEnabled(WebViewInstance* wv, bool enabled); + +void platformTick(); + +void runt_webview_create(const v8::FunctionCallbackInfo& args); +void runt_webview_load_html(const v8::FunctionCallbackInfo& args); +void runt_webview_load_url(const v8::FunctionCallbackInfo& args); +void runt_webview_eval_js(const v8::FunctionCallbackInfo& args); +void runt_webview_show(const v8::FunctionCallbackInfo& args); +void runt_webview_hide(const v8::FunctionCallbackInfo& args); +void runt_webview_destroy(const v8::FunctionCallbackInfo& args); +void runt_webview_resize(const v8::FunctionCallbackInfo& args); +void runt_webview_move(const v8::FunctionCallbackInfo& args); +void runt_webview_set_bounds(const v8::FunctionCallbackInfo& args); +void runt_webview_get_x(const v8::FunctionCallbackInfo& args); +void runt_webview_get_y(const v8::FunctionCallbackInfo& args); +void runt_webview_get_width(const v8::FunctionCallbackInfo& args); +void runt_webview_get_height(const v8::FunctionCallbackInfo& args); +void runt_webview_set_transparent(const v8::FunctionCallbackInfo& args); +void runt_webview_set_click_through(const v8::FunctionCallbackInfo& args); +void runt_webview_set_title(const v8::FunctionCallbackInfo& args); +void runt_webview_send(const v8::FunctionCallbackInfo& args); +void runt_webview_set_on_message(const v8::FunctionCallbackInfo& args); +void runt_webview_set_on_load(const v8::FunctionCallbackInfo& args); +void runt_webview_set_on_error(const v8::FunctionCallbackInfo& args); +void runt_webview_set_on_close(const v8::FunctionCallbackInfo& args); +void runt_webview_count(const v8::FunctionCallbackInfo& args); +void runt_webview_is_valid(const v8::FunctionCallbackInfo& args); +void runt_webview_set_active_dom(const v8::FunctionCallbackInfo& args); +void runt_webview_get_active_dom(const v8::FunctionCallbackInfo& args); +void runt_webview_eval_js_async(const v8::FunctionCallbackInfo& args); +void runt_webview_go_back(const v8::FunctionCallbackInfo& args); +void runt_webview_go_forward(const v8::FunctionCallbackInfo& args); +void runt_webview_reload(const v8::FunctionCallbackInfo& args); +void runt_webview_can_go_back(const v8::FunctionCallbackInfo& args); +void runt_webview_can_go_forward(const v8::FunctionCallbackInfo& args); +void runt_webview_get_url(const v8::FunctionCallbackInfo& args); +void runt_webview_get_page_title(const v8::FunctionCallbackInfo& args); +void runt_webview_minimize(const v8::FunctionCallbackInfo& args); +void runt_webview_maximize(const v8::FunctionCallbackInfo& args); +void runt_webview_restore(const v8::FunctionCallbackInfo& args); +void runt_webview_set_fullscreen(const v8::FunctionCallbackInfo& args); +void runt_webview_is_fullscreen(const v8::FunctionCallbackInfo& args); +void runt_webview_enable_devtools(const v8::FunctionCallbackInfo& args); +void runt_webview_set_context_menu(const v8::FunctionCallbackInfo& args); + +#endif diff --git a/Sources/webview_linux.cpp b/Sources/webview_linux.cpp new file mode 100644 index 0000000..b832dac --- /dev/null +++ b/Sources/webview_linux.cpp @@ -0,0 +1,362 @@ +#ifdef WITH_WEBVIEW +#ifdef KINC_LINUX + +#include "webview.h" +#include +#include + +#include +#include + +struct LinuxWebViewState { + WebKitWebView* webView; + GtkWidget* detachedWindow; + bool isDetached; + gulong closeHandlerId; + gulong messageHandlerId; + gulong loadHandlerId; + int webviewId; +}; + +static bool g_gtkInitialized = false; + +static void ensureGtkInit() { + if (g_gtkInitialized) return; + if (!gtk_init_check(nullptr, nullptr)) { + kinc_log(KINC_LOG_LEVEL_ERROR, "Failed to initialize webview for GTK"); + return; + } + g_gtkInitialized = true; +} + +static void onMessageReceived(WebKitUserContentManager* manager, + WebKitJavascriptResult* result, gpointer userData) { + auto* state = (LinuxWebViewState*)userData; + JSCValue* value = webkit_javascript_result_get_js_value(result); + if (jsc_value_is_string(value)) { + char* str = jsc_value_to_string(value); + WebViewManager::instance().dispatchMessage(state->webviewId, std::string(str)); + g_free(str); + } else { + char* str = jsc_value_to_string(value); + WebViewManager::instance().dispatchMessage(state->webviewId, std::string(str)); + g_free(str); + } +} + +static void onLoadChanged(WebKitWebView* webView, WebKitLoadEvent loadEvent, + gpointer userData) { + auto* state = (LinuxWebViewState*)userData; + if (loadEvent == WEBKIT_LOAD_FINISHED) { + WebViewManager::instance().dispatchLoad(state->webviewId); + } +} + +static gboolean onDetachedClose(GtkWidget* widget, GdkEvent* event, gpointer userData) { + auto* state = (LinuxWebViewState*)userData; + WebViewManager::instance().dispatchClose(state->webviewId); + gtk_widget_hide(widget); + return TRUE; +} + +int platformCreate(WebViewInstance* wv) { + ensureGtkInit(); + if (!g_gtkInitialized) { + kinc_log(KINC_LOG_LEVEL_ERROR, "GTK WebView not initialized"); + return -1; + } + + auto* state = new LinuxWebViewState(); + state->webView = nullptr; + state->detachedWindow = nullptr; + state->isDetached = (wv->parentWindow < 0); + state->webviewId = wv->id; + wv->platformHandle = state; + + state->webView = WEBKIT_WEB_VIEW(webkit_web_view_new()); + + if (wv->transparent) { + GdkRGBA rgba = {0.0, 0.0, 0.0, 0.0}; + webkit_web_view_set_background_color(state->webView, &rgba); + } + + WebKitUserContentManager* contentManager = + webkit_web_view_get_user_content_manager(state->webView); + webkit_user_content_manager_register_script_message_handler(contentManager, "lnxrnt"); + state->messageHandlerId = g_signal_connect(contentManager, "script-message-received::lnxrnt", + G_CALLBACK(onMessageReceived), state); + + state->loadHandlerId = g_signal_connect(state->webView, "load-changed", + G_CALLBACK(onLoadChanged), state); + + if (state->isDetached) { + state->detachedWindow = gtk_window_new(GTK_WINDOW_TOPLEVEL); + gtk_window_set_title(GTK_WINDOW(state->detachedWindow), wv->title.c_str()); + gtk_window_set_default_size(GTK_WINDOW(state->detachedWindow), wv->width, wv->height); + gtk_window_move(GTK_WINDOW(state->detachedWindow), wv->x, wv->y); + + GtkWidget* scrolledWindow = gtk_scrolled_window_new(nullptr, nullptr); + gtk_container_add(GTK_CONTAINER(scrolledWindow), GTK_WIDGET(state->webView)); + gtk_container_add(GTK_CONTAINER(state->detachedWindow), scrolledWindow); + + state->closeHandlerId = g_signal_connect(state->detachedWindow, "delete-event", + G_CALLBACK(onDetachedClose), state); + } else { + state->detachedWindow = gtk_window_new(GTK_WINDOW_POPUP); + gtk_window_set_decorated(GTK_WINDOW(state->detachedWindow), FALSE); + gtk_window_set_default_size(GTK_WINDOW(state->detachedWindow), wv->width, wv->height); + + gtk_window_move(GTK_WINDOW(state->detachedWindow), wv->x, wv->y); + + GtkWidget* scrolledWindow = gtk_scrolled_window_new(nullptr, nullptr); + gtk_container_add(GTK_CONTAINER(scrolledWindow), GTK_WIDGET(state->webView)); + gtk_container_add(GTK_CONTAINER(state->detachedWindow), scrolledWindow); + } + + return wv->id; +} + +void platformDestroy(WebViewInstance* wv) { + auto* state = (LinuxWebViewState*)wv->platformHandle; + if (!state) return; + + if (state->messageHandlerId) { + WebKitUserContentManager* cm = webkit_web_view_get_user_content_manager(state->webView); + g_signal_handler_disconnect(cm, state->messageHandlerId); + } + if (state->loadHandlerId) { + g_signal_handler_disconnect(state->webView, state->loadHandlerId); + } + if (state->closeHandlerId && state->detachedWindow) { + g_signal_handler_disconnect(state->detachedWindow, state->closeHandlerId); + } + if (state->detachedWindow) { + gtk_widget_destroy(state->detachedWindow); + } + delete state; + wv->platformHandle = nullptr; +} + +void platformLoadHTML(WebViewInstance* wv, const std::string& html) { + auto* state = (LinuxWebViewState*)wv->platformHandle; + if (!state || !state->webView) return; + webkit_web_view_load_html(state->webView, html.c_str(), nullptr); +} + +void platformLoadURL(WebViewInstance* wv, const std::string& url) { + auto* state = (LinuxWebViewState*)wv->platformHandle; + if (!state || !state->webView) return; + webkit_web_view_load_uri(state->webView, url.c_str()); +} + +void platformEvalJS(WebViewInstance* wv, const std::string& js) { + auto* state = (LinuxWebViewState*)wv->platformHandle; + if (!state || !state->webView) return; + webkit_web_view_evaluate_javascript(state->webView, js.c_str(), -1, nullptr, nullptr, nullptr, nullptr, nullptr); +} + +static void platformEvalJSAsyncCallback(GObject* source, GAsyncResult* res, gpointer user_data) { + int webviewId = (int)(intptr_t)user_data; + WebKitJavascriptResult* jsResult = webkit_web_view_evaluate_javascript_finish(WEBKIT_WEB_VIEW(source), res, nullptr); + std::string resultStr = "null"; + if (jsResult) { + JSCValue* value = webkit_javascript_result_get_js_value(jsResult); + if (value && !jsc_value_is_undefined(value) && !jsc_value_is_null(value)) { + JSCValue* jsonStr = jsc_value_to_string(value); + gchar* str = (gchar*)jsc_value_to_string(jsonStr); + if (str) { + resultStr = std::string(str); + g_free(str); + } + g_object_unref(jsonStr); + } + webkit_javascript_result_unref(jsResult); + } + WebViewManager::instance().dispatchEvalResult(webviewId, resultStr); +} + +void platformEvalJSAsync(WebViewInstance* wv, const std::string& js) { + auto* state = (LinuxWebViewState*)wv->platformHandle; + if (!state || !state->webView) return; + webkit_web_view_evaluate_javascript(state->webView, js.c_str(), -1, nullptr, nullptr, + (GAsyncReadyCallback)platformEvalJSAsyncCallback, (gpointer)(intptr_t)wv->id); +} + +void platformShow(WebViewInstance* wv) { + auto* state = (LinuxWebViewState*)wv->platformHandle; + if (!state) return; + if (state->detachedWindow) { + gtk_widget_show_all(state->detachedWindow); + } +} + +void platformHide(WebViewInstance* wv) { + auto* state = (LinuxWebViewState*)wv->platformHandle; + if (!state) return; + if (state->detachedWindow) { + gtk_widget_hide(state->detachedWindow); + } +} + +void platformResize(WebViewInstance* wv, int width, int height) { + auto* state = (LinuxWebViewState*)wv->platformHandle; + if (!state || !state->detachedWindow) return; + gtk_window_resize(GTK_WINDOW(state->detachedWindow), width, height); +} + +void platformMove(WebViewInstance* wv, int x, int y) { + auto* state = (LinuxWebViewState*)wv->platformHandle; + if (!state || !state->detachedWindow) return; + gtk_window_move(GTK_WINDOW(state->detachedWindow), x, y); +} + +void platformSetTransparent(WebViewInstance* wv, bool transparent) { + auto* state = (LinuxWebViewState*)wv->platformHandle; + if (!state || !state->webView) return; + if (transparent) { + GdkRGBA rgba = {0.0, 0.0, 0.0, 0.0}; + webkit_web_view_set_background_color(state->webView, &rgba); + } else { + GdkRGBA rgba = {1.0, 1.0, 1.0, 1.0}; + webkit_web_view_set_background_color(state->webView, &rgba); + } +} + +void platformSetClickThrough(WebViewInstance* wv, bool enabled) { + auto* state = (LinuxWebViewState*)wv->platformHandle; + if (!state || !state->webView) return; + const char* js = enabled + ? "document.body.style.pointerEvents = 'none';" + : "document.body.style.pointerEvents = 'auto';"; + webkit_web_view_evaluate_javascript(state->webView, js, -1, nullptr, nullptr, nullptr, nullptr, nullptr); +} + +void platformSetTitle(WebViewInstance* wv, const std::string& title) { + auto* state = (LinuxWebViewState*)wv->platformHandle; + if (!state || !state->isDetached || !state->detachedWindow) return; + gtk_window_set_title(GTK_WINDOW(state->detachedWindow), title.c_str()); +} + +void platformSend(WebViewInstance* wv, const std::string& message) { + auto* state = (LinuxWebViewState*)wv->platformHandle; + if (!state || !state->webView) return; + std::string escaped; + for (size_t i = 0; i < message.size(); i++) { + char c = message[i]; + switch (c) { + case '\\': escaped += "\\\\"; break; + case '\'': escaped += "\\'"; break; + case '\n': escaped += "\\n"; break; + case '\r': escaped += "\\r"; break; + default: escaped += c; break; + } + } + std::string js = "if(window.__lnxrntOnMessage)window.__lnxrntOnMessage('" + escaped + "');"; + webkit_web_view_evaluate_javascript(state->webView, js.c_str(), -1, nullptr, nullptr, nullptr, nullptr, nullptr); +} + +void platformGoBack(WebViewInstance* wv) { + auto* state = (LinuxWebViewState*)wv->platformHandle; + if (!state || !state->webView) return; + webkit_web_view_go_back(state->webView); +} + +void platformGoForward(WebViewInstance* wv) { + auto* state = (LinuxWebViewState*)wv->platformHandle; + if (!state || !state->webView) return; + webkit_web_view_go_forward(state->webView); +} + +void platformReload(WebViewInstance* wv) { + auto* state = (LinuxWebViewState*)wv->platformHandle; + if (!state || !state->webView) return; + webkit_web_view_reload(state->webView); +} + +bool platformCanGoBack(WebViewInstance* wv) { + auto* state = (LinuxWebViewState*)wv->platformHandle; + if (!state || !state->webView) return false; + return webkit_web_view_can_go_back(state->webView); +} + +bool platformCanGoForward(WebViewInstance* wv) { + auto* state = (LinuxWebViewState*)wv->platformHandle; + if (!state || !state->webView) return false; + return webkit_web_view_can_go_forward(state->webView); +} + +std::string platformGetURL(WebViewInstance* wv) { + auto* state = (LinuxWebViewState*)wv->platformHandle; + if (!state || !state->webView) return ""; + const gchar* uri = webkit_web_view_get_uri(state->webView); + return uri ? std::string(uri) : ""; +} + +std::string platformGetTitle(WebViewInstance* wv) { + auto* state = (LinuxWebViewState*)wv->platformHandle; + if (!state || !state->webView) return ""; + const gchar* title = webkit_web_view_get_title(state->webView); + return title ? std::string(title) : ""; +} + +void platformMinimize(WebViewInstance* wv) { + auto* state = (LinuxWebViewState*)wv->platformHandle; + if (!state || !state->isDetached || !state->detachedWindow) return; + gtk_window_iconify(GTK_WINDOW(state->detachedWindow)); +} + +void platformMaximize(WebViewInstance* wv) { + auto* state = (LinuxWebViewState*)wv->platformHandle; + if (!state || !state->isDetached || !state->detachedWindow) return; + gtk_window_maximize(GTK_WINDOW(state->detachedWindow)); +} + +void platformRestore(WebViewInstance* wv) { + auto* state = (LinuxWebViewState*)wv->platformHandle; + if (!state || !state->isDetached || !state->detachedWindow) return; + gtk_window_unmaximize(GTK_WINDOW(state->detachedWindow)); + gtk_window_deiconify(GTK_WINDOW(state->detachedWindow)); +} + +void platformSetFullscreen(WebViewInstance* wv, bool fullscreen) { + auto* state = (LinuxWebViewState*)wv->platformHandle; + if (!state || !state->isDetached || !state->detachedWindow) return; + if (fullscreen) { + gtk_window_fullscreen(GTK_WINDOW(state->detachedWindow)); + } else { + gtk_window_unfullscreen(GTK_WINDOW(state->detachedWindow)); + } +} + +bool platformIsFullscreen(WebViewInstance* wv) { + auto* state = (LinuxWebViewState*)wv->platformHandle; + if (!state || !state->isDetached || !state->detachedWindow) return false; + GdkWindow* gdkWin = gtk_widget_get_window(state->detachedWindow); + if (!gdkWin) return false; + GdkWindowState state2 = gdk_window_get_state(gdkWin); + return (state2 & GDK_WINDOW_STATE_FULLSCREEN) != 0; +} + +void platformEnableDevTools(WebViewInstance* wv, bool enabled) { + auto* state = (LinuxWebViewState*)wv->platformHandle; + if (!state || !state->webView) return; + WebKitSettings* settings = webkit_web_view_get_settings(state->webView); + webkit_settings_set_enable_developer_extras(settings, enabled); +} + +void platformSetContextMenuEnabled(WebViewInstance* wv, bool enabled) { + auto* state = (LinuxWebViewState*)wv->platformHandle; + if (!state || !state->webView) return; + WebKitSettings* settings = webkit_web_view_get_settings(state->webView); + webkit_settings_set_enable_context_menu(settings, enabled); +} + +void platformTick() { + if (!g_gtkInitialized) return; + while (gtk_events_pending()) { + gtk_main_iteration_do(FALSE); + } +} + +#endif +#endif diff --git a/Sources/webview_mac.mm b/Sources/webview_mac.mm new file mode 100644 index 0000000..1ccb173 --- /dev/null +++ b/Sources/webview_mac.mm @@ -0,0 +1,468 @@ +#ifdef WITH_WEBVIEW + +#include + +#ifdef KINC_MACOS + +#import +#import +#include "webview.h" +#include +#include + +@interface LNXRNTWebViewDelegate : NSObject { + @public int webviewId; + @public bool isDetached; + @public NSWindow* detachedWindow; +} +- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message; +- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation; +- (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error; +- (BOOL)windowShouldClose:(NSWindow *)sender; +- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler; +- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler; +- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString *result))completionHandler; +- (WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures; +- (void)webView:(WKWebView *)webView runOpenPanelWithParameters:(WKOpenPanelParameters *)parameters initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSArray *URLs))completionHandler; +@end + +@implementation LNXRNTWebViewDelegate + +- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message { + NSString *msgBody = [[message body] isKindOfClass:[NSString class]] ? [message body] : [NSString stringWithFormat:@"%@", [message body]]; + const char *cstr = [msgBody UTF8String]; + WebViewManager::instance().dispatchMessage(webviewId, std::string(cstr)); +} + +- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { + WebViewManager::instance().dispatchLoad(webviewId); +} + +- (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error { + NSString *desc = [error localizedDescription]; + WebViewManager::instance().dispatchError(webviewId, std::string([desc UTF8String])); +} + +- (BOOL)windowShouldClose:(NSWindow *)sender { + WebViewManager::instance().dispatchClose(webviewId); + [sender orderOut:nil]; + return NO; +} + +- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler { + NSAlert *alert = [[NSAlert alloc] init]; + [alert setMessageText:message]; + [alert addButtonWithTitle:@"OK"]; + [alert runModal]; + completionHandler(); +} + +- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler { + NSAlert *alert = [[NSAlert alloc] init]; + [alert setMessageText:message]; + [alert addButtonWithTitle:@"OK"]; + [alert addButtonWithTitle:@"Cancel"]; + NSModalResponse result = [alert runModal]; + completionHandler(result == NSAlertFirstButtonReturn); +} + +- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString *result))completionHandler { + NSAlert *alert = [[NSAlert alloc] init]; + [alert setMessageText:prompt]; + [alert addButtonWithTitle:@"OK"]; + [alert addButtonWithTitle:@"Cancel"]; + NSTextField *input = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 250, 24)]; + [input setStringValue:defaultText ? defaultText : @""]; + [alert setAccessoryView:input]; + NSModalResponse result = [alert runModal]; + if (result == NSAlertFirstButtonReturn) { + completionHandler([input stringValue]); + } else { + completionHandler(nil); + } +} + +- (WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures { + if (!navigationAction.targetFrame.isMainFrame) { + return nil; + } + NSRect frame = NSMakeRect(100, 100, 800, 600); + NSUInteger styleMask = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | + NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable; + NSWindow *newWindow = [[NSWindow alloc] + initWithContentRect:frame + styleMask:styleMask + backing:NSBackingStoreBuffered + defer:YES]; + WKWebView *newWebView = [[WKWebView alloc] initWithFrame:frame configuration:configuration]; + newWebView.navigationDelegate = self; + newWebView.UIDelegate = self; + [newWindow setContentView:newWebView]; + [newWindow makeKeyAndOrderFront:nil]; + return newWebView; +} + +- (void)webView:(WKWebView *)webView runOpenPanelWithParameters:(WKOpenPanelParameters *)parameters initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSArray *URLs))completionHandler { + NSOpenPanel *openPanel = [NSOpenPanel openPanel]; + openPanel.allowsMultipleSelection = parameters.allowsMultipleSelection; + openPanel.canChooseDirectories = parameters.allowsDirectories; + if ([openPanel runModal] == NSModalResponseOK) { + completionHandler(openPanel.URLs); + } else { + completionHandler(nil); + } +} + +@end + +struct MacWebViewState { + WKWebView* webView; + LNXRNTWebViewDelegate* delegate; + NSWindow* detachedWindow; + bool isDetached; +}; + +extern "C" NSWindow* kinc_get_mac_window_handle(int window_index); + +int platformCreate(WebViewInstance* wv) { + auto* state = new MacWebViewState(); + state->webView = nullptr; + state->delegate = nullptr; + state->detachedWindow = nullptr; + state->isDetached = (wv->parentWindow < 0); + wv->platformHandle = state; + + WKWebViewConfiguration* config = [[WKWebViewConfiguration alloc] init]; + + WKUserContentController* contentController = [[WKUserContentController alloc] init]; + state->delegate = [[LNXRNTWebViewDelegate alloc] init]; + state->delegate->webviewId = wv->id; + state->delegate->isDetached = state->isDetached; + [contentController addScriptMessageHandler:state->delegate name:@"lnxrnt"]; + config.userContentController = contentController; + + CGFloat scale = 1.0; + if (!state->isDetached) { + NSWindow* kincWindow = kinc_get_mac_window_handle(wv->parentWindow); + if (kincWindow) { + scale = [kincWindow backingScaleFactor]; + } + } + CGFloat ptWidth = wv->width / scale; + CGFloat ptHeight = wv->height / scale; + NSRect frame = NSMakeRect(wv->x, wv->y, ptWidth, ptHeight); + state->webView = [[WKWebView alloc] initWithFrame:frame configuration:config]; + state->webView.navigationDelegate = state->delegate; + state->webView.UIDelegate = state->delegate; + + if (wv->transparent) { + [state->webView setValue:@NO forKey:@"opaque"]; + [state->webView setValue:@NO forKey:@"drawsBackground"]; + } + + if (state->isDetached) { + NSUInteger styleMask = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | + NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable; + state->detachedWindow = [[NSWindow alloc] + initWithContentRect:frame + styleMask:styleMask + backing:NSBackingStoreBuffered + defer:YES]; + [state->detachedWindow setTitle:[NSString stringWithUTF8String:wv->title.c_str()]]; + [state->detachedWindow setContentView:state->webView]; + [state->detachedWindow setDelegate:state->delegate]; + state->delegate->detachedWindow = state->detachedWindow; + } else { + NSWindow* kincWindow = kinc_get_mac_window_handle(wv->parentWindow); + if (!kincWindow) { + kinc_log(KINC_LOG_LEVEL_ERROR, "Failed to get Kinc NSWindow for webview window %d", wv->parentWindow); + [state->webView release]; + [contentController release]; + [state->delegate release]; + delete state; + wv->platformHandle = nullptr; + return -1; + } + + NSView* contentView = [kincWindow contentView]; + CGFloat flippedY = [contentView frame].size.height - wv->y - ptHeight; + [state->webView setFrameOrigin:NSMakePoint(wv->x, flippedY)]; + [contentView addSubview:state->webView positioned:NSWindowAbove relativeTo:nil]; + } + + return wv->id; +} + +void platformDestroy(WebViewInstance* wv) { + auto* state = (MacWebViewState*)wv->platformHandle; + if (!state) return; + + if (state->webView) { + [state->webView removeFromSuperview]; + [state->webView release]; + } + if (state->detachedWindow) { + [state->detachedWindow close]; + } + if (state->delegate) { + [state->delegate release]; + } + delete state; + wv->platformHandle = nullptr; +} + +void platformLoadHTML(WebViewInstance* wv, const std::string& html) { + auto* state = (MacWebViewState*)wv->platformHandle; + if (!state || !state->webView) return; + NSString* nsHtml = [NSString stringWithUTF8String:html.c_str()]; + [state->webView loadHTMLString:nsHtml baseURL:nil]; +} + +void platformLoadURL(WebViewInstance* wv, const std::string& url) { + auto* state = (MacWebViewState*)wv->platformHandle; + if (!state || !state->webView) return; + NSString* nsUrl = [NSString stringWithUTF8String:url.c_str()]; + NSURL* nsURL = [NSURL URLWithString:nsUrl]; + [state->webView loadRequest:[NSURLRequest requestWithURL:nsURL]]; +} + +void platformEvalJS(WebViewInstance* wv, const std::string& js) { + auto* state = (MacWebViewState*)wv->platformHandle; + if (!state || !state->webView) return; + NSString* nsJs = [NSString stringWithUTF8String:js.c_str()]; + [state->webView evaluateJavaScript:nsJs completionHandler:nil]; +} + +void platformEvalJSAsync(WebViewInstance* wv, const std::string& js) { + auto* state = (MacWebViewState*)wv->platformHandle; + if (!state || !state->webView) return; + NSString* nsJs = [NSString stringWithUTF8String:js.c_str()]; + int webviewId = wv->id; + [state->webView evaluateJavaScript:nsJs completionHandler:^(id result, NSError* error) { + std::string resultStr; + if (error) { + resultStr = "null"; + } else if (result == nil) { + resultStr = "null"; + } else if ([result isKindOfClass:[NSString class]]) { + resultStr = std::string([[result description] UTF8String]); + } else if ([result isKindOfClass:[NSNumber class]]) { + resultStr = std::string([[result description] UTF8String]); + } else { + NSError* jsonErr = nil; + NSData* jsonData = [NSJSONSerialization dataWithJSONObject:result options:0 error:&jsonErr]; + if (jsonData && !jsonErr) { + NSString* jsonStr = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + resultStr = std::string([jsonStr UTF8String]); + } else { + resultStr = std::string([[result description] UTF8String]); + } + } + WebViewManager::instance().dispatchEvalResult(webviewId, resultStr); + }]; +} + +void platformShow(WebViewInstance* wv) { + auto* state = (MacWebViewState*)wv->platformHandle; + if (!state) return; + if (state->isDetached && state->detachedWindow) { + [state->detachedWindow makeKeyAndOrderFront:nil]; + } else if (state->webView) { + [state->webView setHidden:NO]; + } +} + +void platformHide(WebViewInstance* wv) { + auto* state = (MacWebViewState*)wv->platformHandle; + if (!state) return; + if (state->isDetached && state->detachedWindow) { + [state->detachedWindow orderOut:nil]; + } else if (state->webView) { + [state->webView setHidden:YES]; + } +} + +void platformResize(WebViewInstance* wv, int width, int height) { + auto* state = (MacWebViewState*)wv->platformHandle; + if (!state || !state->webView) return; + + if (state->isDetached && state->detachedWindow) { + NSRect frame = NSMakeRect(wv->x, wv->y, width, height); + [state->detachedWindow setFrame:frame display:YES]; + } else { + NSView* superview = [state->webView superview]; + CGFloat scale = [[superview window] backingScaleFactor]; + if (scale == 0) scale = 1.0; + CGFloat ptW = width / scale; + CGFloat ptH = height / scale; + if (superview) { + CGFloat flippedY = [superview frame].size.height - wv->y - ptH; + [state->webView setFrame:NSMakeRect(wv->x, flippedY, ptW, ptH)]; + } else { + [state->webView setFrameSize:NSMakeSize(ptW, ptH)]; + } + } +} + +void platformMove(WebViewInstance* wv, int x, int y) { + auto* state = (MacWebViewState*)wv->platformHandle; + if (!state || !state->webView) return; + + if (state->isDetached && state->detachedWindow) { + [state->detachedWindow setFrameOrigin:NSMakePoint(x, y)]; + } else { + NSView* superview = [state->webView superview]; + if (superview) { + CGFloat scale = [[superview window] backingScaleFactor]; + if (scale == 0) scale = 1.0; + CGFloat ptH = wv->height / scale; + CGFloat flippedY = [superview frame].size.height - y - ptH; + [state->webView setFrameOrigin:NSMakePoint(x, flippedY)]; + } + } +} + +void platformSetTransparent(WebViewInstance* wv, bool transparent) { + auto* state = (MacWebViewState*)wv->platformHandle; + if (!state || !state->webView) return; + [state->webView setValue:(transparent ? @NO : @YES) forKey:@"opaque"]; + [state->webView setValue:(transparent ? @NO : @YES) forKey:@"drawsBackground"]; +} + +void platformSetClickThrough(WebViewInstance* wv, bool enabled) { + auto* state = (MacWebViewState*)wv->platformHandle; + if (!state || !state->webView) return; + if (enabled) { + [state->webView setAcceptsTouchEvents:NO]; + [state->webView evaluateJavaScript: + @"document.body.style.pointerEvents = 'none';" + completionHandler:nil]; + } else { + [state->webView setAcceptsTouchEvents:YES]; + [state->webView evaluateJavaScript: + @"document.body.style.pointerEvents = 'auto';" + completionHandler:nil]; + } +} + +void platformSetTitle(WebViewInstance* wv, const std::string& title) { + auto* state = (MacWebViewState*)wv->platformHandle; + if (!state || !state->isDetached || !state->detachedWindow) return; + [state->detachedWindow setTitle:[NSString stringWithUTF8String:title.c_str()]]; +} + +void platformSend(WebViewInstance* wv, const std::string& message) { + auto* state = (MacWebViewState*)wv->platformHandle; + if (!state || !state->webView) return; + NSString* escaped = [[NSString stringWithUTF8String:message.c_str()] + stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"]; + escaped = [escaped stringByReplacingOccurrencesOfString:@"'" withString:@"\\'"]; + escaped = [escaped stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"]; + escaped = [escaped stringByReplacingOccurrencesOfString:@"\r" withString:@"\\r"]; + NSString* js = [NSString stringWithFormat: + @"if(window.__lnxrntOnMessage)window.__lnxrntOnMessage('%@');", escaped]; + [state->webView evaluateJavaScript:js completionHandler:nil]; +} + +void platformGoBack(WebViewInstance* wv) { + auto* state = (MacWebViewState*)wv->platformHandle; + if (!state || !state->webView) return; + if ([state->webView canGoBack]) [state->webView goBack]; +} + +void platformGoForward(WebViewInstance* wv) { + auto* state = (MacWebViewState*)wv->platformHandle; + if (!state || !state->webView) return; + if ([state->webView canGoForward]) [state->webView goForward]; +} + +void platformReload(WebViewInstance* wv) { + auto* state = (MacWebViewState*)wv->platformHandle; + if (!state || !state->webView) return; + [state->webView reload]; +} + +bool platformCanGoBack(WebViewInstance* wv) { + auto* state = (MacWebViewState*)wv->platformHandle; + if (!state || !state->webView) return false; + return [state->webView canGoBack]; +} + +bool platformCanGoForward(WebViewInstance* wv) { + auto* state = (MacWebViewState*)wv->platformHandle; + if (!state || !state->webView) return false; + return [state->webView canGoForward]; +} + +std::string platformGetURL(WebViewInstance* wv) { + auto* state = (MacWebViewState*)wv->platformHandle; + if (!state || !state->webView) return ""; + NSString* url = [state->webView.URL absoluteString]; + return url ? std::string([url UTF8String]) : ""; +} + +std::string platformGetTitle(WebViewInstance* wv) { + auto* state = (MacWebViewState*)wv->platformHandle; + if (!state || !state->webView) return ""; + NSString* title = [state->webView title]; + return title ? std::string([title UTF8String]) : ""; +} + +void platformMinimize(WebViewInstance* wv) { + auto* state = (MacWebViewState*)wv->platformHandle; + if (!state || !state->isDetached || !state->detachedWindow) return; + [state->detachedWindow miniaturize:nil]; +} + +void platformMaximize(WebViewInstance* wv) { + auto* state = (MacWebViewState*)wv->platformHandle; + if (!state || !state->isDetached || !state->detachedWindow) return; + [state->detachedWindow zoom:nil]; +} + +void platformRestore(WebViewInstance* wv) { + auto* state = (MacWebViewState*)wv->platformHandle; + if (!state || !state->isDetached || !state->detachedWindow) return; + if ([state->detachedWindow isMiniaturized]) [state->detachedWindow deminiaturize:nil]; + if ([state->detachedWindow isZoomed]) [state->detachedWindow zoom:nil]; +} + +void platformSetFullscreen(WebViewInstance* wv, bool fullscreen) { + auto* state = (MacWebViewState*)wv->platformHandle; + if (!state || !state->isDetached || !state->detachedWindow) return; + if (fullscreen) { + [state->detachedWindow toggleFullScreen:nil]; + } else { + if (([state->detachedWindow styleMask] & NSWindowStyleMaskFullScreen) != 0) { + [state->detachedWindow toggleFullScreen:nil]; + } + } +} + +bool platformIsFullscreen(WebViewInstance* wv) { + auto* state = (MacWebViewState*)wv->platformHandle; + if (!state || !state->isDetached || !state->detachedWindow) return false; + return ([state->detachedWindow styleMask] & NSWindowStyleMaskFullScreen) != 0; +} + +void platformEnableDevTools(WebViewInstance* wv, bool enabled) { + auto* state = (MacWebViewState*)wv->platformHandle; + if (!state || !state->webView) return; + [state->webView.configuration.preferences setValue:@(enabled) forKey:@"developerExtrasEnabled"]; +} + +void platformSetContextMenuEnabled(WebViewInstance* wv, bool enabled) { + auto* state = (MacWebViewState*)wv->platformHandle; + if (!state || !state->webView) return; + if (!enabled) { + NSString* js = @"document.addEventListener('contextmenu', function(e){e.preventDefault();}, true);"; + [state->webView evaluateJavaScript:js completionHandler:nil]; + } +} + +void platformTick() { + // WKWebView callbacks are in NSRunLoop already integrated with Kinc loop. +} + +#endif +#endif diff --git a/Sources/webview_win.cpp b/Sources/webview_win.cpp new file mode 100644 index 0000000..ac4138c --- /dev/null +++ b/Sources/webview_win.cpp @@ -0,0 +1,595 @@ +#ifdef WITH_WEBVIEW +#ifdef KINC_WINDOWS + +#include "webview.h" +#include +#include +#include +#include +#include +#include +#include + +#include "WebView2.h" +#include "WebView2EnvironmentCreator.h" + +#pragma comment(lib, "WebView2Loader.lib") + +struct WinWebViewState { + ICoreWebView2Controller* controller; + ICoreWebView2* webview; + HWND parentHwnd; + HWND detachedHwnd; + bool isDetached; + bool destroyed; + bool pending; + EventRegistrationToken messageToken; + EventRegistrationToken navCompletedToken; + EventRegistrationToken errorToken; + int webviewId; + int x, y, width, height; + bool transparent; + std::wstring pendingHTML; + std::wstring pendingURL; +}; + +static ICoreWebView2Environment* g_webview2Env = nullptr; +static bool g_envCreating = false; +static std::vector g_pendingCreates; + +static std::wstring utf8ToWide(const std::string& str) { + if (str.empty()) return std::wstring(); + int len = MultiByteToWideChar(CP_UTF8, 0, str.c_str(), (int)str.length(), nullptr, 0); + std::wstring wstr(len, 0); + MultiByteToWideChar(CP_UTF8, 0, str.c_str(), (int)str.length(), &wstr[0], len); + return wstr; +} + +static void createController(WinWebViewState* state); + +static HRESULT STDMETHODCALLTYPE onEnvCreated(HRESULT result, ICoreWebView2Environment* env) { + if (FAILED(result) || !env) { + kinc_log(KINC_LOG_LEVEL_ERROR, "[WebView] Failed to create WebView2 environment: 0x%08X", result); + g_envCreating = false; + return result; + } + g_webview2Env = env; + g_webview2Env->AddRef(); + g_envCreating = false; + + auto pending = std::move(g_pendingCreates); + for (auto* wv : pending) { + auto* state = (WinWebViewState*)wv->platformHandle; + if (!state || state->destroyed) continue; + state->pending = false; + createController(state); + } + + return S_OK; +} + +static void ensureWebView2Environment() { + if (g_webview2Env || g_envCreating) return; + g_envCreating = true; + + HRESULT hr = CreateCoreWebView2EnvironmentWithOptions( + nullptr, nullptr, nullptr, + Callback( + onEnvCreated).Get()); + + if (FAILED(hr)) { + kinc_log(KINC_LOG_LEVEL_ERROR, "[WebView] CreateCoreWebView2EnvironmentWithOptions failed: 0x%08X", hr); + g_envCreating = false; + } +} + +static HRESULT STDMETHODCALLTYPE onMessageReceived( + ICoreWebView2* sender, ICoreWebView2WebMessageReceivedEventArgs* args, DWORD* webviewIdPtr) { + + LPWSTR message = nullptr; + args->TryGetWebMessageAsString(&message); + if (message) { + int len = WideCharToMultiByte(CP_UTF8, 0, message, -1, nullptr, 0, nullptr, nullptr); + std::string utf8Message(len - 1, 0); + WideCharToMultiByte(CP_UTF8, 0, message, -1, &utf8Message[0], len, nullptr, nullptr); + + int webviewId = (int)(intptr_t)webviewIdPtr; + WebViewManager::instance().dispatchMessage(webviewId, utf8Message); + + CoTaskMemFree(message); + } + return S_OK; +} + +static HRESULT STDMETHODCALLTYPE onNavCompleted( + ICoreWebView2* sender, ICoreWebView2NavigationCompletedEventArgs* args, DWORD* webviewIdPtr) { + int webviewId = (int)(intptr_t)webviewIdPtr; + WebViewManager::instance().dispatchLoad(webviewId); + return S_OK; +} + +static HRESULT STDMETHODCALLTYPE onProcessFailed( + ICoreWebView2* sender, ICoreWebView2ProcessFailedEventArgs* args, DWORD* webviewIdPtr) { + int webviewId = (int)(intptr_t)webviewIdPtr; + WebViewManager::instance().dispatchError(webviewId, "WebView2 process failed"); + return S_OK; +} + +static void createController(WinWebViewState* state) { + g_webview2Env->CreateCoreWebView2Controller( + state->parentHwnd, + Callback( + [state](HRESULT result, ICoreWebView2Controller* controller) -> HRESULT { + if (state->destroyed) { + if (controller) controller->Release(); + delete state; + return S_OK; + } + if (FAILED(result) || !controller) { + kinc_log(KINC_LOG_LEVEL_ERROR, "CreateController for webview failed: 0x%08X", result); + return result; + } + + state->controller = controller; + controller->get_CoreWebView2(&state->webview); + + RECT bounds; + if (state->isDetached) { + GetClientRect(state->detachedHwnd, &bounds); + } else { + bounds.left = state->x; + bounds.top = state->y; + bounds.right = state->x + state->width; + bounds.bottom = state->y + state->height; + } + controller->put_Bounds(bounds); + + if (state->transparent) { + COREWEBVIEW2_COLOR bgColor = {0, 0, 0, 0}; + ICoreWebView2Controller2* ctrl2; + if (SUCCEEDED(controller->QueryInterface(IID_PPV_ARGS(&ctrl2)))) { + ctrl2->put_DefaultBackgroundColor(bgColor); + ctrl2->Release(); + } + } + + state->webview->add_WebMessageReceived( + Callback( + [state](ICoreWebView2* s, ICoreWebView2WebMessageReceivedEventArgs* a) -> HRESULT { + return onMessageReceived(s, a, (DWORD*)(intptr_t)state->webviewId); + }).Get(), + &state->messageToken); + + state->webview->add_NavigationCompleted( + Callback( + [state](ICoreWebView2* s, ICoreWebView2NavigationCompletedEventArgs* a) -> HRESULT { + return onNavCompleted(s, a, (DWORD*)(intptr_t)state->webviewId); + }).Get(), + &state->navCompletedToken); + + state->webview->add_ProcessFailed( + Callback( + [state](ICoreWebView2* s, ICoreWebView2ProcessFailedEventArgs* a) -> HRESULT { + return onProcessFailed(s, a, (DWORD*)(intptr_t)state->webviewId); + }).Get(), + &state->errorToken); + + if (!state->pendingHTML.empty()) { + state->webview->NavigateToString(state->pendingHTML.c_str()); + state->pendingHTML.clear(); + } + if (!state->pendingURL.empty()) { + state->webview->Navigate(state->pendingURL.c_str()); + state->pendingURL.clear(); + } + + return S_OK; + }).Get()); +} + +int platformCreate(WebViewInstance* wv) { + ensureWebView2Environment(); + + auto* state = new WinWebViewState(); + state->controller = nullptr; + state->webview = nullptr; + state->detachedHwnd = nullptr; + state->isDetached = (wv->parentWindow < 0); + state->destroyed = false; + state->pending = false; + state->messageToken = {0}; + state->navCompletedToken = {0}; + state->errorToken = {0}; + state->webviewId = wv->id; + state->x = wv->x; + state->y = wv->y; + state->width = wv->width; + state->height = wv->height; + state->transparent = wv->transparent; + wv->platformHandle = state; + + if (state->isDetached) { + static const wchar_t* className = L"LNXRNT_WebViewWindow"; + static bool registered = false; + if (!registered) { + WNDCLASSW wc = {}; + wc.lpfnWndProc = DefWindowProcW; + wc.hInstance = GetModuleHandle(nullptr); + wc.lpszClassName = className; + RegisterClassW(&wc); + registered = true; + } + + state->detachedHwnd = CreateWindowExW( + WS_EX_APPWINDOW, className, + utf8ToWide(wv->title).c_str(), + WS_OVERLAPPEDWINDOW, + wv->x, wv->y, wv->width, wv->height, + nullptr, nullptr, GetModuleHandle(nullptr), nullptr); + state->parentHwnd = state->detachedHwnd; + } else { + extern struct HWND__ *kinc_windows_window_handle(int window_index); + state->parentHwnd = kinc_windows_window_handle(wv->parentWindow); + if (!state->parentHwnd) { + kinc_log(KINC_LOG_LEVEL_ERROR, "[WebView] Failed to get Kinc HWND for window %d", wv->parentWindow); + delete state; + wv->platformHandle = nullptr; + return -1; + } + } + + if (!g_webview2Env) { + state->pending = true; + g_pendingCreates.push_back(wv); + kinc_log(KINC_LOG_LEVEL_INFO, "[WebView] Environment not ready, queuing creation id=%d", wv->id); + return wv->id; + } + + createController(state); + + return wv->id; +} + +void platformDestroy(WebViewInstance* wv) { + auto* state = (WinWebViewState*)wv->platformHandle; + if (!state) return; + + if (state->pending) { + g_pendingCreates.erase( + std::remove(g_pendingCreates.begin(), g_pendingCreates.end(), wv), + g_pendingCreates.end()); + state->pending = false; + if (state->detachedHwnd) { + DestroyWindow(state->detachedHwnd); + state->detachedHwnd = nullptr; + } + delete state; + wv->platformHandle = nullptr; + return; + } + + if (!state->controller) { + state->destroyed = true; + wv->platformHandle = nullptr; + if (state->detachedHwnd) { + DestroyWindow(state->detachedHwnd); + state->detachedHwnd = nullptr; + } + return; + } + + if (state->webview) { + if (state->messageToken.value) state->webview->remove_WebMessageReceived(state->messageToken); + if (state->navCompletedToken.value) state->webview->remove_NavigationCompleted(state->navCompletedToken); + if (state->errorToken.value) state->webview->remove_ProcessFailed(state->errorToken); + state->webview->Release(); + } + if (state->controller) { + state->controller->Close(); + state->controller->Release(); + } + if (state->detachedHwnd) { + DestroyWindow(state->detachedHwnd); + } + delete state; + wv->platformHandle = nullptr; +} + +void platformLoadHTML(WebViewInstance* wv, const std::string& html) { + auto* state = (WinWebViewState*)wv->platformHandle; + if (!state) return; + if (!state->webview) { + state->pendingHTML = utf8ToWide(html); + state->pendingURL.clear(); + return; + } + state->webview->NavigateToString(utf8ToWide(html).c_str()); +} + +void platformLoadURL(WebViewInstance* wv, const std::string& url) { + auto* state = (WinWebViewState*)wv->platformHandle; + if (!state) return; + if (!state->webview) { + state->pendingURL = utf8ToWide(url); + state->pendingHTML.clear(); + return; + } + state->webview->Navigate(utf8ToWide(url).c_str()); +} + +void platformEvalJS(WebViewInstance* wv, const std::string& js) { + auto* state = (WinWebViewState*)wv->platformHandle; + if (!state || !state->webview) return; + state->webview->ExecuteScript(utf8ToWide(js).c_str(), nullptr); +} + +void platformEvalJSAsync(WebViewInstance* wv, const std::string& js) { + auto* state = (WinWebViewState*)wv->platformHandle; + if (!state || !state->webview) return; + int webviewId = wv->id; + state->webview->ExecuteScript(utf8ToWide(js).c_str(), + Callback( + [webviewId](HRESULT errorCode, LPCWSTR resultJson) -> HRESULT { + std::string resultStr; + if (FAILED(errorCode) || resultJson == nullptr) { + resultStr = "null"; + } else { + std::wstring wstr(resultJson); + resultStr = std::string(wstr.begin(), wstr.end()); + } + WebViewManager::instance().dispatchEvalResult(webviewId, resultStr); + return S_OK; + }).Get()); +} + +void platformShow(WebViewInstance* wv) { + auto* state = (WinWebViewState*)wv->platformHandle; + if (!state) return; + if (state->isDetached && state->detachedHwnd) { + ShowWindow(state->detachedHwnd, SW_SHOW); + } + if (state->controller) { + state->controller->put_IsVisible(TRUE); + } +} + +void platformHide(WebViewInstance* wv) { + auto* state = (WinWebViewState*)wv->platformHandle; + if (!state) return; + if (state->isDetached && state->detachedHwnd) { + ShowWindow(state->detachedHwnd, SW_HIDE); + } + if (state->controller) { + state->controller->put_IsVisible(FALSE); + } +} + +void platformResize(WebViewInstance* wv, int width, int height) { + auto* state = (WinWebViewState*)wv->platformHandle; + if (!state || !state->controller) return; + + if (state->isDetached && state->detachedHwnd) { + RECT rect = {0, 0, width, height}; + AdjustWindowRect(&rect, WS_OVERLAPPEDWINDOW, FALSE); + SetWindowPos(state->detachedHwnd, nullptr, 0, 0, + rect.right - rect.left, rect.bottom - rect.top, SWP_NOMOVE | SWP_NOZORDER); + } + + RECT bounds; + if (state->isDetached) { + GetClientRect(state->detachedHwnd, &bounds); + } else { + bounds.left = wv->x; + bounds.top = wv->y; + bounds.right = wv->x + width; + bounds.bottom = wv->y + height; + } + state->controller->put_Bounds(bounds); +} + +void platformMove(WebViewInstance* wv, int x, int y) { + auto* state = (WinWebViewState*)wv->platformHandle; + if (!state) return; + + if (state->isDetached && state->detachedHwnd) { + SetWindowPos(state->detachedHwnd, nullptr, x, y, 0, 0, SWP_NOSIZE | SWP_NOZORDER); + } else if (state->controller) { + RECT bounds; + bounds.left = x; + bounds.top = y; + bounds.right = x + wv->width; + bounds.bottom = y + wv->height; + state->controller->put_Bounds(bounds); + } +} + +void platformSetTransparent(WebViewInstance* wv, bool transparent) { + auto* state = (WinWebViewState*)wv->platformHandle; + if (!state || !state->controller) return; + + ICoreWebView2Controller2* ctrl2; + if (SUCCEEDED(state->controller->QueryInterface(IID_PPV_ARGS(&ctrl2)))) { + COREWEBVIEW2_COLOR bgColor; + if (transparent) { + bgColor = {0, 0, 0, 0}; + } else { + bgColor = {255, 255, 255, 255}; + } + ctrl2->put_DefaultBackgroundColor(bgColor); + ctrl2->Release(); + } +} + +void platformSetClickThrough(WebViewInstance* wv, bool enabled) { + auto* state = (WinWebViewState*)wv->platformHandle; + if (!state || !state->controller) return; + + if (enabled) { + HWND childHwnd = FindWindowEx(state->parentHwnd, nullptr, L"WebView2", nullptr); + if (childHwnd) { + LONG_PTR exStyle = GetWindowLongPtr(childHwnd, GWL_EXSTYLE); + SetWindowLongPtr(childHwnd, GWL_EXSTYLE, exStyle | WS_EX_TRANSPARENT); + } + } else { + HWND childHwnd = FindWindowEx(state->parentHwnd, nullptr, L"WebView2", nullptr); + if (childHwnd) { + LONG_PTR exStyle = GetWindowLongPtr(childHwnd, GWL_EXSTYLE); + SetWindowLongPtr(childHwnd, GWL_EXSTYLE, exStyle & ~WS_EX_TRANSPARENT); + } + } +} + +void platformSetTitle(WebViewInstance* wv, const std::string& title) { + auto* state = (WinWebViewState*)wv->platformHandle; + if (!state || !state->isDetached || !state->detachedHwnd) return; + SetWindowTextW(state->detachedHwnd, utf8ToWide(title).c_str()); +} + +void platformSend(WebViewInstance* wv, const std::string& message) { + auto* state = (WinWebViewState*)wv->platformHandle; + if (!state || !state->webview) return; + state->webview->PostWebMessageAsString(utf8ToWide(message).c_str()); +} + +void platformGoBack(WebViewInstance* wv) { + auto* state = (WinWebViewState*)wv->platformHandle; + if (!state || !state->webview) return; + BOOL canGo = FALSE; + state->webview->get_CanGoBack(&canGo); + if (canGo) state->webview->GoBack(); +} + +void platformGoForward(WebViewInstance* wv) { + auto* state = (WinWebViewState*)wv->platformHandle; + if (!state || !state->webview) return; + BOOL canGo = FALSE; + state->webview->get_CanGoForward(&canGo); + if (canGo) state->webview->GoForward(); +} + +void platformReload(WebViewInstance* wv) { + auto* state = (WinWebViewState*)wv->platformHandle; + if (!state || !state->webview) return; + state->webview->Reload(); +} + +bool platformCanGoBack(WebViewInstance* wv) { + auto* state = (WinWebViewState*)wv->platformHandle; + if (!state || !state->webview) return false; + BOOL canGo = FALSE; + state->webview->get_CanGoBack(&canGo); + return canGo != FALSE; +} + +bool platformCanGoForward(WebViewInstance* wv) { + auto* state = (WinWebViewState*)wv->platformHandle; + if (!state || !state->webview) return false; + BOOL canGo = FALSE; + state->webview->get_CanGoForward(&canGo); + return canGo != FALSE; +} + +std::string platformGetURL(WebViewInstance* wv) { + auto* state = (WinWebViewState*)wv->platformHandle; + if (!state || !state->webview) return ""; + LPWSTR uri = nullptr; + state->webview->get_Source(&uri); + std::string result; + if (uri) { + int len = WideCharToMultiByte(CP_UTF8, 0, uri, -1, nullptr, 0, nullptr, nullptr); + result.resize(len - 1); + WideCharToMultiByte(CP_UTF8, 0, uri, -1, &result[0], len, nullptr, nullptr); + CoTaskMemFree(uri); + } + return result; +} + +std::string platformGetTitle(WebViewInstance* wv) { + auto* state = (WinWebViewState*)wv->platformHandle; + if (!state || !state->webview) return ""; + LPWSTR title = nullptr; + state->webview->get_DocumentTitle(&title); + std::string result; + if (title) { + int len = WideCharToMultiByte(CP_UTF8, 0, title, -1, nullptr, 0, nullptr, nullptr); + result.resize(len - 1); + WideCharToMultiByte(CP_UTF8, 0, title, -1, &result[0], len, nullptr, nullptr); + CoTaskMemFree(title); + } + return result; +} + +void platformMinimize(WebViewInstance* wv) { + auto* state = (WinWebViewState*)wv->platformHandle; + if (!state || !state->isDetached || !state->detachedHwnd) return; + ShowWindow(state->detachedHwnd, SW_MINIMIZE); +} + +void platformMaximize(WebViewInstance* wv) { + auto* state = (WinWebViewState*)wv->platformHandle; + if (!state || !state->isDetached || !state->detachedHwnd) return; + ShowWindow(state->detachedHwnd, SW_MAXIMIZE); +} + +void platformRestore(WebViewInstance* wv) { + auto* state = (WinWebViewState*)wv->platformHandle; + if (!state || !state->isDetached || !state->detachedHwnd) return; + ShowWindow(state->detachedHwnd, SW_RESTORE); +} + +void platformSetFullscreen(WebViewInstance* wv, bool fullscreen) { + auto* state = (WinWebViewState*)wv->platformHandle; + if (!state || !state->isDetached || !state->detachedHwnd) return; + LONG_PTR style = GetWindowLongPtr(state->detachedHwnd, GWL_STYLE); + if (fullscreen) { + SetWindowLongPtr(state->detachedHwnd, GWL_STYLE, style & ~WS_OVERLAPPEDWINDOW); + HMONITOR hmon = MonitorFromWindow(state->detachedHwnd, MONITOR_DEFAULTTONEAREST); + MONITORINFO mi = { sizeof(mi) }; + GetMonitorInfo(hmon, &mi); + SetWindowPos(state->detachedHwnd, HWND_TOP, mi.rcMonitor.left, mi.rcMonitor.top, + mi.rcMonitor.right - mi.rcMonitor.left, mi.rcMonitor.bottom - mi.rcMonitor.top, + SWP_NOOWNERZORDER | SWP_FRAMECHANGED); + } else { + SetWindowLongPtr(state->detachedHwnd, GWL_STYLE, style | WS_OVERLAPPEDWINDOW); + SetWindowPos(state->detachedHwnd, nullptr, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER | SWP_FRAMECHANGED); + } +} + +bool platformIsFullscreen(WebViewInstance* wv) { + auto* state = (WinWebViewState*)wv->platformHandle; + if (!state || !state->isDetached || !state->detachedHwnd) return false; + LONG_PTR style = GetWindowLongPtr(state->detachedHwnd, GWL_STYLE); + return (style & WS_OVERLAPPEDWINDOW) == 0; +} + +void platformEnableDevTools(WebViewInstance* wv, bool enabled) { + auto* state = (WinWebViewState*)wv->platformHandle; + if (!state || !state->webview) return; + ICoreWebView2Settings* settings; + if (SUCCEEDED(state->webview->get_Settings(&settings))) { + ICoreWebView2Settings3* settings3; + if (SUCCEEDED(settings->QueryInterface(IID_PPV_ARGS(&settings3)))) { + settings3->put_AreDevToolsEnabled(enabled ? TRUE : FALSE); + settings3->Release(); + } + settings->Release(); + } +} + +void platformSetContextMenuEnabled(WebViewInstance* wv, bool enabled) { + auto* state = (WinWebViewState*)wv->platformHandle; + if (!state || !state->webview) return; + ICoreWebView2Settings* settings; + if (SUCCEEDED(state->webview->get_Settings(&settings))) { + settings->put_AreDefaultContextMenusEnabled(enabled ? TRUE : FALSE); + settings->Release(); + } +} + +void platformTick() { + // COM event loop is pumped by Kincs loop. +} + +#endif +#endif diff --git a/kfile.js b/kfile.js index 652fed3..35c7817 100644 --- a/kfile.js +++ b/kfile.js @@ -10,6 +10,7 @@ let flags = { with_compute: true, with_networking: true, with_viewport: true, + with_webview: true, with_uws: false, with_ssl: true, debug_network: true @@ -62,6 +63,21 @@ if (flags.with_viewport && (platform === Platform.Windows || platform === Platfo project.addDefine('WITH_VIEWPORT'); } +if (flags.with_webview && (platform === Platform.Windows || platform === Platform.Linux || platform === Platform.OSX)) { + project.addDefine('WITH_WEBVIEW'); + project.addFile('Sources/webview.h'); + project.addFile('Sources/webview.cpp'); + if (platform === Platform.Windows) { + project.addFile('Sources/webview_win.cpp'); + } + else if (platform === Platform.OSX) { + project.addFile('Sources/webview_mac.mm'); + } + else if (platform === Platform.Linux) { + project.addFile('Sources/webview_linux.cpp'); + } +} + project.addFile('Sources/main.cpp'); @@ -124,6 +140,10 @@ if (platform === Platform.Windows) { project.addLib('Dwmapi'); // DWMWA_USE_IMMERSIVE_DARK_MODE project.addLib('winmm'); // timeGetTime for V8 project.addLib(libdir + 'v8_monolith'); + if (flags.with_webview) { + project.addIncludeDir('webview2/include'); + project.addLib('WebView2Loader.lib'); + } if (!flags.release) { project.addDefine('_HAS_ITERATOR_DEBUGGING=0'); project.addDefine('_ITERATOR_DEBUG_LEVEL=0'); @@ -135,14 +155,25 @@ if (platform === Platform.Windows) { } } else if (platform === Platform.Linux) { + project.addCppFlag('-fno-rtti'); project.addLib('v8_monolith -L' + libdir); project.addDefine("KINC_NO_WAYLAND"); + if (flags.with_webview) { + project.addLib('webkit2gtk-4.1'); + project.addLib('gtk-3'); + project.addLib('glib-2.0'); + } if (flags.with_networking) { project.addLib('pthread'); } } else if (platform === Platform.OSX) { + project.addCppFlag('-fno-rtti'); project.addLib('v8_monolith -L' + libdir); + if (flags.with_webview) { + project.addLinkerFlag('-framework WebKit'); + project.addLinkerFlag('-framework AppKit'); + } if (flags.with_networking) { project.addIncludeDir('apple-libressl-sdk/include'); project.addLinkerFlag('-L' + root + '/apple-libressl-sdk/lib');