2026-07-15 19:37:38 -07:00
|
|
|
#ifdef WITH_WEBVIEW
|
2026-07-22 20:58:04 -07:00
|
|
|
#ifdef _WIN32
|
2026-07-15 19:37:38 -07:00
|
|
|
|
|
|
|
|
#include "webview.h"
|
|
|
|
|
#include <kinc/log.h>
|
|
|
|
|
#include <kinc/window.h>
|
|
|
|
|
#include <windows.h>
|
2026-07-22 20:58:04 -07:00
|
|
|
#include <windowsx.h>
|
2026-07-15 19:37:38 -07:00
|
|
|
#include <unknwn.h>
|
|
|
|
|
#include <comdef.h>
|
2026-07-22 20:58:04 -07:00
|
|
|
#include <wrl.h>
|
|
|
|
|
#include <dcomp.h>
|
2026-07-15 19:37:38 -07:00
|
|
|
#include <vector>
|
|
|
|
|
#include <algorithm>
|
2026-07-22 20:58:04 -07:00
|
|
|
#include <map>
|
2026-07-15 19:37:38 -07:00
|
|
|
|
|
|
|
|
#include "WebView2.h"
|
|
|
|
|
|
2026-07-22 20:58:04 -07:00
|
|
|
#pragma comment(lib, "dcomp.lib")
|
|
|
|
|
|
|
|
|
|
using namespace Microsoft::WRL;
|
|
|
|
|
|
|
|
|
|
extern "C" struct HWND__ *kinc_windows_window_handle(int window_index);
|
|
|
|
|
|
|
|
|
|
struct WinWebViewState;
|
|
|
|
|
|
|
|
|
|
struct DCompShared {
|
|
|
|
|
IDCompositionDevice* device;
|
|
|
|
|
IDCompositionTarget* target;
|
|
|
|
|
IDCompositionVisual* rootVisual;
|
|
|
|
|
HWND dcompHwnd;
|
|
|
|
|
HWND parentHwnd;
|
|
|
|
|
int refCount;
|
|
|
|
|
std::vector<WinWebViewState*> webviews;
|
|
|
|
|
};
|
|
|
|
|
static std::map<HWND, DCompShared*> g_dcompShared;
|
|
|
|
|
static std::map<HWND, DCompShared*> g_dcompByHwnd;
|
2026-07-15 19:37:38 -07:00
|
|
|
|
|
|
|
|
struct WinWebViewState {
|
|
|
|
|
ICoreWebView2Controller* controller;
|
|
|
|
|
ICoreWebView2* webview;
|
|
|
|
|
HWND parentHwnd;
|
|
|
|
|
HWND detachedHwnd;
|
|
|
|
|
bool isDetached;
|
|
|
|
|
bool destroyed;
|
|
|
|
|
bool pending;
|
2026-07-22 20:58:04 -07:00
|
|
|
bool pendingShow;
|
|
|
|
|
DCompShared* dcompShared;
|
|
|
|
|
IDCompositionVisual* dcompVisual;
|
|
|
|
|
ICoreWebView2CompositionController* compController;
|
|
|
|
|
bool useComposition;
|
2026-07-15 19:37:38 -07:00
|
|
|
EventRegistrationToken messageToken;
|
2026-07-22 20:58:04 -07:00
|
|
|
EventRegistrationToken navStartingToken;
|
2026-07-15 19:37:38 -07:00
|
|
|
EventRegistrationToken navCompletedToken;
|
|
|
|
|
EventRegistrationToken errorToken;
|
2026-07-22 20:58:04 -07:00
|
|
|
EventRegistrationToken sourceChangedToken;
|
|
|
|
|
EventRegistrationToken contentLoadingToken;
|
2026-07-15 19:37:38 -07:00
|
|
|
int webviewId;
|
|
|
|
|
int x, y, width, height;
|
|
|
|
|
bool transparent;
|
2026-07-22 20:58:04 -07:00
|
|
|
EventRegistrationToken cursorChangedToken;
|
|
|
|
|
HCURSOR currentCursor;
|
2026-07-15 19:37:38 -07:00
|
|
|
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;
|
|
|
|
|
|
2026-07-22 20:58:04 -07:00
|
|
|
LPWSTR versionInfo = nullptr;
|
|
|
|
|
HRESULT verHr = GetAvailableCoreWebView2BrowserVersionString(nullptr, &versionInfo);
|
|
|
|
|
if (SUCCEEDED(verHr) && versionInfo) {
|
|
|
|
|
CoTaskMemFree(versionInfo);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-15 19:37:38 -07:00
|
|
|
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;
|
2026-07-22 20:58:04 -07:00
|
|
|
BOOL success = FALSE;
|
|
|
|
|
if (args) args->get_IsSuccess(&success);
|
2026-07-15 19:37:38 -07:00
|
|
|
WebViewManager::instance().dispatchLoad(webviewId);
|
|
|
|
|
return S_OK;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-22 20:58:04 -07:00
|
|
|
static HRESULT STDMETHODCALLTYPE onSourceChanged(
|
|
|
|
|
ICoreWebView2* sender, ICoreWebView2SourceChangedEventArgs* args, DWORD* webviewIdPtr) {
|
|
|
|
|
int webviewId = (int)(intptr_t)webviewIdPtr;
|
|
|
|
|
LPWSTR uri = nullptr;
|
|
|
|
|
sender->get_Source(&uri);
|
|
|
|
|
if (uri) CoTaskMemFree(uri);
|
|
|
|
|
return S_OK;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static HRESULT STDMETHODCALLTYPE onContentLoading(
|
|
|
|
|
ICoreWebView2* sender, ICoreWebView2ContentLoadingEventArgs* args, DWORD* webviewIdPtr) {
|
|
|
|
|
int webviewId = (int)(intptr_t)webviewIdPtr;
|
|
|
|
|
return S_OK;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-15 19:37:38 -07:00
|
|
|
static HRESULT STDMETHODCALLTYPE onProcessFailed(
|
|
|
|
|
ICoreWebView2* sender, ICoreWebView2ProcessFailedEventArgs* args, DWORD* webviewIdPtr) {
|
|
|
|
|
int webviewId = (int)(intptr_t)webviewIdPtr;
|
2026-07-22 20:58:04 -07:00
|
|
|
COREWEBVIEW2_PROCESS_FAILED_KIND kind = COREWEBVIEW2_PROCESS_FAILED_KIND_BROWSER_PROCESS_EXITED;
|
|
|
|
|
if (args) args->get_ProcessFailedKind(&kind);
|
2026-07-15 19:37:38 -07:00
|
|
|
WebViewManager::instance().dispatchError(webviewId, "WebView2 process failed");
|
|
|
|
|
return S_OK;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-22 20:58:04 -07:00
|
|
|
static void onControllerReady(WinWebViewState* state, HRESULT result, ICoreWebView2Controller* controller) {
|
|
|
|
|
if (state->destroyed) {
|
|
|
|
|
if (controller) controller->Release();
|
|
|
|
|
delete state;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (FAILED(result) || !controller) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
state->controller = controller;
|
|
|
|
|
state->controller->AddRef();
|
|
|
|
|
controller->get_CoreWebView2(&state->webview);
|
|
|
|
|
state->webview->AddRef();
|
|
|
|
|
|
|
|
|
|
RECT bounds;
|
|
|
|
|
if (state->isDetached) {
|
|
|
|
|
GetClientRect(state->detachedHwnd, &bounds);
|
|
|
|
|
} else if (state->useComposition) {
|
|
|
|
|
bounds.left = 0;
|
|
|
|
|
bounds.top = 0;
|
|
|
|
|
bounds.right = state->width;
|
|
|
|
|
bounds.bottom = state->height;
|
|
|
|
|
} 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();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
HRESULT hr;
|
|
|
|
|
hr = state->webview->add_WebMessageReceived(
|
|
|
|
|
Callback<ICoreWebView2WebMessageReceivedEventHandler>(
|
|
|
|
|
[state](ICoreWebView2* s, ICoreWebView2WebMessageReceivedEventArgs* a) -> HRESULT {
|
|
|
|
|
return onMessageReceived(s, a, (DWORD*)(intptr_t)state->webviewId);
|
|
|
|
|
}).Get(),
|
|
|
|
|
&state->messageToken);
|
|
|
|
|
|
|
|
|
|
hr = state->webview->add_NavigationStarting(
|
|
|
|
|
Callback<ICoreWebView2NavigationStartingEventHandler>(
|
|
|
|
|
[state](ICoreWebView2* s, ICoreWebView2NavigationStartingEventArgs* a) -> HRESULT {
|
|
|
|
|
LPWSTR uri = nullptr;
|
|
|
|
|
a->get_Uri(&uri);
|
|
|
|
|
if (uri) CoTaskMemFree(uri);
|
|
|
|
|
return S_OK;
|
|
|
|
|
}).Get(),
|
|
|
|
|
&state->navStartingToken);
|
|
|
|
|
|
|
|
|
|
hr = state->webview->add_NavigationCompleted(
|
|
|
|
|
Callback<ICoreWebView2NavigationCompletedEventHandler>(
|
|
|
|
|
[state](ICoreWebView2* s, ICoreWebView2NavigationCompletedEventArgs* a) -> HRESULT {
|
|
|
|
|
return onNavCompleted(s, a, (DWORD*)(intptr_t)state->webviewId);
|
|
|
|
|
}).Get(),
|
|
|
|
|
&state->navCompletedToken);
|
|
|
|
|
|
|
|
|
|
hr = state->webview->add_SourceChanged(
|
|
|
|
|
Callback<ICoreWebView2SourceChangedEventHandler>(
|
|
|
|
|
[state](ICoreWebView2* s, ICoreWebView2SourceChangedEventArgs* a) -> HRESULT {
|
|
|
|
|
return onSourceChanged(s, a, (DWORD*)(intptr_t)state->webviewId);
|
|
|
|
|
}).Get(),
|
|
|
|
|
&state->sourceChangedToken);
|
|
|
|
|
|
|
|
|
|
hr = state->webview->add_ContentLoading(
|
|
|
|
|
Callback<ICoreWebView2ContentLoadingEventHandler>(
|
|
|
|
|
[state](ICoreWebView2* s, ICoreWebView2ContentLoadingEventArgs* a) -> HRESULT {
|
|
|
|
|
return onContentLoading(s, a, (DWORD*)(intptr_t)state->webviewId);
|
|
|
|
|
}).Get(),
|
|
|
|
|
&state->contentLoadingToken);
|
|
|
|
|
|
|
|
|
|
hr = 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->pendingShow) {
|
|
|
|
|
HRESULT visHr = state->controller->put_IsVisible(TRUE);
|
|
|
|
|
state->pendingShow = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!state->pendingHTML.empty()) {
|
|
|
|
|
HRESULT navHr = state->webview->NavigateToString(state->pendingHTML.c_str());
|
|
|
|
|
state->pendingHTML.clear();
|
|
|
|
|
}
|
|
|
|
|
{
|
|
|
|
|
LPWSTR uri = nullptr;
|
|
|
|
|
state->webview->get_Source(&uri);
|
|
|
|
|
if (uri) CoTaskMemFree(uri);
|
|
|
|
|
}
|
|
|
|
|
if (!state->pendingURL.empty()) {
|
|
|
|
|
HRESULT navHr = state->webview->Navigate(state->pendingURL.c_str());
|
|
|
|
|
state->pendingURL.clear();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static const wchar_t* g_dcompClassName = L"LNXRNT_DCompChild";
|
|
|
|
|
static HWND g_dcompParentHwnd = nullptr;
|
|
|
|
|
|
|
|
|
|
static WinWebViewState* findWebviewAtPoint(DCompShared* shared, int screenX, int screenY) {
|
|
|
|
|
if (!shared) return nullptr;
|
|
|
|
|
RECT popupRect;
|
|
|
|
|
GetWindowRect(shared->dcompHwnd, &popupRect);
|
|
|
|
|
int localX = screenX - popupRect.left;
|
|
|
|
|
int localY = screenY - popupRect.top;
|
|
|
|
|
for (auto* wv : shared->webviews) {
|
|
|
|
|
if (wv->destroyed || !wv->compController) continue;
|
|
|
|
|
if (localX >= wv->x && localX < wv->x + wv->width &&
|
|
|
|
|
localY >= wv->y && localY < wv->y + wv->height) {
|
|
|
|
|
return wv;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool isWebviewInteractive(WinWebViewState* wv) {
|
|
|
|
|
if (!wv || !wv->compController) return false;
|
|
|
|
|
HCURSOR cursor = nullptr;
|
|
|
|
|
HRESULT hr = wv->compController->get_Cursor(&cursor);
|
|
|
|
|
if (FAILED(hr) || !cursor) return false;
|
|
|
|
|
HCURSOR defaultArrow = LoadCursor(nullptr, IDC_ARROW);
|
|
|
|
|
if (cursor == defaultArrow) return false;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void forwardMouseToWebview(WinWebViewState* wv, UINT msg, WPARAM wParam, LPARAM lParam) {
|
|
|
|
|
if (!wv || !wv->compController) return;
|
|
|
|
|
int x = GET_X_LPARAM(lParam) - wv->x;
|
|
|
|
|
int y = GET_Y_LPARAM(lParam) - wv->y;
|
|
|
|
|
POINT pt = {x, y};
|
|
|
|
|
|
|
|
|
|
COREWEBVIEW2_MOUSE_EVENT_KIND kind;
|
|
|
|
|
COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS vkeys = (COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS)0;
|
|
|
|
|
UINT32 mouseData = 0;
|
|
|
|
|
|
|
|
|
|
switch (msg) {
|
|
|
|
|
case WM_MOUSEMOVE:
|
|
|
|
|
kind = COREWEBVIEW2_MOUSE_EVENT_KIND_MOVE;
|
|
|
|
|
break;
|
|
|
|
|
case WM_LBUTTONDOWN:
|
|
|
|
|
kind = COREWEBVIEW2_MOUSE_EVENT_KIND_LEFT_BUTTON_DOWN;
|
|
|
|
|
vkeys = (COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS)(
|
|
|
|
|
COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS_LEFT_BUTTON |
|
|
|
|
|
(GET_KEYSTATE_WPARAM(wParam) & MK_SHIFT ? COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS_SHIFT : 0) |
|
|
|
|
|
(GET_KEYSTATE_WPARAM(wParam) & MK_CONTROL ? COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS_CONTROL : 0));
|
|
|
|
|
break;
|
|
|
|
|
case WM_LBUTTONUP:
|
|
|
|
|
kind = COREWEBVIEW2_MOUSE_EVENT_KIND_LEFT_BUTTON_UP;
|
|
|
|
|
vkeys = (COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS)(
|
|
|
|
|
(GET_KEYSTATE_WPARAM(wParam) & MK_SHIFT ? COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS_SHIFT : 0) |
|
|
|
|
|
(GET_KEYSTATE_WPARAM(wParam) & MK_CONTROL ? COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS_CONTROL : 0));
|
|
|
|
|
break;
|
|
|
|
|
case WM_RBUTTONDOWN:
|
|
|
|
|
kind = COREWEBVIEW2_MOUSE_EVENT_KIND_RIGHT_BUTTON_DOWN;
|
|
|
|
|
vkeys = COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS_RIGHT_BUTTON;
|
|
|
|
|
break;
|
|
|
|
|
case WM_RBUTTONUP:
|
|
|
|
|
kind = COREWEBVIEW2_MOUSE_EVENT_KIND_RIGHT_BUTTON_UP;
|
|
|
|
|
break;
|
|
|
|
|
case WM_MBUTTONDOWN:
|
|
|
|
|
kind = COREWEBVIEW2_MOUSE_EVENT_KIND_MIDDLE_BUTTON_DOWN;
|
|
|
|
|
vkeys = COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS_MIDDLE_BUTTON;
|
|
|
|
|
break;
|
|
|
|
|
case WM_MBUTTONUP:
|
|
|
|
|
kind = COREWEBVIEW2_MOUSE_EVENT_KIND_MIDDLE_BUTTON_UP;
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
wv->compController->SendMouseInput(kind, vkeys, mouseData, pt);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void forwardMouseToParent(DCompShared* shared, HWND popupHwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
|
|
|
|
|
if (!shared || !shared->parentHwnd) return;
|
|
|
|
|
POINT pt = {GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)};
|
|
|
|
|
MapWindowPoints(popupHwnd, shared->parentHwnd, &pt, 1);
|
|
|
|
|
LPARAM newLParam = MAKELPARAM(pt.x, pt.y);
|
|
|
|
|
SendMessageW(shared->parentHwnd, msg, wParam, newLParam);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static WinWebViewState* g_mouseCaptureWv = nullptr;
|
|
|
|
|
static bool g_mouseCaptureIsWebview = false;
|
|
|
|
|
|
|
|
|
|
static LRESULT CALLBACK dcompWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
|
|
|
|
|
if (msg == WM_TIMER && wParam == 1) {
|
|
|
|
|
HWND parent = g_dcompParentHwnd;
|
|
|
|
|
if (parent && IsWindow(parent)) {
|
|
|
|
|
RECT clientRect;
|
|
|
|
|
GetClientRect(parent, &clientRect);
|
|
|
|
|
POINT pt = {0, 0};
|
|
|
|
|
ClientToScreen(parent, &pt);
|
|
|
|
|
SetWindowPos(hwnd, HWND_TOP, pt.x, pt.y,
|
|
|
|
|
clientRect.right - clientRect.left,
|
|
|
|
|
clientRect.bottom - clientRect.top,
|
|
|
|
|
SWP_NOACTIVATE | SWP_SHOWWINDOW);
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto it = g_dcompByHwnd.find(hwnd);
|
|
|
|
|
DCompShared* shared = (it != g_dcompByHwnd.end()) ? it->second : nullptr;
|
|
|
|
|
|
|
|
|
|
if (msg == WM_NCHITTEST) {
|
|
|
|
|
return HTCLIENT;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (msg == WM_SETCURSOR) {
|
|
|
|
|
POINT screenPt;
|
|
|
|
|
GetCursorPos(&screenPt);
|
|
|
|
|
WinWebViewState* wv = findWebviewAtPoint(shared, screenPt.x, screenPt.y);
|
|
|
|
|
if (wv && isWebviewInteractive(wv) && wv->currentCursor) {
|
|
|
|
|
SetCursor(wv->currentCursor);
|
|
|
|
|
return TRUE;
|
|
|
|
|
}
|
|
|
|
|
return DefWindowProcW(hwnd, msg, wParam, lParam);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (msg == WM_MOUSEMOVE) {
|
|
|
|
|
POINT clientPt = {GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)};
|
|
|
|
|
POINT screenPt = clientPt;
|
|
|
|
|
ClientToScreen(hwnd, &screenPt);
|
|
|
|
|
WinWebViewState* wv = findWebviewAtPoint(shared, screenPt.x, screenPt.y);
|
|
|
|
|
bool interactive = isWebviewInteractive(wv);
|
|
|
|
|
|
|
|
|
|
if (g_mouseCaptureIsWebview && g_mouseCaptureWv) {
|
|
|
|
|
forwardMouseToWebview(g_mouseCaptureWv, msg, wParam, lParam);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (wv) {
|
|
|
|
|
forwardMouseToWebview(wv, msg, wParam, lParam);
|
|
|
|
|
}
|
|
|
|
|
if (!interactive) {
|
|
|
|
|
forwardMouseToParent(shared, hwnd, msg, wParam, lParam);
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (msg == WM_LBUTTONDOWN || msg == WM_RBUTTONDOWN || msg == WM_MBUTTONDOWN) {
|
|
|
|
|
POINT clientPt = {GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)};
|
|
|
|
|
POINT screenPt = clientPt;
|
|
|
|
|
ClientToScreen(hwnd, &screenPt);
|
|
|
|
|
WinWebViewState* wv = findWebviewAtPoint(shared, screenPt.x, screenPt.y);
|
|
|
|
|
bool interactive = isWebviewInteractive(wv);
|
|
|
|
|
|
|
|
|
|
if (interactive && wv) {
|
|
|
|
|
g_mouseCaptureWv = wv;
|
|
|
|
|
g_mouseCaptureIsWebview = true;
|
|
|
|
|
forwardMouseToWebview(wv, msg, wParam, lParam);
|
|
|
|
|
} else {
|
|
|
|
|
g_mouseCaptureWv = nullptr;
|
|
|
|
|
g_mouseCaptureIsWebview = false;
|
|
|
|
|
forwardMouseToParent(shared, hwnd, msg, wParam, lParam);
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (msg == WM_LBUTTONUP || msg == WM_RBUTTONUP || msg == WM_MBUTTONUP) {
|
|
|
|
|
if (g_mouseCaptureIsWebview && g_mouseCaptureWv) {
|
|
|
|
|
forwardMouseToWebview(g_mouseCaptureWv, msg, wParam, lParam);
|
|
|
|
|
g_mouseCaptureWv = nullptr;
|
|
|
|
|
g_mouseCaptureIsWebview = false;
|
|
|
|
|
} else {
|
|
|
|
|
forwardMouseToParent(shared, hwnd, msg, wParam, lParam);
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (msg == WM_MOUSEWHEEL) {
|
|
|
|
|
POINT screenPt = {GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)};
|
|
|
|
|
WinWebViewState* wv = findWebviewAtPoint(shared, screenPt.x, screenPt.y);
|
|
|
|
|
if (wv && isWebviewInteractive(wv)) {
|
|
|
|
|
POINT clientPt = screenPt;
|
|
|
|
|
ScreenToClient(hwnd, &clientPt);
|
|
|
|
|
int x = clientPt.x - wv->x;
|
|
|
|
|
int y = clientPt.y - wv->y;
|
|
|
|
|
POINT pt = {x, y};
|
|
|
|
|
wv->compController->SendMouseInput(
|
|
|
|
|
COREWEBVIEW2_MOUSE_EVENT_KIND_WHEEL,
|
|
|
|
|
(COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS)0,
|
|
|
|
|
GET_WHEEL_DELTA_WPARAM(wParam), pt);
|
|
|
|
|
} else {
|
|
|
|
|
forwardMouseToParent(shared, hwnd, msg, wParam, lParam);
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return DefWindowProcW(hwnd, msg, wParam, lParam);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static DCompShared* getOrCreateDCompShared(HWND hwnd) {
|
|
|
|
|
auto it = g_dcompShared.find(hwnd);
|
|
|
|
|
if (it != g_dcompShared.end()) {
|
|
|
|
|
it->second->refCount++;
|
|
|
|
|
return it->second;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto* shared = new DCompShared();
|
|
|
|
|
shared->device = nullptr;
|
|
|
|
|
shared->target = nullptr;
|
|
|
|
|
shared->rootVisual = nullptr;
|
|
|
|
|
shared->dcompHwnd = nullptr;
|
|
|
|
|
shared->parentHwnd = hwnd;
|
|
|
|
|
shared->refCount = 1;
|
|
|
|
|
|
|
|
|
|
static bool registered = false;
|
|
|
|
|
if (!registered) {
|
|
|
|
|
WNDCLASSW wc = {};
|
|
|
|
|
wc.lpfnWndProc = dcompWndProc;
|
|
|
|
|
wc.hInstance = GetModuleHandle(nullptr);
|
|
|
|
|
wc.lpszClassName = g_dcompClassName;
|
|
|
|
|
RegisterClassW(&wc);
|
|
|
|
|
registered = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
g_dcompParentHwnd = hwnd;
|
|
|
|
|
|
|
|
|
|
RECT clientRect;
|
|
|
|
|
GetClientRect(hwnd, &clientRect);
|
|
|
|
|
|
|
|
|
|
POINT pt = {0, 0};
|
|
|
|
|
ClientToScreen(hwnd, &pt);
|
|
|
|
|
int screenX = pt.x;
|
|
|
|
|
int screenY = pt.y;
|
|
|
|
|
int width = clientRect.right - clientRect.left;
|
|
|
|
|
int height = clientRect.bottom - clientRect.top;
|
|
|
|
|
|
|
|
|
|
shared->dcompHwnd = CreateWindowExW(
|
|
|
|
|
WS_EX_NOREDIRECTIONBITMAP | WS_EX_NOACTIVATE,
|
|
|
|
|
g_dcompClassName, L"",
|
|
|
|
|
WS_POPUP,
|
|
|
|
|
screenX, screenY,
|
|
|
|
|
width, height,
|
|
|
|
|
nullptr, nullptr, GetModuleHandle(nullptr), nullptr);
|
|
|
|
|
|
|
|
|
|
if (!shared->dcompHwnd) {
|
|
|
|
|
delete shared;
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
g_dcompByHwnd[shared->dcompHwnd] = shared;
|
|
|
|
|
SetWindowPos(shared->dcompHwnd, HWND_TOP, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_SHOWWINDOW);
|
|
|
|
|
SetTimer(shared->dcompHwnd, 1, 16, nullptr);
|
|
|
|
|
|
|
|
|
|
HRESULT hr = DCompositionCreateDevice(nullptr, IID_PPV_ARGS(&shared->device));
|
|
|
|
|
if (FAILED(hr) || !shared->device) {
|
|
|
|
|
DestroyWindow(shared->dcompHwnd);
|
|
|
|
|
delete shared;
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hr = shared->device->CreateTargetForHwnd(shared->dcompHwnd, TRUE, &shared->target);
|
|
|
|
|
if (FAILED(hr) || !shared->target) {
|
|
|
|
|
shared->device->Release();
|
|
|
|
|
DestroyWindow(shared->dcompHwnd);
|
|
|
|
|
delete shared;
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hr = shared->device->CreateVisual(&shared->rootVisual);
|
|
|
|
|
if (FAILED(hr) || !shared->rootVisual) {
|
|
|
|
|
shared->target->Release();
|
|
|
|
|
shared->device->Release();
|
|
|
|
|
DestroyWindow(shared->dcompHwnd);
|
|
|
|
|
delete shared;
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
shared->target->SetRoot(shared->rootVisual);
|
|
|
|
|
shared->device->Commit();
|
|
|
|
|
|
|
|
|
|
g_dcompShared[hwnd] = shared;
|
|
|
|
|
return shared;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void releaseDCompShared(HWND hwnd) {
|
|
|
|
|
auto it = g_dcompShared.find(hwnd);
|
|
|
|
|
if (it == g_dcompShared.end()) return;
|
|
|
|
|
DCompShared* shared = it->second;
|
|
|
|
|
shared->refCount--;
|
|
|
|
|
if (shared->refCount <= 0) {
|
|
|
|
|
if (shared->rootVisual) shared->rootVisual->Release();
|
|
|
|
|
if (shared->target) {
|
|
|
|
|
shared->target->SetRoot(nullptr);
|
|
|
|
|
shared->target->Release();
|
|
|
|
|
}
|
|
|
|
|
if (shared->device) shared->device->Release();
|
|
|
|
|
if (shared->dcompHwnd) {
|
|
|
|
|
g_dcompByHwnd.erase(shared->dcompHwnd);
|
|
|
|
|
KillTimer(shared->dcompHwnd, 1);
|
|
|
|
|
DestroyWindow(shared->dcompHwnd);
|
|
|
|
|
}
|
|
|
|
|
delete shared;
|
|
|
|
|
g_dcompShared.erase(it);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void createCompositionController(WinWebViewState* state) {
|
|
|
|
|
|
|
|
|
|
state->dcompShared = getOrCreateDCompShared(state->parentHwnd);
|
|
|
|
|
if (!state->dcompShared) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
HRESULT hr = state->dcompShared->device->CreateVisual(&state->dcompVisual);
|
|
|
|
|
if (FAILED(hr) || !state->dcompVisual) {
|
|
|
|
|
releaseDCompShared(state->parentHwnd);
|
|
|
|
|
state->dcompShared = nullptr;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
state->dcompVisual->SetOffsetX((float)state->x);
|
|
|
|
|
state->dcompVisual->SetOffsetY((float)state->y);
|
|
|
|
|
|
|
|
|
|
ICoreWebView2Environment3* env3;
|
|
|
|
|
hr = g_webview2Env->QueryInterface(IID_PPV_ARGS(&env3));
|
|
|
|
|
if (FAILED(hr) || !env3) {
|
|
|
|
|
state->dcompVisual->Release();
|
|
|
|
|
state->dcompVisual = nullptr;
|
|
|
|
|
releaseDCompShared(state->parentHwnd);
|
|
|
|
|
state->dcompShared = nullptr;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
env3->CreateCoreWebView2CompositionController(
|
|
|
|
|
state->dcompShared->dcompHwnd,
|
|
|
|
|
Callback<ICoreWebView2CreateCoreWebView2CompositionControllerCompletedHandler>(
|
|
|
|
|
[state](HRESULT result, ICoreWebView2CompositionController* compController) -> HRESULT {
|
2026-07-15 19:37:38 -07:00
|
|
|
if (state->destroyed) {
|
2026-07-22 20:58:04 -07:00
|
|
|
if (compController) compController->Release();
|
2026-07-15 19:37:38 -07:00
|
|
|
delete state;
|
|
|
|
|
return S_OK;
|
|
|
|
|
}
|
2026-07-22 20:58:04 -07:00
|
|
|
if (FAILED(result) || !compController) {
|
2026-07-15 19:37:38 -07:00
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-22 20:58:04 -07:00
|
|
|
state->compController = compController;
|
|
|
|
|
state->compController->AddRef();
|
2026-07-15 19:37:38 -07:00
|
|
|
|
2026-07-22 20:58:04 -07:00
|
|
|
HRESULT targetHr = compController->put_RootVisualTarget(state->dcompVisual);
|
|
|
|
|
|
|
|
|
|
state->dcompShared->rootVisual->AddVisual(state->dcompVisual, FALSE, nullptr);
|
|
|
|
|
state->dcompShared->device->Commit();
|
|
|
|
|
|
|
|
|
|
ICoreWebView2Controller* controller = nullptr;
|
|
|
|
|
HRESULT qiHr = compController->QueryInterface(IID_PPV_ARGS(&controller));
|
|
|
|
|
if (FAILED(qiHr) || !controller) {
|
|
|
|
|
compController->Release();
|
|
|
|
|
return qiHr;
|
2026-07-15 19:37:38 -07:00
|
|
|
}
|
|
|
|
|
|
2026-07-22 20:58:04 -07:00
|
|
|
onControllerReady(state, result, controller);
|
2026-07-15 19:37:38 -07:00
|
|
|
|
2026-07-22 20:58:04 -07:00
|
|
|
state->dcompShared->webviews.push_back(state);
|
2026-07-15 19:37:38 -07:00
|
|
|
|
2026-07-22 20:58:04 -07:00
|
|
|
state->compController->add_CursorChanged(
|
|
|
|
|
Callback<ICoreWebView2CursorChangedEventHandler>(
|
|
|
|
|
[state](ICoreWebView2CompositionController* sender, IUnknown* args) -> HRESULT {
|
|
|
|
|
HCURSOR cursor = nullptr;
|
|
|
|
|
HRESULT hr = sender->get_Cursor(&cursor);
|
|
|
|
|
if (SUCCEEDED(hr) && cursor) {
|
|
|
|
|
state->currentCursor = cursor;
|
|
|
|
|
}
|
|
|
|
|
return S_OK;
|
2026-07-15 19:37:38 -07:00
|
|
|
}).Get(),
|
2026-07-22 20:58:04 -07:00
|
|
|
&state->cursorChangedToken);
|
2026-07-15 19:37:38 -07:00
|
|
|
|
2026-07-22 20:58:04 -07:00
|
|
|
controller->Release();
|
|
|
|
|
compController->Release();
|
|
|
|
|
return S_OK;
|
|
|
|
|
}).Get());
|
|
|
|
|
|
|
|
|
|
env3->Release();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void createController(WinWebViewState* state) {
|
|
|
|
|
|
|
|
|
|
if (state->useComposition) {
|
|
|
|
|
createCompositionController(state);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-07-15 19:37:38 -07:00
|
|
|
|
2026-07-22 20:58:04 -07:00
|
|
|
g_webview2Env->CreateCoreWebView2Controller(
|
|
|
|
|
state->parentHwnd,
|
|
|
|
|
Callback<ICoreWebView2CreateCoreWebView2ControllerCompletedHandler>(
|
|
|
|
|
[state](HRESULT result, ICoreWebView2Controller* controller) -> HRESULT {
|
|
|
|
|
onControllerReady(state, result, controller);
|
2026-07-15 19:37:38 -07:00
|
|
|
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;
|
2026-07-22 20:58:04 -07:00
|
|
|
state->pendingShow = false;
|
|
|
|
|
state->dcompShared = nullptr;
|
|
|
|
|
state->dcompVisual = nullptr;
|
|
|
|
|
state->compController = nullptr;
|
|
|
|
|
state->useComposition = (wv->transparent && !state->isDetached);
|
2026-07-15 19:37:38 -07:00
|
|
|
state->messageToken = {0};
|
2026-07-22 20:58:04 -07:00
|
|
|
state->navStartingToken = {0};
|
2026-07-15 19:37:38 -07:00
|
|
|
state->navCompletedToken = {0};
|
|
|
|
|
state->errorToken = {0};
|
2026-07-22 20:58:04 -07:00
|
|
|
state->sourceChangedToken = {0};
|
|
|
|
|
state->contentLoadingToken = {0};
|
|
|
|
|
state->cursorChangedToken = {0};
|
|
|
|
|
state->currentCursor = nullptr;
|
2026-07-15 19:37:38 -07:00
|
|
|
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 {
|
|
|
|
|
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;
|
|
|
|
|
}
|
2026-07-22 20:58:04 -07:00
|
|
|
if (state->dcompShared) {
|
|
|
|
|
auto& wvs = state->dcompShared->webviews;
|
|
|
|
|
wvs.erase(std::remove(wvs.begin(), wvs.end(), state), wvs.end());
|
|
|
|
|
}
|
|
|
|
|
if (state->compController) {
|
|
|
|
|
if (state->cursorChangedToken.value) state->compController->remove_CursorChanged(state->cursorChangedToken);
|
|
|
|
|
state->compController->put_RootVisualTarget(nullptr);
|
|
|
|
|
state->compController->Release();
|
|
|
|
|
state->compController = nullptr;
|
|
|
|
|
}
|
|
|
|
|
if (state->dcompShared && state->dcompVisual) {
|
|
|
|
|
state->dcompShared->rootVisual->RemoveVisual(state->dcompVisual);
|
|
|
|
|
state->dcompShared->device->Commit();
|
|
|
|
|
state->dcompVisual->Release();
|
|
|
|
|
state->dcompVisual = nullptr;
|
|
|
|
|
}
|
|
|
|
|
if (state->dcompShared) {
|
|
|
|
|
releaseDCompShared(state->parentHwnd);
|
|
|
|
|
state->dcompShared = nullptr;
|
|
|
|
|
}
|
2026-07-15 19:37:38 -07:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (state->webview) {
|
|
|
|
|
if (state->messageToken.value) state->webview->remove_WebMessageReceived(state->messageToken);
|
2026-07-22 20:58:04 -07:00
|
|
|
if (state->navStartingToken.value) state->webview->remove_NavigationStarting(state->navStartingToken);
|
2026-07-15 19:37:38 -07:00
|
|
|
if (state->navCompletedToken.value) state->webview->remove_NavigationCompleted(state->navCompletedToken);
|
|
|
|
|
if (state->errorToken.value) state->webview->remove_ProcessFailed(state->errorToken);
|
2026-07-22 20:58:04 -07:00
|
|
|
if (state->sourceChangedToken.value) state->webview->remove_SourceChanged(state->sourceChangedToken);
|
|
|
|
|
if (state->contentLoadingToken.value) state->webview->remove_ContentLoading(state->contentLoadingToken);
|
2026-07-15 19:37:38 -07:00
|
|
|
state->webview->Release();
|
|
|
|
|
}
|
2026-07-22 20:58:04 -07:00
|
|
|
if (state->dcompShared) {
|
|
|
|
|
auto& wvs = state->dcompShared->webviews;
|
|
|
|
|
wvs.erase(std::remove(wvs.begin(), wvs.end(), state), wvs.end());
|
|
|
|
|
}
|
|
|
|
|
if (state->compController) {
|
|
|
|
|
if (state->cursorChangedToken.value) state->compController->remove_CursorChanged(state->cursorChangedToken);
|
|
|
|
|
state->compController->put_RootVisualTarget(nullptr);
|
|
|
|
|
state->compController->Release();
|
|
|
|
|
state->compController = nullptr;
|
|
|
|
|
}
|
|
|
|
|
if (state->dcompShared && state->dcompVisual) {
|
|
|
|
|
state->dcompShared->rootVisual->RemoveVisual(state->dcompVisual);
|
|
|
|
|
state->dcompShared->device->Commit();
|
|
|
|
|
state->dcompVisual->Release();
|
|
|
|
|
state->dcompVisual = nullptr;
|
|
|
|
|
}
|
2026-07-15 19:37:38 -07:00
|
|
|
if (state->controller) {
|
|
|
|
|
state->controller->Close();
|
|
|
|
|
state->controller->Release();
|
|
|
|
|
}
|
|
|
|
|
if (state->detachedHwnd) {
|
|
|
|
|
DestroyWindow(state->detachedHwnd);
|
|
|
|
|
}
|
2026-07-22 20:58:04 -07:00
|
|
|
if (state->dcompShared) {
|
|
|
|
|
releaseDCompShared(state->parentHwnd);
|
|
|
|
|
state->dcompShared = nullptr;
|
|
|
|
|
}
|
2026-07-15 19:37:38 -07:00
|
|
|
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;
|
|
|
|
|
}
|
2026-07-22 20:58:04 -07:00
|
|
|
std::wstring wHtml = utf8ToWide(html);
|
|
|
|
|
HRESULT navHr = state->webview->NavigateToString(wHtml.c_str());
|
2026-07-15 19:37:38 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
2026-07-22 20:58:04 -07:00
|
|
|
int len = WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, nullptr, 0, nullptr, nullptr);
|
|
|
|
|
if (len > 0) {
|
|
|
|
|
resultStr.resize(len - 1);
|
|
|
|
|
WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, &resultStr[0], len, nullptr, nullptr);
|
|
|
|
|
}
|
2026-07-15 19:37:38 -07:00
|
|
|
}
|
|
|
|
|
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);
|
2026-07-22 20:58:04 -07:00
|
|
|
} else {
|
|
|
|
|
state->pendingShow = true;
|
2026-07-15 19:37:38 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
2026-07-22 20:58:04 -07:00
|
|
|
} else if (state->useComposition) {
|
|
|
|
|
bounds.left = 0;
|
|
|
|
|
bounds.top = 0;
|
|
|
|
|
bounds.right = width;
|
|
|
|
|
bounds.bottom = height;
|
2026-07-15 19:37:38 -07:00
|
|
|
} else {
|
|
|
|
|
bounds.left = wv->x;
|
|
|
|
|
bounds.top = wv->y;
|
|
|
|
|
bounds.right = wv->x + width;
|
|
|
|
|
bounds.bottom = wv->y + height;
|
|
|
|
|
}
|
|
|
|
|
state->controller->put_Bounds(bounds);
|
2026-07-22 20:58:04 -07:00
|
|
|
|
|
|
|
|
if (state->dcompShared && state->dcompVisual) {
|
|
|
|
|
state->dcompVisual->SetOffsetX((float)wv->x);
|
|
|
|
|
state->dcompVisual->SetOffsetY((float)wv->y);
|
|
|
|
|
state->dcompShared->device->Commit();
|
|
|
|
|
}
|
2026-07-15 19:37:38 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
2026-07-22 20:58:04 -07:00
|
|
|
if (state->useComposition) {
|
|
|
|
|
bounds.left = 0;
|
|
|
|
|
bounds.top = 0;
|
|
|
|
|
bounds.right = wv->width;
|
|
|
|
|
bounds.bottom = wv->height;
|
|
|
|
|
} else {
|
|
|
|
|
bounds.left = x;
|
|
|
|
|
bounds.top = y;
|
|
|
|
|
bounds.right = x + wv->width;
|
|
|
|
|
bounds.bottom = y + wv->height;
|
|
|
|
|
}
|
2026-07-15 19:37:38 -07:00
|
|
|
state->controller->put_Bounds(bounds);
|
|
|
|
|
}
|
2026-07-22 20:58:04 -07:00
|
|
|
|
|
|
|
|
if (state->dcompShared && state->dcompVisual) {
|
|
|
|
|
state->dcompVisual->SetOffsetX((float)x);
|
|
|
|
|
state->dcompVisual->SetOffsetY((float)y);
|
|
|
|
|
state->dcompShared->device->Commit();
|
|
|
|
|
}
|
2026-07-15 19:37:38 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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() {
|
2026-07-22 20:58:04 -07:00
|
|
|
// COM event loop is by Kores loop via PeekMessageW/DispatchMessageW.
|
2026-07-15 19:37:38 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endif
|
|
|
|
|
#endif
|