Files
LNXRNT/Sources/webview_win.cpp
2026-07-15 19:37:38 -07:00

596 lines
18 KiB
C++

#ifdef WITH_WEBVIEW
#ifdef KINC_WINDOWS
#include "webview.h"
#include <kinc/log.h>
#include <kinc/window.h>
#include <windows.h>
#include <unknwn.h>
#include <comdef.h>
#include <vector>
#include <algorithm>
#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<WebViewInstance*> 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<ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler>(
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<ICoreWebView2CreateCoreWebView2ControllerCompletedHandler>(
[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<ICoreWebView2WebMessageReceivedEventHandler>(
[state](ICoreWebView2* s, ICoreWebView2WebMessageReceivedEventArgs* a) -> HRESULT {
return onMessageReceived(s, a, (DWORD*)(intptr_t)state->webviewId);
}).Get(),
&state->messageToken);
state->webview->add_NavigationCompleted(
Callback<ICoreWebView2NavigationCompletedEventHandler>(
[state](ICoreWebView2* s, ICoreWebView2NavigationCompletedEventArgs* a) -> HRESULT {
return onNavCompleted(s, a, (DWORD*)(intptr_t)state->webviewId);
}).Get(),
&state->navCompletedToken);
state->webview->add_ProcessFailed(
Callback<ICoreWebView2ProcessFailedEventHandler>(
[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<ICoreWebView2ExecuteScriptCompletedHandler>(
[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