#include "webview_cef_plugin.h" #include "webview_cef_keyevent.h" #ifdef WEBVIEW_CEF_GPU_TEXTURE #include "webview_cef_gpu_texture.h" #endif // This must be included before many other Windows headers. #include #include #include // For getPlatformVersion; remove unless needed for your plugin implementation. #include #include #include #include #include #include #include #include #include #include #include #include namespace webview_cef { class WebviewTextureRenderer : public WebviewTexture{ public: WebviewTextureRenderer(FlutterDesktopTextureRegistrarRef texture_registrar) { registrar_ = texture_registrar; texture = std::make_unique( flutter::PixelBufferTexture([this](size_t width, size_t height) -> const FlutterDesktopPixelBuffer* { return this->CopyPixelBuffer(width, height); })); FlutterDesktopTextureInfo info = {}; info.type = kFlutterDesktopPixelBufferTexture; info.pixel_buffer_config.user_data = std::get_if(texture.get()); info.pixel_buffer_config.callback = [](size_t width, size_t height, void* user_data) -> const FlutterDesktopPixelBuffer* { auto texture = static_cast(user_data); return texture->CopyPixelBuffer(width, height); }; textureId = FlutterDesktopTextureRegistrarRegisterExternalTexture(registrar_, &info); } virtual ~WebviewTextureRenderer() { std::lock_guard autolock(mutex_); if(registrar_){ // FlutterDesktopTextureRegistrarUnregisterExternalTexture(registrar_, textureId, nullptr, nullptr); } } const FlutterDesktopPixelBuffer *CopyPixelBuffer(size_t width, size_t height) const{ std::lock_guard autolock(mutex_); return pixel_buffer.get(); } virtual void onFrame(const void* buffer, int width, int height) override{ const std::lock_guard autolock(mutex_); if (!pixel_buffer.get() || pixel_buffer.get()->width != width || pixel_buffer.get()->height != height) { if (!pixel_buffer.get()) { pixel_buffer = std::make_unique(); pixel_buffer->release_context = nullptr; } pixel_buffer->width = width; pixel_buffer->height = height; const auto size = width * height * 4; backing_pixel_buffer.reset(new uint8_t[size]); pixel_buffer->buffer = backing_pixel_buffer.get(); } SwapBufferFromBgraToRgba((void*)pixel_buffer->buffer, buffer, width, height); if(registrar_){ FlutterDesktopTextureRegistrarMarkExternalTextureFrameAvailable(registrar_, textureId); } } FlutterDesktopTextureRegistrarRef registrar_; std::unique_ptr texture; mutable std::shared_ptr pixel_buffer; std::unique_ptr backing_pixel_buffer; mutable std::mutex mutex_; }; static flutter::EncodableValue encode_wvalue_to_flvalue(WValue* args) { // A null value (or a Null-typed WValue) maps to a null EncodableValue. // Note: EncodableValue(nullptr) must NOT be used — under C++20 it resolves // to std::string(const char*=nullptr) and crashes in strlen. if (args == nullptr) { return flutter::EncodableValue(); } WValueType type = webview_value_get_type(args); switch(type){ case Webview_Value_Type_Bool: return flutter::EncodableValue(webview_value_get_bool(args)); case Webview_Value_Type_Int: return flutter::EncodableValue(webview_value_get_int(args)); case Webview_Value_Type_Float: // flutter::EncodableValue has no scalar float alternative; C++20's // stricter std::variant rules no longer auto-promote float to double. return flutter::EncodableValue(static_cast(webview_value_get_float(args))); case Webview_Value_Type_Double: return flutter::EncodableValue(webview_value_get_double(args)); case Webview_Value_Type_String: return flutter::EncodableValue(webview_value_get_string(args)); case Webview_Value_Type_Uint8_List: { const uint8_t* data = webview_value_get_uint8_list(args); return flutter::EncodableValue(std::vector(data, data + webview_value_get_len(args))); } case Webview_Value_Type_Int32_List: { const int32_t* data = webview_value_get_int32_list(args); return flutter::EncodableValue(std::vector(data, data + webview_value_get_len(args))); } case Webview_Value_Type_Int64_List: { const int64_t* data = webview_value_get_int64_list(args); return flutter::EncodableValue(std::vector(data, data + webview_value_get_len(args))); } case Webview_Value_Type_Float_List: { const float* data = webview_value_get_float_list(args); return flutter::EncodableValue(std::vector(data, data + webview_value_get_len(args))); } case Webview_Value_Type_Double_List: { const double* data = webview_value_get_double_list(args); return flutter::EncodableValue(std::vector(data, data + webview_value_get_len(args))); } case Webview_Value_Type_List: { flutter::EncodableList ret; size_t len = webview_value_get_len(args); for (size_t i = 0; i < len; i++) { ret.push_back(encode_wvalue_to_flvalue(webview_value_get_list_value(args, i))); } return ret; } case Webview_Value_Type_Map: { flutter::EncodableMap ret; size_t len = webview_value_get_len(args); for (size_t i = 0; i < len; i++) { ret[encode_wvalue_to_flvalue(webview_value_get_key(args, i))] = encode_wvalue_to_flvalue(webview_value_get_value(args, i)); } return ret; } default: return flutter::EncodableValue(); } } static WValue *encode_flvalue_to_wvalue(flutter::EncodableValue* args) { size_t index = args->index(); if (index == 1) { return webview_value_new_bool(*std::get_if(args)); } else if (index == 2 || index == 3) { return webview_value_new_int(*std::get_if(args)); } else if (index == 4) { return webview_value_new_double(*std::get_if(args)); } else if (index == 5) { return webview_value_new_string((*std::get_if(args)).c_str()); } else if (index == 6) { auto list = *std::get_if>(args); return webview_value_new_uint8_list(list.data(), list.size()); } else if (index == 7) { auto list = *std::get_if>(args); return webview_value_new_int32_list(list.data(), list.size()); } else if (index == 8) { auto list = *std::get_if>(args); return webview_value_new_int64_list(list.data(), list.size()); } else if (index == 9) { auto list = *std::get_if>(args); return webview_value_new_double_list(list.data(), list.size()); } else if (index == 10) { WValue * ret = webview_value_new_list(); flutter::EncodableList list = *std::get_if(args); for (size_t i = 0; i < list.size(); i++) { WValue *value = encode_flvalue_to_wvalue(&list[i]); webview_value_append(ret, value); webview_value_unref(value); } return ret; } else if (index == 11) { WValue * ret = webview_value_new_map(); flutter::EncodableMap map = *std::get_if(args); for (flutter::EncodableMap::iterator it = map.begin(); it != map.end(); it++) { WValue *key = encode_flvalue_to_wvalue(const_cast(&it->first)); WValue *value = encode_flvalue_to_wvalue(const_cast(&it->second)); webview_value_set(ret, key, value); webview_value_unref(key); webview_value_unref(value); } return ret; } else if (index == 12) { return nullptr; } else if (index == 13) { auto list = *std::get_if>(args); return webview_value_new_float_list(list.data(), list.size()); } return nullptr; } std::unordered_map> webviewPlugins; std::unordered_map> webviewChannels; // Guards webviewPlugins against the vsync driver thread's snapshot reads. static std::mutex g_pluginsMutex; #ifdef WEBVIEW_CEF_GPU_TEXTURE // ---- Vsync-driven external BeginFrame ----------------------------------- // With external_begin_frame_enabled the browser only produces a frame when // SendExternalBeginFrame is called. We tick it on every display vblank so the // webview's frame rate follows the monitor's actual refresh rate (adaptive, // not capped at 60). WaitForVBlank blocks until the next vblank and tracks // the current refresh rate automatically. static std::thread g_vsyncThread; static std::atomic g_vsyncRunning{false}; static Microsoft::WRL::ComPtr AcquirePrimaryDxgiOutput() { Microsoft::WRL::ComPtr factory; if (FAILED(CreateDXGIFactory1(IID_PPV_ARGS(&factory)))) return nullptr; Microsoft::WRL::ComPtr adapter; if (FAILED(factory->EnumAdapters1(0, &adapter))) return nullptr; Microsoft::WRL::ComPtr output; if (FAILED(adapter->EnumOutputs(0, &output))) return nullptr; return output; } static UINT QueryRefreshIntervalMs() { DEVMODE dm = {}; dm.dmSize = sizeof(dm); if (EnumDisplaySettings(nullptr, ENUM_CURRENT_SETTINGS, &dm) && dm.dmDisplayFrequency > 1) { return (std::max)(UINT(1), UINT(1000 / dm.dmDisplayFrequency)); } return 16; // assume ~60 Hz } static void VsyncThreadProc() { Microsoft::WRL::ComPtr output = AcquirePrimaryDxgiOutput(); const UINT fallbackMs = QueryRefreshIntervalMs(); while (g_vsyncRunning) { if (output) { if (FAILED(output->WaitForVBlank())) { output.Reset(); } } if (!output) { std::this_thread::sleep_for(std::chrono::milliseconds(fallbackMs)); output = AcquirePrimaryDxgiOutput(); } std::vector> snapshot; { std::lock_guard lock(g_pluginsMutex); snapshot.reserve(webviewPlugins.size()); for (auto& kv : webviewPlugins) snapshot.push_back(kv.second); } for (auto& p : snapshot) { if (p) p->tickBeginFrame(); } } } static void StartVsyncDriver() { bool expected = false; if (g_vsyncRunning.compare_exchange_strong(expected, true)) { g_vsyncThread = std::thread(VsyncThreadProc); } } static void StopVsyncDriver() { if (g_vsyncRunning.exchange(false)) { if (g_vsyncThread.joinable()) g_vsyncThread.join(); } } #endif // WEBVIEW_CEF_GPU_TEXTURE static constexpr UINT_PTR kImeSubclassId = 1; // JJ_MKVMaker: the CEF runtime is downloaded only when the user picks the built-in Chrome // engine (\cef\libcef.dll). Without it, do nothing that affects the rest of the app // (no vsync thread, no IME subclass). static bool CefRuntimePresent() { wchar_t buf[MAX_PATH * 4]; const DWORD n = GetModuleFileNameW(nullptr, buf, static_cast(sizeof(buf) / sizeof(buf[0]))); std::wstring dir(buf, n); dir = dir.substr(0, dir.find_last_of(L"\\/")); return GetFileAttributesW((dir + L"\\cef\\libcef.dll").c_str()) != INVALID_FILE_ATTRIBUTES; } // The OS IME must be intercepted in the window procedure BEFORE DefWindowProc // runs, otherwise the default handling consumes/converts the result string and // shows its own composition window. We can't do that from the runner's // post-DispatchMessage hook, so the Flutter view HWND is subclassed here. static LRESULT CALLBACK ImeSubclassProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam, UINT_PTR, DWORD_PTR) { // JJ_MKVMaker: only take over IME while an editable node inside a CEF browser has focus. // Otherwise (Flutter's own text fields) leave IME to Flutter, so Korean / Japanese // composition keeps working everywhere else in the app. { auto it = webviewPlugins.find(hwnd); const bool cefEditing = it != webviewPlugins.end() && it->second->isEditableFocused(); if (!cefEditing && (message == WM_IME_SETCONTEXT || message == WM_IME_STARTCOMPOSITION || message == WM_IME_COMPOSITION || message == WM_IME_ENDCOMPOSITION)) { return DefSubclassProc(hwnd, message, wparam, lparam); } } switch (message) { case WM_IME_SETCONTEXT: // Don't let the OS draw its own composition window; the preedit is // rendered inside the web page via ImeSetComposition. lparam &= ~ISC_SHOWUICOMPOSITIONWINDOW; return DefSubclassProc(hwnd, message, wparam, lparam); case WM_IME_STARTCOMPOSITION: // Suppress default composition UI; we drive composition ourselves. return 0; case WM_IME_COMPOSITION: { auto pit = webviewPlugins.find(hwnd); if (pit == webviewPlugins.end() || !pit->second->isEditableFocused()) { return DefSubclassProc(hwnd, message, wparam, lparam); } HIMC imc = ImmGetContext(hwnd); if (imc) { // Committed result -> commit it to the focused browser. if (lparam & GCS_RESULTSTR) { LONG bytes = ImmGetCompositionStringW(imc, GCS_RESULTSTR, nullptr, 0); if (bytes > 0) { std::wstring ws(bytes / sizeof(wchar_t), L'\0'); ImmGetCompositionStringW(imc, GCS_RESULTSTR, &ws[0], bytes); pit->second->imeCommitTextNative(ws); } } // Ongoing preedit -> set composition (real-time on-screen text). if (lparam & GCS_COMPSTR) { LONG bytes = ImmGetCompositionStringW(imc, GCS_COMPSTR, nullptr, 0); if (bytes > 0) { std::wstring ws(bytes / sizeof(wchar_t), L'\0'); ImmGetCompositionStringW(imc, GCS_COMPSTR, &ws[0], bytes); int cursor = static_cast( ImmGetCompositionStringW(imc, GCS_CURSORPOS, nullptr, 0)); pit->second->imeSetCompositionNative(ws, cursor); } } ImmReleaseContext(hwnd, imc); } // Consume: prevent DefWindowProc from showing the default IME window or // generating duplicate WM_IME_CHAR/WM_CHAR for the committed text. return 0; } case WM_IME_ENDCOMPOSITION: { auto pit = webviewPlugins.find(hwnd); if (pit != webviewPlugins.end()) { pit->second->imeFinishCompositionNative(); } return DefSubclassProc(hwnd, message, wparam, lparam); } } return DefSubclassProc(hwnd, message, wparam, lparam); } void WebviewCefPlugin::RegisterWithRegistrar(FlutterDesktopPluginRegistrarRef registrar) { auto plugin = std::make_unique(); plugin->m_textureRegistrar = FlutterDesktopRegistrarGetTextureRegistrar(registrar); flutter::PluginRegistrarWindows *window_registrar = flutter::PluginRegistrarManager::GetInstance() ->GetRegistrar(registrar); plugin->m_channel = std::make_unique>( window_registrar->messenger(), "webview_cef", &flutter::StandardMethodCodec::GetInstance()); plugin->m_channel->SetMethodCallHandler( [plugin_pointer = plugin.get()](const auto &call, auto result) { plugin_pointer->HandleMethodCall(call, std::move(result)); }); plugin->m_hwnd = FlutterDesktopViewGetHWND(FlutterDesktopPluginRegistrarGetView(registrar)); { std::lock_guard lock(g_pluginsMutex); webviewPlugins.emplace(plugin->m_hwnd, plugin->m_plugin); } if (CefRuntimePresent()) { #ifdef WEBVIEW_CEF_GPU_TEXTURE // Begin ticking external BeginFrame on every vblank (drives GPU frames). StartVsyncDriver(); #endif // Subclass the Flutter view window to intercept WM_IME_* before the engine // and DefWindowProc handle them (required for correct CJK composition/commit). SetWindowSubclass(plugin->m_hwnd, ImeSubclassProc, kImeSubclassId, 0); } webviewChannels.emplace(plugin->m_hwnd, [plugin_pointer = plugin.get()](std::string method, flutter::EncodableValue* arguments) { plugin_pointer->m_channel->InvokeMethod(method, std::make_unique(*arguments)); }); plugin->m_plugin->setInvokeMethodFunc([plugin_pointer = plugin.get()](std::string method, WValue* arguments) { flutter::EncodableValue* methodValue = new flutter::EncodableValue(method); flutter::EncodableValue* args = new flutter::EncodableValue(encode_wvalue_to_flvalue(arguments)); PostMessage(plugin_pointer->m_hwnd, WM_USER + 1, WPARAM(methodValue), LPARAM(args)); }); plugin->m_plugin->setCreateTextureFunc([plugin_pointer = plugin.get()]() { #ifdef WEBVIEW_CEF_GPU_TEXTURE // Zero-copy GPU path: CEF OnAcceleratedPaint shared texture -> Flutter // D3D11 surface texture. Falls back to the software pixel-buffer path // only if the D3D11 device could not be created. auto gpu = std::make_shared(plugin_pointer->m_textureRegistrar); if (gpu->isValid()) { return std::dynamic_pointer_cast(gpu); } #endif std::shared_ptr renderer = std::make_shared(plugin_pointer->m_textureRegistrar); return std::dynamic_pointer_cast(renderer); }); window_registrar->AddPlugin(std::move(plugin)); } WebviewCefPlugin::WebviewCefPlugin() { m_plugin = std::make_shared(); } WebviewCefPlugin::~WebviewCefPlugin() { m_plugin = nullptr; RemoveWindowSubclass(m_hwnd, ImeSubclassProc, kImeSubclassId); bool nowEmpty = false; { std::lock_guard lock(g_pluginsMutex); webviewPlugins.erase(m_hwnd); nowEmpty = webviewPlugins.empty(); } webviewChannels.erase(m_hwnd); if(nowEmpty){ #ifdef WEBVIEW_CEF_GPU_TEXTURE // Stop the vsync thread before shutting CEF down. Must not hold // g_pluginsMutex here: the thread takes it while snapshotting. StopVsyncDriver(); #endif webview_cef::stopCEF(); } } void WebviewCefPlugin::HandleMethodCall( const flutter::MethodCall& method_call, std::shared_ptr> result) { WValue *encodeArgs = encode_flvalue_to_wvalue(const_cast(method_call.arguments())); m_plugin->HandleMethodCall(method_call.method_name(), encodeArgs, [=](int ret, WValue* args){ if (ret > 0){ result->Success(encode_wvalue_to_flvalue(args)); } else if (ret < 0){ result->Error("error", "error", encode_wvalue_to_flvalue(args)); } else{ result->NotImplemented(); } }); webview_value_unref(encodeArgs); } void WebviewCefPlugin::handleMessageProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) { switch (message) { case WM_USER + 1: { // These were heap-allocated in setInvokeMethodFunc; take ownership and // free them after dispatch (InvokeMethod copies the arguments). flutter::EncodableValue *method = (flutter::EncodableValue *)wparam; flutter::EncodableValue *args = (flutter::EncodableValue *)lparam; if (webviewPlugins.find(hwnd) != webviewPlugins.end()) { webviewChannels[hwnd]((*std::get_if(method)), args); } delete method; delete args; break; } // WM_IME_* are handled in ImeSubclassProc (before DefWindowProc), not here. case WM_SYSCHAR: case WM_SYSKEYDOWN: case WM_SYSKEYUP: case WM_KEYDOWN: case WM_KEYUP: case WM_CHAR: { if (webviewPlugins.find(hwnd) != webviewPlugins.end()) { CefKeyEvent keyEvent = getCefKeyEvent(message, wparam, lparam); webviewPlugins[hwnd]->sendKeyEvent(keyEvent); } break; } } } } // namespace webview_cef