From 607679c0de46eee0ab38b89a142d2652afb8065d Mon Sep 17 00:00:00 2001 From: David Botton Date: Thu, 1 Feb 2024 09:42:24 -0500 Subject: [PATCH] Update to latest webview.h for clogframe --- clogframe/webview.h | 2188 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 1813 insertions(+), 375 deletions(-) diff --git a/clogframe/webview.h b/clogframe/webview.h index f9ae113..4142354 100644 --- a/clogframe/webview.h +++ b/clogframe/webview.h @@ -26,8 +26,82 @@ #define WEBVIEW_H #ifndef WEBVIEW_API +#if defined(WEBVIEW_SHARED) || defined(WEBVIEW_BUILD_SHARED) +#if defined(_WIN32) || defined(__CYGWIN__) +#if defined(WEBVIEW_BUILD_SHARED) +#define WEBVIEW_API __declspec(dllexport) +#else +#define WEBVIEW_API __declspec(dllimport) +#endif +#else +#define WEBVIEW_API __attribute__((visibility("default"))) +#endif +#elif !defined(WEBVIEW_STATIC) && defined(__cplusplus) +#define WEBVIEW_API inline +#else #define WEBVIEW_API extern #endif +#endif + +#ifndef WEBVIEW_VERSION_MAJOR +// The current library major version. +#define WEBVIEW_VERSION_MAJOR 0 +#endif + +#ifndef WEBVIEW_VERSION_MINOR +// The current library minor version. +#define WEBVIEW_VERSION_MINOR 11 +#endif + +#ifndef WEBVIEW_VERSION_PATCH +// The current library patch version. +#define WEBVIEW_VERSION_PATCH 0 +#endif + +#ifndef WEBVIEW_VERSION_PRE_RELEASE +// SemVer 2.0.0 pre-release labels prefixed with "-". +#define WEBVIEW_VERSION_PRE_RELEASE "" +#endif + +#ifndef WEBVIEW_VERSION_BUILD_METADATA +// SemVer 2.0.0 build metadata prefixed with "+". +#define WEBVIEW_VERSION_BUILD_METADATA "" +#endif + +// Utility macro for stringifying a macro argument. +#define WEBVIEW_STRINGIFY(x) #x + +// Utility macro for stringifying the result of a macro argument expansion. +#define WEBVIEW_EXPAND_AND_STRINGIFY(x) WEBVIEW_STRINGIFY(x) + +// SemVer 2.0.0 version number in MAJOR.MINOR.PATCH format. +#define WEBVIEW_VERSION_NUMBER \ + WEBVIEW_EXPAND_AND_STRINGIFY(WEBVIEW_VERSION_MAJOR) \ + "." WEBVIEW_EXPAND_AND_STRINGIFY( \ + WEBVIEW_VERSION_MINOR) "." WEBVIEW_EXPAND_AND_STRINGIFY(WEBVIEW_VERSION_PATCH) + +// Holds the elements of a MAJOR.MINOR.PATCH version number. +typedef struct { + // Major version. + unsigned int major; + // Minor version. + unsigned int minor; + // Patch version. + unsigned int patch; +} webview_version_t; + +// Holds the library's version information. +typedef struct { + // The elements of the version number. + webview_version_t version; + // SemVer 2.0.0 version number in MAJOR.MINOR.PATCH format. + char version_number[32]; + // SemVer 2.0.0 pre-release labels prefixed with "-" if specified, otherwise + // an empty string. + char pre_release[48]; + // SemVer 2.0.0 build metadata prefixed with "+", otherwise an empty string. + char build_metadata[48]; +} webview_version_info_t; #ifdef __cplusplus extern "C" { @@ -36,7 +110,7 @@ extern "C" { typedef void *webview_t; // Creates a new webview instance. If debug is non-zero - developer tools will -// be enabled (if the platform supports them). Window parameter can be a +// be enabled (if the platform supports them). The window parameter can be a // pointer to the native window handle. If it's non-null - then child WebView // is embedded into the given parent window. Otherwise a new window is created. // Depending on the platform, a GtkWindow, NSWindow or HWND pointer can be @@ -61,9 +135,9 @@ WEBVIEW_API void webview_terminate(webview_t w); WEBVIEW_API void webview_dispatch(webview_t w, void (*fn)(webview_t w, void *arg), void *arg); -// Returns a native window handle pointer. When using GTK backend the pointer -// is GtkWindow pointer, when using Cocoa backend the pointer is NSWindow -// pointer, when using Win32 backend the pointer is HWND pointer. +// Returns a native window handle pointer. When using a GTK backend the pointer +// is a GtkWindow pointer, when using a Cocoa backend the pointer is a NSWindow +// pointer, when using a Win32 backend the pointer is a HWND pointer. WEBVIEW_API void *webview_get_window(webview_t w); // Updates the title of the native window. Must be called from the UI thread. @@ -74,7 +148,7 @@ WEBVIEW_API void webview_set_title(webview_t w, const char *title); #define WEBVIEW_HINT_MIN 1 // Width and height are minimum bounds #define WEBVIEW_HINT_MAX 2 // Width and height are maximum bounds #define WEBVIEW_HINT_FIXED 3 // Window size can not be changed by a user -// Updates native window size. See WEBVIEW_HINT constants. +// Updates the size of the native window. See WEBVIEW_HINT constants. WEBVIEW_API void webview_set_size(webview_t w, int width, int height, int hints); @@ -90,7 +164,7 @@ WEBVIEW_API void webview_navigate(webview_t w, const char *url); WEBVIEW_API void webview_set_html(webview_t w, const char *html); // Injects JavaScript code at the initialization of the new page. Every time -// the webview will open a the new page - this initialization code will be +// the webview will open a new page - this initialization code will be // executed. It is guaranteed that code is executed before window.onload. WEBVIEW_API void webview_init(webview_t w, const char *js); @@ -100,10 +174,10 @@ WEBVIEW_API void webview_init(webview_t w, const char *js); WEBVIEW_API void webview_eval(webview_t w, const char *js); // Binds a native C callback so that it will appear under the given name as a -// global JavaScript function. Internally it uses webview_init(). Callback -// receives a request string and a user-provided argument pointer. Request -// string is a JSON array of all the arguments passed to the JavaScript -// function. +// global JavaScript function. Internally it uses webview_init(). The callback +// receives a sequential request id, a request string and a user-provided +// argument pointer. The request string is a JSON array of all the arguments +// passed to the JavaScript function. WEBVIEW_API void webview_bind(webview_t w, const char *name, void (*fn)(const char *seq, const char *req, void *arg), @@ -112,13 +186,18 @@ WEBVIEW_API void webview_bind(webview_t w, const char *name, // Removes a native C callback that was previously set by webview_bind. WEBVIEW_API void webview_unbind(webview_t w, const char *name); -// Allows to return a value from the native binding. Original request pointer -// must be provided to help internal RPC engine match requests with responses. -// If status is zero - result is expected to be a valid JSON result value. -// If status is not zero - result is an error JSON object. +// Responds to a binding call from the JS side. The ID/sequence number must +// match the value passed to the binding handler in order to respond to the +// call and complete the promise on the JS side. A status of zero resolves +// the promise, and any other value rejects it. The result must either be a +// valid JSON value or an empty string for the primitive JS value "undefined". WEBVIEW_API void webview_return(webview_t w, const char *seq, int status, const char *result); +// Get the library's version information. +// @since 0.10 +WEBVIEW_API const webview_version_info_t *webview_version(void); + #ifdef __cplusplus } @@ -151,8 +230,10 @@ WEBVIEW_API void webview_return(webview_t w, const char *seq, int status, WEBVIEW_DEPRECATED("Private API should not be used") #endif +#include #include #include +#include #include #include #include @@ -162,12 +243,78 @@ WEBVIEW_API void webview_return(webview_t w, const char *seq, int status, #include +#if defined(_WIN32) +#define WIN32_LEAN_AND_MEAN +#include +#else +#include +#endif + namespace webview { using dispatch_fn_t = std::function; namespace detail { +// The library's version information. +constexpr const webview_version_info_t library_version_info{ + {WEBVIEW_VERSION_MAJOR, WEBVIEW_VERSION_MINOR, WEBVIEW_VERSION_PATCH}, + WEBVIEW_VERSION_NUMBER, + WEBVIEW_VERSION_PRE_RELEASE, + WEBVIEW_VERSION_BUILD_METADATA}; + +#if defined(_WIN32) +// Converts a narrow (UTF-8-encoded) string into a wide (UTF-16-encoded) string. +inline std::wstring widen_string(const std::string &input) { + if (input.empty()) { + return std::wstring(); + } + UINT cp = CP_UTF8; + DWORD flags = MB_ERR_INVALID_CHARS; + auto input_c = input.c_str(); + auto input_length = static_cast(input.size()); + auto required_length = + MultiByteToWideChar(cp, flags, input_c, input_length, nullptr, 0); + if (required_length > 0) { + std::wstring output(static_cast(required_length), L'\0'); + if (MultiByteToWideChar(cp, flags, input_c, input_length, &output[0], + required_length) > 0) { + return output; + } + } + // Failed to convert string from UTF-8 to UTF-16 + return std::wstring(); +} + +// Converts a wide (UTF-16-encoded) string into a narrow (UTF-8-encoded) string. +inline std::string narrow_string(const std::wstring &input) { + struct wc_flags { + enum TYPE : unsigned int { + // WC_ERR_INVALID_CHARS + err_invalid_chars = 0x00000080U + }; + }; + if (input.empty()) { + return std::string(); + } + UINT cp = CP_UTF8; + DWORD flags = wc_flags::err_invalid_chars; + auto input_c = input.c_str(); + auto input_length = static_cast(input.size()); + auto required_length = WideCharToMultiByte(cp, flags, input_c, input_length, + nullptr, 0, nullptr, nullptr); + if (required_length > 0) { + std::string output(static_cast(required_length), '\0'); + if (WideCharToMultiByte(cp, flags, input_c, input_length, &output[0], + required_length, nullptr, nullptr) > 0) { + return output; + } + } + // Failed to convert string from UTF-16 to UTF-8 + return std::string(); +} +#endif + inline int json_parse_c(const char *s, size_t sz, const char *key, size_t keysz, const char **value, size_t *valuesz) { enum { @@ -177,19 +324,22 @@ inline int json_parse_c(const char *s, size_t sz, const char *key, size_t keysz, JSON_STATE_ESCAPE, JSON_STATE_UTF8 } state = JSON_STATE_VALUE; - const char *k = NULL; + const char *k = nullptr; int index = 1; int depth = 0; int utf8_bytes = 0; - if (key == NULL) { - index = keysz; + *value = nullptr; + *valuesz = 0; + + if (key == nullptr) { + index = static_cast(keysz); + if (index < 0) { + return -1; + } keysz = 0; } - *value = NULL; - *valuesz = 0; - for (; sz > 0; s++, sz--) { enum { JSON_ACTION_NONE, @@ -198,7 +348,7 @@ inline int json_parse_c(const char *s, size_t sz, const char *key, size_t keysz, JSON_ACTION_START_STRUCT, JSON_ACTION_END_STRUCT } action = JSON_ACTION_NONE; - unsigned char c = *s; + auto c = static_cast(*s); switch (state) { case JSON_STATE_VALUE: if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == ',' || @@ -286,16 +436,16 @@ inline int json_parse_c(const char *s, size_t sz, const char *key, size_t keysz, } } else if (action == JSON_ACTION_END || action == JSON_ACTION_END_STRUCT) { - if (*value != NULL && index == 0) { + if (*value != nullptr && index == 0) { *valuesz = (size_t)(s + 1 - *value); return 0; - } else if (keysz > 0 && k != NULL) { + } else if (keysz > 0 && k != nullptr) { if (keysz == (size_t)(s - k - 1) && memcmp(key, k + 1, keysz) == 0) { index = 0; } else { index = 2; } - k = NULL; + k = nullptr; } } } @@ -307,9 +457,63 @@ inline int json_parse_c(const char *s, size_t sz, const char *key, size_t keysz, return -1; } -inline std::string json_escape(const std::string &s) { - // TODO: implement - return '"' + s + '"'; +constexpr bool is_json_special_char(unsigned int c) { + return c == '"' || c == '\\'; +} + +constexpr bool is_control_char(unsigned int c) { + return c <= 0x1f || (c >= 0x7f && c <= 0x9f); +} + +inline std::string json_escape(const std::string &s, bool add_quotes = true) { + constexpr char hex_alphabet[]{"0123456789abcdef"}; + // Calculate the size of the resulting string. + // Add space for the double quotes. + auto required_length = s.size() + (add_quotes ? 2 : 0); + for (auto c : s) { + auto uc = static_cast(c); + if (is_json_special_char(uc)) { + // '\' and a single following character + required_length += 2; + continue; + } + if (is_control_char(uc)) { + // '\', 'u', 4 digits + required_length += 6; + continue; + } + ++required_length; + } + // Allocate memory for resulting string only once. + std::string result; + result.reserve(required_length); + if (add_quotes) { + result += '"'; + } + // Copy string while escaping characters. + for (auto c : s) { + auto uc = static_cast(c); + if (is_json_special_char(uc)) { + result += '\\'; + result += c; + continue; + } + if (is_control_char(uc)) { + auto h = (uc >> 4) & 0x0f; + auto l = uc & 0x0f; + result += "\\u00"; + // NOLINTBEGIN(cppcoreguidelines-pro-bounds-constant-array-index) + result += hex_alphabet[h]; + result += hex_alphabet[l]; + // NOLINTEND(cppcoreguidelines-pro-bounds-constant-array-index) + continue; + } + result += c; + } + if (add_quotes) { + result += '"'; + } + return result; } inline int json_unescape(const char *s, size_t n, char *out) { @@ -351,7 +555,7 @@ inline int json_unescape(const char *s, size_t n, char *out) { return -1; } } - if (out != NULL) { + if (out != nullptr) { *out++ = c; } s++; @@ -361,7 +565,7 @@ inline int json_unescape(const char *s, size_t n, char *out) { if (*s != '"') { return -1; } - if (out != NULL) { + if (out != nullptr) { *out = '\0'; } return r; @@ -371,7 +575,7 @@ inline std::string json_parse(const std::string &s, const std::string &key, const int index) { const char *value; size_t value_sz; - if (key == "") { + if (key.empty()) { json_parse_c(s.c_str(), s.length(), nullptr, index, &value, &value_sz); } else { json_parse_c(s.c_str(), s.length(), key.c_str(), key.length(), &value, @@ -379,7 +583,7 @@ inline std::string json_parse(const std::string &s, const std::string &key, } if (value != nullptr) { if (value[0] != '"') { - return std::string(value, value_sz); + return {value, value_sz}; } int n = json_unescape(value, value_sz, nullptr); if (n > 0) { @@ -393,6 +597,117 @@ inline std::string json_parse(const std::string &s, const std::string &key, return ""; } +// Holds a symbol name and associated type for code clarity. +template class library_symbol { +public: + using type = T; + + constexpr explicit library_symbol(const char *name) : m_name(name) {} + constexpr const char *get_name() const { return m_name; } + +private: + const char *m_name; +}; + +// Loads a native shared library and allows one to get addresses for those +// symbols. +class native_library { +public: + native_library() = default; + + explicit native_library(const std::string &name) + : m_handle{load_library(name)} {} + +#ifdef _WIN32 + explicit native_library(const std::wstring &name) + : m_handle{load_library(name)} {} +#endif + + ~native_library() { + if (m_handle) { +#ifdef _WIN32 + FreeLibrary(m_handle); +#else + dlclose(m_handle); +#endif + m_handle = nullptr; + } + } + + native_library(const native_library &other) = delete; + native_library &operator=(const native_library &other) = delete; + native_library(native_library &&other) = default; + native_library &operator=(native_library &&other) = default; + + // Returns true if the library is currently loaded; otherwise false. + operator bool() const { return is_loaded(); } + + // Get the address for the specified symbol or nullptr if not found. + template + typename Symbol::type get(const Symbol &symbol) const { + if (is_loaded()) { + // NOLINTBEGIN(cppcoreguidelines-pro-type-reinterpret-cast) +#ifdef _WIN32 +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wcast-function-type" +#endif + return reinterpret_cast( + GetProcAddress(m_handle, symbol.get_name())); +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif +#else + return reinterpret_cast( + dlsym(m_handle, symbol.get_name())); +#endif + // NOLINTEND(cppcoreguidelines-pro-type-reinterpret-cast) + } + return nullptr; + } + + // Returns true if the library is currently loaded; otherwise false. + bool is_loaded() const { return !!m_handle; } + + void detach() { m_handle = nullptr; } + + // Returns true if the library by the given name is currently loaded; otherwise false. + static inline bool is_loaded(const std::string &name) { +#ifdef _WIN32 + auto handle = GetModuleHandleW(widen_string(name).c_str()); +#else + auto handle = dlopen(name.c_str(), RTLD_NOW | RTLD_NOLOAD); + if (handle) { + dlclose(handle); + } +#endif + return !!handle; + } + +private: +#ifdef _WIN32 + using mod_handle_t = HMODULE; +#else + using mod_handle_t = void *; +#endif + + static inline mod_handle_t load_library(const std::string &name) { +#ifdef _WIN32 + return load_library(widen_string(name)); +#else + return dlopen(name.c_str(), RTLD_NOW); +#endif + } + +#ifdef _WIN32 + static inline mod_handle_t load_library(const std::wstring &name) { + return LoadLibraryW(name.c_str()); + } +#endif + + mod_handle_t m_handle{}; +}; + } // namespace detail WEBVIEW_DEPRECATED_PRIVATE @@ -430,29 +745,158 @@ inline std::string json_parse(const std::string &s, const std::string &key, // // ==================================================================== // +#include + #include #include #include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include +#include + namespace webview { namespace detail { +// Namespace containing workaround for WebKit 2.42 when using NVIDIA GPU +// driver. +// See WebKit bug: https://bugs.webkit.org/show_bug.cgi?id=261874 +// Please remove all of the code in this namespace when it's no longer needed. +namespace webkit_dmabuf { + +// Get environment variable. Not thread-safe. +static inline std::string get_env(const std::string &name) { + auto *value = std::getenv(name.c_str()); + if (value) { + return {value}; + } + return {}; +} + +// Set environment variable. Not thread-safe. +static inline void set_env(const std::string &name, const std::string &value) { + ::setenv(name.c_str(), value.c_str(), 1); +} + +// Checks whether the NVIDIA GPU driver is used based on whether the kernel +// module is loaded. +static inline bool is_using_nvidia_driver() { + struct ::stat buffer; + if (::stat("/sys/module/nvidia", &buffer) != 0) { + return false; + } + return S_ISDIR(buffer.st_mode); +} + +// Checks whether the windowing system is Wayland. +static inline bool is_wayland_display() { + if (!get_env("WAYLAND_DISPLAY").empty()) { + return true; + } + if (get_env("XDG_SESSION_TYPE") == "wayland") { + return true; + } + if (get_env("DESKTOP_SESSION").find("wayland") != std::string::npos) { + return true; + } + return false; +} + +// Checks whether the GDK X11 backend is used. +// See: https://docs.gtk.org/gdk3/class.DisplayManager.html +static inline bool is_gdk_x11_backend() { +#ifdef GDK_WINDOWING_X11 + auto *manager = gdk_display_manager_get(); + auto *display = gdk_display_manager_get_default_display(manager); + return GDK_IS_X11_DISPLAY(display); +#else + return false; +#endif +} + +// Checks whether WebKit is affected by bug when using DMA-BUF renderer. +// Returns true if all of the following conditions are met: +// - WebKit version is >= 2.42 (please narrow this down when there's a fix). +// - Environment variables are empty or not set: +// - WEBKIT_DISABLE_DMABUF_RENDERER +// - Windowing system is not Wayland. +// - GDK backend is X11. +// - NVIDIA GPU driver is used. +static inline bool is_webkit_dmabuf_bugged() { + auto wk_major = webkit_get_major_version(); + auto wk_minor = webkit_get_minor_version(); + // TODO: Narrow down affected WebKit version when there's a fixed version + auto is_affected_wk_version = wk_major == 2 && wk_minor >= 42; + if (!is_affected_wk_version) { + return false; + } + if (!get_env("WEBKIT_DISABLE_DMABUF_RENDERER").empty()) { + return false; + } + if (is_wayland_display()) { + return false; + } + if (!is_gdk_x11_backend()) { + return false; + } + if (!is_using_nvidia_driver()) { + return false; + } + return true; +} + +// Applies workaround for WebKit DMA-BUF bug if needed. +// See WebKit bug: https://bugs.webkit.org/show_bug.cgi?id=261874 +static inline void apply_webkit_dmabuf_workaround() { + if (!is_webkit_dmabuf_bugged()) { + return; + } + set_env("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); +} +} // namespace webkit_dmabuf + +namespace webkit_symbols { +using webkit_web_view_evaluate_javascript_t = + void (*)(WebKitWebView *, const char *, gssize, const char *, const char *, + GCancellable *, GAsyncReadyCallback, gpointer); + +using webkit_web_view_run_javascript_t = void (*)(WebKitWebView *, + const gchar *, GCancellable *, + GAsyncReadyCallback, + gpointer); + +constexpr auto webkit_web_view_evaluate_javascript = + library_symbol( + "webkit_web_view_evaluate_javascript"); +constexpr auto webkit_web_view_run_javascript = + library_symbol( + "webkit_web_view_run_javascript"); +} // namespace webkit_symbols + class gtk_webkit_engine { public: gtk_webkit_engine(bool debug, void *window) : m_window(static_cast(window)) { - if (gtk_init_check(0, NULL) == FALSE) { - return; - } - m_window = static_cast(window); - if (m_window == nullptr) { + auto owns_window = !window; + if (owns_window) { + if (gtk_init_check(nullptr, nullptr) == FALSE) { + return; + } m_window = gtk_window_new(GTK_WINDOW_TOPLEVEL); + inc_window_count(); + g_signal_connect(G_OBJECT(m_window), "destroy", + G_CALLBACK(+[](GtkWidget *, gpointer arg) { + auto *w = static_cast(arg); + if (dec_window_count() <= 0) { + w->terminate(); + } + }), + this); } - g_signal_connect(G_OBJECT(m_window), "destroy", - G_CALLBACK(+[](GtkWidget *, gpointer arg) { - static_cast(arg)->terminate(); - }), - this); + webkit_dmabuf::apply_webkit_dmabuf_workaround(); // Initialize webview widget m_webview = webkit_web_view_new(); WebKitUserContentManager *manager = @@ -472,7 +916,7 @@ public: "external.postMessage(s);}}"); gtk_container_add(GTK_CONTAINER(m_window), GTK_WIDGET(m_webview)); - gtk_widget_grab_focus(GTK_WIDGET(m_webview)); + gtk_widget_show(GTK_WIDGET(m_webview)); WebKitSettings *settings = webkit_web_view_get_settings(WEBKIT_WEB_VIEW(m_webview)); @@ -483,7 +927,10 @@ public: webkit_settings_set_enable_developer_extras(settings, true); } - gtk_widget_show_all(m_window); + if (owns_window) { + gtk_widget_grab_focus(GTK_WIDGET(m_webview)); + gtk_widget_show_all(m_window); + } } virtual ~gtk_webkit_engine() = default; void *window() { return (void *)m_window; } @@ -524,21 +971,35 @@ public: } void set_html(const std::string &html) { - webkit_web_view_load_html(WEBKIT_WEB_VIEW(m_webview), html.c_str(), NULL); + webkit_web_view_load_html(WEBKIT_WEB_VIEW(m_webview), html.c_str(), + nullptr); } void init(const std::string &js) { WebKitUserContentManager *manager = webkit_web_view_get_user_content_manager(WEBKIT_WEB_VIEW(m_webview)); webkit_user_content_manager_add_script( - manager, webkit_user_script_new( - js.c_str(), WEBKIT_USER_CONTENT_INJECT_TOP_FRAME, - WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, NULL, NULL)); + manager, + webkit_user_script_new(js.c_str(), WEBKIT_USER_CONTENT_INJECT_TOP_FRAME, + WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, + nullptr, nullptr)); } void eval(const std::string &js) { - webkit_web_view_run_javascript(WEBKIT_WEB_VIEW(m_webview), js.c_str(), NULL, - NULL, NULL); + auto &lib = get_webkit_library(); + auto wkmajor = webkit_get_major_version(); + auto wkminor = webkit_get_minor_version(); + if ((wkmajor == 2 && wkminor >= 40) || wkmajor > 2) { + if (auto fn = + lib.get(webkit_symbols::webkit_web_view_evaluate_javascript)) { + fn(WEBKIT_WEB_VIEW(m_webview), js.c_str(), + static_cast(js.size()), nullptr, nullptr, nullptr, nullptr, + nullptr); + } + } else if (auto fn = + lib.get(webkit_symbols::webkit_web_view_run_javascript)) { + fn(WEBKIT_WEB_VIEW(m_webview), js.c_str(), nullptr, nullptr, nullptr); + } } private: @@ -546,13 +1007,14 @@ private: static char *get_string_from_js_result(WebKitJavascriptResult *r) { char *s; -#if WEBKIT_MAJOR_VERSION >= 2 && WEBKIT_MINOR_VERSION >= 22 +#if (WEBKIT_MAJOR_VERSION == 2 && WEBKIT_MINOR_VERSION >= 22) || \ + WEBKIT_MAJOR_VERSION > 2 JSCValue *value = webkit_javascript_result_get_js_value(r); s = jsc_value_to_string(value); #else JSGlobalContextRef ctx = webkit_javascript_result_get_global_context(r); JSValueRef value = webkit_javascript_result_get_value(r); - JSStringRef js = JSValueToStringCopy(ctx, value, NULL); + JSStringRef js = JSValueToStringCopy(ctx, value, nullptr); size_t n = JSStringGetMaximumUTF8CStringSize(js); s = g_new(char, n); JSStringGetUTF8CString(js, s, n); @@ -561,6 +1023,50 @@ private: return s; } + static const native_library &get_webkit_library() { + static const native_library non_loaded_lib; + static native_library loaded_lib; + + if (loaded_lib.is_loaded()) { + return loaded_lib; + } + + constexpr std::array lib_names{"libwebkit2gtk-4.1.so", + "libwebkit2gtk-4.0.so"}; + auto found = + std::find_if(lib_names.begin(), lib_names.end(), [](const char *name) { + return native_library::is_loaded(name); + }); + + if (found == lib_names.end()) { + return non_loaded_lib; + } + + loaded_lib = native_library(*found); + + auto loaded = loaded_lib.is_loaded(); + if (!loaded) { + return non_loaded_lib; + } + + return loaded_lib; + } + + static std::atomic_uint &window_ref_count() { + static std::atomic_uint ref_count{0}; + return ref_count; + } + + static unsigned int inc_window_count() { return ++window_ref_count(); } + + static unsigned int dec_window_count() { + auto &count = window_ref_count(); + if (count > 0) { + return --count; + } + return 0; + } + GtkWidget *m_window; GtkWidget *m_webview; }; @@ -607,6 +1113,28 @@ Result msg_send(Args... args) noexcept { return invoke(objc_msgSend, args...); } +// Wrapper around NSAutoreleasePool that drains the pool on destruction. +class autoreleasepool { +public: + autoreleasepool() + : m_pool(msg_send(objc_getClass("NSAutoreleasePool"), + sel_registerName("new"))) {} + + ~autoreleasepool() { + if (m_pool) { + msg_send(m_pool, sel_registerName("drain")); + } + } + + autoreleasepool(const autoreleasepool &) = delete; + autoreleasepool &operator=(const autoreleasepool &) = delete; + autoreleasepool(autoreleasepool &&) = delete; + autoreleasepool &operator=(autoreleasepool &&) = delete; + +private: + id m_pool{}; +}; + } // namespace objc enum NSBackingStoreType : NSUInteger { NSBackingStoreBuffered = 2 }; @@ -629,35 +1157,48 @@ enum WKUserScriptInjectionTime : NSInteger { enum NSModalResponse : NSInteger { NSModalResponseOK = 1 }; // Convenient conversion of string literals. -id operator"" _cls(const char *s, std::size_t) { return (id)objc_getClass(s); } -SEL operator"" _sel(const char *s, std::size_t) { return sel_registerName(s); } -id operator"" _str(const char *s, std::size_t) { +inline id operator"" _cls(const char *s, std::size_t) { + return (id)objc_getClass(s); +} +inline SEL operator"" _sel(const char *s, std::size_t) { + return sel_registerName(s); +} +inline id operator"" _str(const char *s, std::size_t) { return objc::msg_send("NSString"_cls, "stringWithUTF8String:"_sel, s); } class cocoa_wkwebview_engine { public: cocoa_wkwebview_engine(bool debug, void *window) - : m_debug{debug}, m_parent_window{window} { + : m_debug{debug}, m_window{static_cast(window)}, m_owns_window{ + !window} { auto app = get_shared_application(); - auto delegate = create_app_delegate(); - objc_setAssociatedObject(delegate, "webview", (id)this, - OBJC_ASSOCIATION_ASSIGN); - objc::msg_send(app, "setDelegate:"_sel, delegate); + // See comments related to application lifecycle in create_app_delegate(). + if (!m_owns_window) { + create_window(); + } else { + // Only set the app delegate if it hasn't already been set. + auto delegate = objc::msg_send(app, "delegate"_sel); + if (delegate) { + create_window(); + } else { + delegate = create_app_delegate(); + objc_setAssociatedObject(delegate, "webview", (id)this, + OBJC_ASSOCIATION_ASSIGN); + objc::msg_send(app, "setDelegate:"_sel, delegate); - // Start the main run loop so that the app delegate gets the - // NSApplicationDidFinishLaunchingNotification notification after the run - // loop has started in order to perform further initialization. - // We need to return from this constructor so this run loop is only - // temporary. - objc::msg_send(app, "run"_sel); + // Start the main run loop so that the app delegate gets the + // NSApplicationDidFinishLaunchingNotification notification after the run + // loop has started in order to perform further initialization. + // We need to return from this constructor so this run loop is only + // temporary. + objc::msg_send(app, "run"_sel); + } + } } virtual ~cocoa_wkwebview_engine() = default; void *window() { return (void *)m_window; } - void terminate() { - auto app = get_shared_application(); - objc::msg_send(app, "terminate:"_sel, nullptr); - } + void terminate() { stop_run_loop(); } void run() { auto app = get_shared_application(); objc::msg_send(app, "run"_sel); @@ -699,6 +1240,8 @@ public: objc::msg_send(m_window, "center"_sel); } void navigate(const std::string &url) { + objc::autoreleasepool pool; + auto nsurl = objc::msg_send( "NSURL"_cls, "URLWithString:"_sel, objc::msg_send("NSString"_cls, "stringWithUTF8String:"_sel, @@ -709,6 +1252,7 @@ public: objc::msg_send("NSURLRequest"_cls, "requestWithURL:"_sel, nsurl)); } void set_html(const std::string &html) { + objc::autoreleasepool pool; objc::msg_send(m_webview, "loadHTMLString:baseURL:"_sel, objc::msg_send("NSString"_cls, "stringWithUTF8String:"_sel, @@ -737,73 +1281,112 @@ public: private: virtual void on_message(const std::string &msg) = 0; - static id create_app_delegate() { - auto cls = - objc_allocateClassPair((Class) "NSResponder"_cls, "AppDelegate", 0); - class_addProtocol(cls, objc_getProtocol("NSTouchBarProvider")); - class_addMethod(cls, "applicationShouldTerminateAfterLastWindowClosed:"_sel, - (IMP)(+[](id, SEL, id) -> BOOL { return 1; }), "c@:@"); - class_addMethod( - cls, "userContentController:didReceiveScriptMessage:"_sel, - (IMP)(+[](id self, SEL, id, id msg) { - auto w = get_associated_webview(self); - w->on_message(((const char *(*)(id, SEL))objc_msgSend)( - objc::msg_send(msg, "body"_sel), "UTF8String"_sel)); - }), - "v@:@@"); - class_addMethod(cls, "applicationDidFinishLaunching:"_sel, - (IMP)(+[](id self, SEL, id notification) { - auto app = objc::msg_send(notification, "object"_sel); - auto w = get_associated_webview(self); - w->on_application_did_finish_launching(self, app); - }), - "v@:@"); - objc_registerClassPair(cls); + id create_app_delegate() { + constexpr auto class_name = "WebviewAppDelegate"; + // Avoid crash due to registering same class twice + auto cls = objc_lookUpClass(class_name); + if (!cls) { + // Note: Avoid registering the class name "AppDelegate" as it is the + // default name in projects created with Xcode, and using the same name + // causes objc_registerClassPair to crash. + cls = objc_allocateClassPair((Class) "NSResponder"_cls, class_name, 0); + class_addProtocol(cls, objc_getProtocol("NSTouchBarProvider")); + class_addMethod(cls, + "applicationShouldTerminateAfterLastWindowClosed:"_sel, + (IMP)(+[](id, SEL, id) -> BOOL { return YES; }), "c@:@"); + class_addMethod(cls, "applicationShouldTerminate:"_sel, + (IMP)(+[](id self, SEL, id sender) -> int { + auto w = get_associated_webview(self); + return w->on_application_should_terminate(self, sender); + }), + "i@:@"); + // If the library was not initialized with an existing window then the user + // is likely managing the application lifecycle and we would not get the + // "applicationDidFinishLaunching:" message and therefore do not need to + // add this method. + if (m_owns_window) { + class_addMethod(cls, "applicationDidFinishLaunching:"_sel, + (IMP)(+[](id self, SEL, id notification) { + auto app = + objc::msg_send(notification, "object"_sel); + auto w = get_associated_webview(self); + w->on_application_did_finish_launching(self, app); + }), + "v@:@"); + } + objc_registerClassPair(cls); + } return objc::msg_send((id)cls, "new"_sel); } + id create_script_message_handler() { + constexpr auto class_name = "WebviewWKScriptMessageHandler"; + // Avoid crash due to registering same class twice + auto cls = objc_lookUpClass(class_name); + if (!cls) { + cls = objc_allocateClassPair((Class) "NSResponder"_cls, class_name, 0); + class_addProtocol(cls, objc_getProtocol("WKScriptMessageHandler")); + class_addMethod( + cls, "userContentController:didReceiveScriptMessage:"_sel, + (IMP)(+[](id self, SEL, id, id msg) { + auto w = get_associated_webview(self); + w->on_message(objc::msg_send( + objc::msg_send(msg, "body"_sel), "UTF8String"_sel)); + }), + "v@:@@"); + objc_registerClassPair(cls); + } + auto instance = objc::msg_send((id)cls, "new"_sel); + objc_setAssociatedObject(instance, "webview", (id)this, + OBJC_ASSOCIATION_ASSIGN); + return instance; + } static id create_webkit_ui_delegate() { - auto cls = - objc_allocateClassPair((Class) "NSObject"_cls, "WebkitUIDelegate", 0); - class_addProtocol(cls, objc_getProtocol("WKUIDelegate")); - class_addMethod( - cls, - "webView:runOpenPanelWithParameters:initiatedByFrame:completionHandler:"_sel, - (IMP)(+[](id, SEL, id, id parameters, id, id completion_handler) { - auto allows_multiple_selection = - objc::msg_send(parameters, "allowsMultipleSelection"_sel); - auto allows_directories = - objc::msg_send(parameters, "allowsDirectories"_sel); + constexpr auto class_name = "WebviewWKUIDelegate"; + // Avoid crash due to registering same class twice + auto cls = objc_lookUpClass(class_name); + if (!cls) { + cls = objc_allocateClassPair((Class) "NSObject"_cls, class_name, 0); + class_addProtocol(cls, objc_getProtocol("WKUIDelegate")); + class_addMethod( + cls, + "webView:runOpenPanelWithParameters:initiatedByFrame:completionHandler:"_sel, + (IMP)(+[](id, SEL, id, id parameters, id, id completion_handler) { + auto allows_multiple_selection = + objc::msg_send(parameters, "allowsMultipleSelection"_sel); + auto allows_directories = + objc::msg_send(parameters, "allowsDirectories"_sel); - // Show a panel for selecting files. - auto panel = objc::msg_send("NSOpenPanel"_cls, "openPanel"_sel); - objc::msg_send(panel, "setCanChooseFiles:"_sel, YES); - objc::msg_send(panel, "setCanChooseDirectories:"_sel, - allows_directories); - objc::msg_send(panel, "setAllowsMultipleSelection:"_sel, - allows_multiple_selection); - auto modal_response = - objc::msg_send(panel, "runModal"_sel); + // Show a panel for selecting files. + auto panel = objc::msg_send("NSOpenPanel"_cls, "openPanel"_sel); + objc::msg_send(panel, "setCanChooseFiles:"_sel, YES); + objc::msg_send(panel, "setCanChooseDirectories:"_sel, + allows_directories); + objc::msg_send(panel, "setAllowsMultipleSelection:"_sel, + allows_multiple_selection); + auto modal_response = + objc::msg_send(panel, "runModal"_sel); - // Get the URLs for the selected files. If the modal was canceled - // then we pass null to the completion handler to signify - // cancellation. - id urls = modal_response == NSModalResponseOK - ? objc::msg_send(panel, "URLs"_sel) - : nullptr; + // Get the URLs for the selected files. If the modal was canceled + // then we pass null to the completion handler to signify + // cancellation. + id urls = modal_response == NSModalResponseOK + ? objc::msg_send(panel, "URLs"_sel) + : nullptr; - // Invoke the completion handler block. - auto sig = objc::msg_send("NSMethodSignature"_cls, - "signatureWithObjCTypes:"_sel, "v@?@"); - auto invocation = objc::msg_send( - "NSInvocation"_cls, "invocationWithMethodSignature:"_sel, sig); - objc::msg_send(invocation, "setTarget:"_sel, - completion_handler); - objc::msg_send(invocation, "setArgument:atIndex:"_sel, &urls, - 1); - objc::msg_send(invocation, "invoke"_sel); - }), - "v@:@@@@"); - objc_registerClassPair(cls); + // Invoke the completion handler block. + auto sig = objc::msg_send( + "NSMethodSignature"_cls, "signatureWithObjCTypes:"_sel, "v@?@"); + auto invocation = objc::msg_send( + "NSInvocation"_cls, "invocationWithMethodSignature:"_sel, sig); + objc::msg_send(invocation, "setTarget:"_sel, + completion_handler); + objc::msg_send(invocation, "setArgument:atIndex:"_sel, &urls, + 1); + objc::msg_send(invocation, "invoke"_sel); + }), + "v@:@@@@"); + objc_registerClassPair(cls); + } return objc::msg_send((id)cls, "new"_sel); } static id get_shared_application() { @@ -828,10 +1411,13 @@ private: objc::msg_send(bundle_path, "hasSuffix:"_sel, ".app"_str); return !!bundled; } - void on_application_did_finish_launching(id delegate, id app) { - // Stop the main run loop so that we can return - // from the constructor. - objc::msg_send(app, "stop:"_sel, nullptr); + void on_application_did_finish_launching(id /*delegate*/, id app) { + // See comments related to application lifecycle in create_app_delegate(). + if (m_owns_window) { + // Stop the main run loop so that we can return + // from the constructor. + stop_run_loop(); + } // Activate the app if it is not bundled. // Bundled apps launched from Finder are activated automatically but @@ -851,15 +1437,16 @@ private: objc::msg_send(app, "activateIgnoringOtherApps:"_sel, YES); } + create_window(); + } + void create_window() { // Main window - if (!m_parent_window) { + if (m_owns_window) { m_window = objc::msg_send("NSWindow"_cls, "alloc"_sel); auto style = NSWindowStyleMaskTitled; m_window = objc::msg_send( m_window, "initWithContentRect:styleMask:backing:defer:"_sel, CGRectMake(0, 0, 0, 0), style, NSBackingStoreBuffered, NO); - } else { - m_window = (id)m_parent_window; } // Webview @@ -901,24 +1488,75 @@ private: objc::msg_send(m_webview, "initWithFrame:configuration:"_sel, CGRectMake(0, 0, 0, 0), config); objc::msg_send(m_webview, "setUIDelegate:"_sel, ui_delegate); - objc::msg_send(m_manager, "addScriptMessageHandler:name:"_sel, - delegate, "external"_str); - init(R"script( + if (m_debug) { + // Explicitly make WKWebView inspectable via Safari on OS versions that + // disable the feature by default (macOS 13.3 and later) and support + // enabling it. According to Apple, the behavior on older OS versions is + // for content to always be inspectable in "debug builds". + // Testing shows that this is true for macOS 12.6 but somehow not 10.15. + // https://webkit.org/blog/13936/enabling-the-inspection-of-web-content-in-apps/ +#if defined(__has_builtin) +#if __has_builtin(__builtin_available) + if (__builtin_available(macOS 13.3, iOS 16.4, tvOS 16.4, *)) { + objc::msg_send( + m_webview, "setInspectable:"_sel, + objc::msg_send("NSNumber"_cls, "numberWithBool:"_sel, YES)); + } +#else +#error __builtin_available not supported by compiler +#endif +#else +#error __has_builtin not supported by compiler +#endif + } + + auto script_message_handler = create_script_message_handler(); + objc::msg_send(m_manager, "addScriptMessageHandler:name:"_sel, + script_message_handler, "external"_str); + + init(R""( window.external = { invoke: function(s) { window.webkit.messageHandlers.external.postMessage(s); }, }; - )script"); + )""); objc::msg_send(m_window, "setContentView:"_sel, m_webview); - objc::msg_send(m_window, "makeKeyAndOrderFront:"_sel, nullptr); + + if (m_owns_window) { + objc::msg_send(m_window, "makeKeyAndOrderFront:"_sel, nullptr); + } } + int on_application_should_terminate(id /*delegate*/, id app) { + dispatch([app, this] { + // Don't terminate the application. + objc::msg_send(app, "replyToApplicationShouldTerminate:"_sel, NO); + // Instead stop the run loop. + stop_run_loop(); + }); + return 2 /*NSTerminateLater*/; + } + void stop_run_loop() { + auto app = get_shared_application(); + // Request the run loop to stop. This doesn't immediately stop the loop. + objc::msg_send(app, "stop:"_sel, nullptr); + // The run loop will stop after processing an NSEvent. + // Event type: NSEventTypeApplicationDefined (macOS 10.12+), + // NSApplicationDefined (macOS 10.0–10.12) + int type = 15; + auto event = objc::msg_send( + "NSEvent"_cls, + "otherEventWithType:location:modifierFlags:timestamp:windowNumber:context:subtype:data1:data2:"_sel, + type, CGPointMake(0, 0), 0, 0, 0, nullptr, 0, 0, 0); + objc::msg_send(app, "postEvent:atStart:"_sel, event, YES); + } + bool m_debug; - void *m_parent_window; id m_window; id m_webview; id m_manager; + bool m_owns_window; }; } // namespace detail @@ -947,10 +1585,12 @@ using browser_engine = detail::cocoa_wkwebview_engine; #include "WebView2.h" #ifdef _MSC_VER +#pragma comment(lib, "advapi32.lib") #pragma comment(lib, "ole32.lib") #pragma comment(lib, "shell32.lib") #pragma comment(lib, "shlwapi.lib") #pragma comment(lib, "user32.lib") +#pragma comment(lib, "version.lib") #endif namespace webview { @@ -958,54 +1598,75 @@ namespace detail { using msg_cb_t = std::function; -// Converts a narrow (UTF-8-encoded) string into a wide (UTF-16-encoded) string. -inline std::wstring widen_string(const std::string &input) { - if (input.empty()) { - return std::wstring(); - } - UINT cp = CP_UTF8; - DWORD flags = MB_ERR_INVALID_CHARS; - auto input_c = input.c_str(); - auto input_length = static_cast(input.size()); - auto required_length = - MultiByteToWideChar(cp, flags, input_c, input_length, nullptr, 0); - if (required_length > 0) { - std::wstring output(static_cast(required_length), L'\0'); - if (MultiByteToWideChar(cp, flags, input_c, input_length, &output[0], - required_length) > 0) { - return output; +// Parses a version string with 1-4 integral components, e.g. "1.2.3.4". +// Missing or invalid components default to 0, and excess components are ignored. +template +std::array +parse_version(const std::basic_string &version) noexcept { + auto parse_component = [](auto sb, auto se) -> unsigned int { + try { + auto n = std::stol(std::basic_string(sb, se)); + return n < 0 ? 0 : n; + } catch (std::exception &) { + return 0; } + }; + auto end = version.end(); + auto sb = version.begin(); // subrange begin + auto se = sb; // subrange end + unsigned int ci = 0; // component index + std::array components{}; + while (sb != end && se != end && ci < components.size()) { + if (*se == static_cast('.')) { + components[ci++] = parse_component(sb, se); + sb = ++se; + continue; + } + ++se; } - // Failed to convert string from UTF-8 to UTF-16 - return std::wstring(); + if (sb < se && ci < components.size()) { + components[ci] = parse_component(sb, se); + } + return components; } -// Converts a wide (UTF-16-encoded) string into a narrow (UTF-8-encoded) string. -inline std::string narrow_string(const std::wstring &input) { - if (input.empty()) { - return std::string(); +template +auto parse_version(const T (&version)[Length]) noexcept { + return parse_version(std::basic_string(version, Length)); +} + +std::wstring get_file_version_string(const std::wstring &file_path) noexcept { + DWORD dummy_handle; // Unused + DWORD info_buffer_length = + GetFileVersionInfoSizeW(file_path.c_str(), &dummy_handle); + if (info_buffer_length == 0) { + return std::wstring(); } - UINT cp = CP_UTF8; - DWORD flags = WC_ERR_INVALID_CHARS; - auto input_c = input.c_str(); - auto input_length = static_cast(input.size()); - auto required_length = WideCharToMultiByte(cp, flags, input_c, input_length, - nullptr, 0, nullptr, nullptr); - if (required_length > 0) { - std::string output(static_cast(required_length), '\0'); - if (WideCharToMultiByte(cp, flags, input_c, input_length, &output[0], - required_length, nullptr, nullptr) > 0) { - return output; - } + std::vector info_buffer; + info_buffer.reserve(info_buffer_length); + if (!GetFileVersionInfoW(file_path.c_str(), 0, info_buffer_length, + info_buffer.data())) { + return std::wstring(); } - // Failed to convert string from UTF-16 to UTF-8 - return std::string(); + auto sub_block = L"\\StringFileInfo\\040904B0\\ProductVersion"; + LPWSTR version = nullptr; + unsigned int version_length = 0; + if (!VerQueryValueW(info_buffer.data(), sub_block, + reinterpret_cast(&version), &version_length)) { + return std::wstring(); + } + if (!version || version_length == 0) { + return std::wstring(); + } + return std::wstring(version, version_length); } // A wrapper around COM library initialization. Calls CoInitializeEx in the // constructor and CoUninitialize in the destructor. class com_init_wrapper { public: + com_init_wrapper() = default; + com_init_wrapper(DWORD dwCoInit) { // We can safely continue as long as COM was either successfully // initialized or already initialized. @@ -1028,8 +1689,12 @@ public: com_init_wrapper(const com_init_wrapper &other) = delete; com_init_wrapper &operator=(const com_init_wrapper &other) = delete; - com_init_wrapper(com_init_wrapper &&other) = delete; - com_init_wrapper &operator=(com_init_wrapper &&other) = delete; + com_init_wrapper(com_init_wrapper &&other) { *this = std::move(other); } + + com_init_wrapper &operator=(com_init_wrapper &&other) { + m_initialized = std::exchange(other.m_initialized, false); + return *this; + } bool is_initialized() const { return m_initialized; } @@ -1037,79 +1702,195 @@ private: bool m_initialized = false; }; -// Holds a symbol name and associated type for code clarity. -template class library_symbol { -public: - using type = T; +namespace ntdll_symbols { +using RtlGetVersion_t = + unsigned int /*NTSTATUS*/ (WINAPI *)(RTL_OSVERSIONINFOW *); - constexpr explicit library_symbol(const char *name) : m_name(name) {} - constexpr const char *get_name() const { return m_name; } +constexpr auto RtlGetVersion = library_symbol("RtlGetVersion"); +} // namespace ntdll_symbols -private: - const char *m_name; +namespace user32_symbols { +using DPI_AWARENESS_CONTEXT = HANDLE; +using SetProcessDpiAwarenessContext_t = BOOL(WINAPI *)(DPI_AWARENESS_CONTEXT); +using SetProcessDPIAware_t = BOOL(WINAPI *)(); +using GetDpiForWindow_t = UINT(WINAPI *)(HWND); +using EnableNonClientDpiScaling_t = BOOL(WINAPI *)(HWND); +using AdjustWindowRectExForDpi_t = BOOL(WINAPI *)(LPRECT, DWORD, BOOL, DWORD, + UINT); +using GetWindowDpiAwarenessContext_t = DPI_AWARENESS_CONTEXT(WINAPI *)(HWND); +using AreDpiAwarenessContextsEqual_t = BOOL(WINAPI *)(DPI_AWARENESS_CONTEXT, + DPI_AWARENESS_CONTEXT); + +// Use intptr_t as the underlying type because we need to +// reinterpret_cast which is a pointer. +// Available since Windows 10, version 1607 +enum class dpi_awareness : intptr_t { + per_monitor_v2_aware = -4, // Available since Windows 10, version 1703 + per_monitor_aware = -3 }; -// Loads a native shared library and allows one to get addresses for those -// symbols. -class native_library { -public: - explicit native_library(const wchar_t *name) : m_handle(LoadLibraryW(name)) {} +constexpr auto SetProcessDpiAwarenessContext = + library_symbol( + "SetProcessDpiAwarenessContext"); +constexpr auto SetProcessDPIAware = + library_symbol("SetProcessDPIAware"); +constexpr auto GetDpiForWindow = + library_symbol("GetDpiForWindow"); +constexpr auto EnableNonClientDpiScaling = + library_symbol("EnableNonClientDpiScaling"); +constexpr auto AdjustWindowRectExForDpi = + library_symbol("AdjustWindowRectExForDpi"); +constexpr auto GetWindowDpiAwarenessContext = + library_symbol( + "GetWindowDpiAwarenessContext"); +constexpr auto AreDpiAwarenessContextsEqual = + library_symbol( + "AreDpiAwarenessContextsEqual"); +} // namespace user32_symbols - ~native_library() { +namespace dwmapi_symbols { +typedef enum { + // This undocumented value is used instead of DWMWA_USE_IMMERSIVE_DARK_MODE + // on Windows 10 older than build 19041 (2004/20H1). + DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_V10_0_19041 = 19, + // Documented as being supported since Windows 11 build 22000 (21H2) but it + // works since Windows 10 build 19041 (2004/20H1). + DWMWA_USE_IMMERSIVE_DARK_MODE = 20 +} DWMWINDOWATTRIBUTE; +using DwmSetWindowAttribute_t = HRESULT(WINAPI *)(HWND, DWORD, LPCVOID, DWORD); + +constexpr auto DwmSetWindowAttribute = + library_symbol("DwmSetWindowAttribute"); +} // namespace dwmapi_symbols + +namespace shcore_symbols { +typedef enum { PROCESS_PER_MONITOR_DPI_AWARE = 2 } PROCESS_DPI_AWARENESS; +using SetProcessDpiAwareness_t = HRESULT(WINAPI *)(PROCESS_DPI_AWARENESS); + +constexpr auto SetProcessDpiAwareness = + library_symbol("SetProcessDpiAwareness"); +} // namespace shcore_symbols + +class reg_key { +public: + explicit reg_key(HKEY root_key, const wchar_t *sub_key, DWORD options, + REGSAM sam_desired) { + HKEY handle; + auto status = + RegOpenKeyExW(root_key, sub_key, options, sam_desired, &handle); + if (status == ERROR_SUCCESS) { + m_handle = handle; + } + } + + explicit reg_key(HKEY root_key, const std::wstring &sub_key, DWORD options, + REGSAM sam_desired) + : reg_key(root_key, sub_key.c_str(), options, sam_desired) {} + + virtual ~reg_key() { if (m_handle) { - FreeLibrary(m_handle); + RegCloseKey(m_handle); m_handle = nullptr; } } - native_library(const native_library &other) = delete; - native_library &operator=(const native_library &other) = delete; - native_library(native_library &&other) = default; - native_library &operator=(native_library &&other) = default; + reg_key(const reg_key &other) = delete; + reg_key &operator=(const reg_key &other) = delete; + reg_key(reg_key &&other) = delete; + reg_key &operator=(reg_key &&other) = delete; - // Returns true if the library is currently loaded; otherwise false. - operator bool() const { return is_loaded(); } + bool is_open() const { return !!m_handle; } + bool get_handle() const { return m_handle; } - // Get the address for the specified symbol or nullptr if not found. - template - typename Symbol::type get(const Symbol &symbol) const { - if (is_loaded()) { - return reinterpret_cast( - GetProcAddress(m_handle, symbol.get_name())); + template + void query_bytes(const wchar_t *name, Container &result) const { + DWORD buf_length = 0; + // Get the size of the data in bytes. + auto status = RegQueryValueExW(m_handle, name, nullptr, nullptr, nullptr, + &buf_length); + if (status != ERROR_SUCCESS && status != ERROR_MORE_DATA) { + result.resize(0); + return; + } + // Read the data. + result.resize(buf_length / sizeof(typename Container::value_type)); + auto *buf = reinterpret_cast(&result[0]); + status = + RegQueryValueExW(m_handle, name, nullptr, nullptr, buf, &buf_length); + if (status != ERROR_SUCCESS) { + result.resize(0); + return; } - return nullptr; } - // Returns true if the library is currently loaded; otherwise false. - bool is_loaded() const { return !!m_handle; } + std::wstring query_string(const wchar_t *name) const { + std::wstring result; + query_bytes(name, result); + // Remove trailing null-characters. + for (std::size_t length = result.size(); length > 0; --length) { + if (result[length - 1] != 0) { + result.resize(length); + break; + } + } + return result; + } + + unsigned int query_uint(const wchar_t *name, + unsigned int default_value) const { + std::vector data; + query_bytes(name, data); + if (data.size() < sizeof(DWORD)) { + return default_value; + } + return static_cast(*reinterpret_cast(data.data())); + } private: - HMODULE m_handle = nullptr; + HKEY m_handle = nullptr; }; -struct user32_symbols { - using DPI_AWARENESS_CONTEXT = HANDLE; - using SetProcessDpiAwarenessContext_t = BOOL(WINAPI *)(DPI_AWARENESS_CONTEXT); +// Compare the specified version against the OS version. +// Returns less than 0 if the OS version is less. +// Returns 0 if the versions are equal. +// Returns greater than 0 if the specified version is greater. +inline int compare_os_version(unsigned int major, unsigned int minor, + unsigned int build) { + // Use RtlGetVersion both to bypass potential issues related to + // VerifyVersionInfo and manifests, and because both GetVersion and + // GetVersionEx are deprecated. + auto ntdll = native_library(L"ntdll.dll"); + if (auto fn = ntdll.get(ntdll_symbols::RtlGetVersion)) { + RTL_OSVERSIONINFOW vi{}; + vi.dwOSVersionInfoSize = sizeof(vi); + if (fn(&vi) != 0) { + return false; + } + if (vi.dwMajorVersion == major) { + if (vi.dwMinorVersion == minor) { + return static_cast(vi.dwBuildNumber) - static_cast(build); + } + return static_cast(vi.dwMinorVersion) - static_cast(minor); + } + return static_cast(vi.dwMajorVersion) - static_cast(major); + } + return false; +} - static constexpr auto SetProcessDpiAwarenessContext = - library_symbol( - "SetProcessDpiAwarenessContext"); - static constexpr auto SetProcessDPIAware = - library_symbol("SetProcessDPIAware"); -}; - -struct shcore_symbols { - typedef enum { PROCESS_PER_MONITOR_DPI_AWARE = 2 } PROCESS_DPI_AWARENESS; - using SetProcessDpiAwareness_t = HRESULT(WINAPI *)(PROCESS_DPI_AWARENESS); - - static constexpr auto SetProcessDpiAwareness = - library_symbol("SetProcessDpiAwareness"); -}; +inline bool is_per_monitor_v2_awareness_available() { + // Windows 10, version 1703 + return compare_os_version(10, 0, 15063) >= 0; +} inline bool enable_dpi_awareness() { auto user32 = native_library(L"user32.dll"); if (auto fn = user32.get(user32_symbols::SetProcessDpiAwarenessContext)) { - if (fn(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE)) { + auto dpi_awareness = + reinterpret_cast( + is_per_monitor_v2_awareness_available() + ? user32_symbols::dpi_awareness::per_monitor_v2_aware + : user32_symbols::dpi_awareness::per_monitor_aware); + if (fn(dpi_awareness)) { return true; } return GetLastError() == ERROR_ACCESS_DENIED; @@ -1126,6 +1907,413 @@ inline bool enable_dpi_awareness() { return true; } +inline bool enable_non_client_dpi_scaling_if_needed(HWND window) { + auto user32 = native_library(L"user32.dll"); + auto get_ctx_fn = user32.get(user32_symbols::GetWindowDpiAwarenessContext); + if (!get_ctx_fn) { + return true; + } + auto awareness = get_ctx_fn(window); + if (!awareness) { + return false; + } + auto ctx_equal_fn = user32.get(user32_symbols::AreDpiAwarenessContextsEqual); + if (!ctx_equal_fn) { + return true; + } + // EnableNonClientDpiScaling is only needed with per monitor v1 awareness. + auto per_monitor = reinterpret_cast( + user32_symbols::dpi_awareness::per_monitor_aware); + if (!ctx_equal_fn(awareness, per_monitor)) { + return true; + } + auto enable_fn = user32.get(user32_symbols::EnableNonClientDpiScaling); + if (!enable_fn) { + return true; + } + return !!enable_fn(window); +} + +constexpr int get_default_window_dpi() { + constexpr const int default_dpi = 96; // USER_DEFAULT_SCREEN_DPI + return default_dpi; +} + +inline int get_window_dpi(HWND window) { + auto user32 = native_library(L"user32.dll"); + if (auto fn = user32.get(user32_symbols::GetDpiForWindow)) { + auto dpi = static_cast(fn(window)); + return dpi; + } + return get_default_window_dpi(); +} + +constexpr int scale_value_for_dpi(int value, int from_dpi, int to_dpi) { + return (value * to_dpi) / from_dpi; +} + +constexpr SIZE scale_size(int width, int height, int from_dpi, int to_dpi) { + auto scaled_width = scale_value_for_dpi(width, from_dpi, to_dpi); + auto scaled_height = scale_value_for_dpi(height, from_dpi, to_dpi); + return {scaled_width, scaled_height}; +} + +inline SIZE make_window_frame_size(HWND window, int width, int height, + int dpi) { + auto style = GetWindowLong(window, GWL_STYLE); + RECT r{0, 0, width, height}; + auto user32 = native_library(L"user32.dll"); + if (auto fn = user32.get(user32_symbols::AdjustWindowRectExForDpi)) { + fn(&r, style, FALSE, 0, static_cast(dpi)); + } else { + AdjustWindowRect(&r, style, 0); + } + auto frame_width = r.right - r.left; + auto frame_height = r.bottom - r.top; + return {frame_width, frame_height}; +} + +inline bool is_dark_theme_enabled() { + constexpr auto *sub_key = + L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; + reg_key key(HKEY_CURRENT_USER, sub_key, 0, KEY_READ); + if (!key.is_open()) { + // Default is light theme + return false; + } + return key.query_uint(L"AppsUseLightTheme", 1) == 0; +} + +inline void apply_window_theme(HWND window) { + auto dark_theme_enabled = is_dark_theme_enabled(); + + // Use "immersive dark mode" on systems that support it. + // Changes the color of the window's title bar (light or dark). + BOOL use_dark_mode{dark_theme_enabled ? TRUE : FALSE}; + static native_library dwmapi{L"dwmapi.dll"}; + if (auto fn = dwmapi.get(dwmapi_symbols::DwmSetWindowAttribute)) { + // Try the modern, documented attribute before the older, undocumented one. + if (fn(window, dwmapi_symbols::DWMWA_USE_IMMERSIVE_DARK_MODE, + &use_dark_mode, sizeof(use_dark_mode)) != S_OK) { + fn(window, + dwmapi_symbols::DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_V10_0_19041, + &use_dark_mode, sizeof(use_dark_mode)); + } + } +} + +// Enable built-in WebView2Loader implementation by default. +#ifndef WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL +#define WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL 1 +#endif + +// Link WebView2Loader.dll explicitly by default only if the built-in +// implementation is enabled. +#ifndef WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK +#define WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL +#endif + +// Explicit linking of WebView2Loader.dll should be used along with +// the built-in implementation. +#if WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL == 1 && \ + WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK != 1 +#undef WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK +#error Please set WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK=1. +#endif + +#if WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL == 1 +// Gets the last component of a Windows native file path. +// For example, if the path is "C:\a\b" then the result is "b". +template +std::basic_string +get_last_native_path_component(const std::basic_string &path) { + auto pos = path.find_last_of(static_cast('\\')); + if (pos != std::basic_string::npos) { + return path.substr(pos + 1); + } + return std::basic_string(); +} +#endif /* WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL */ + +template struct cast_info_t { + using type = T; + IID iid; +}; + +namespace mswebview2 { +static constexpr IID + IID_ICoreWebView2CreateCoreWebView2ControllerCompletedHandler{ + 0x6C4819F3, + 0xC9B7, + 0x4260, + {0x81, 0x27, 0xC9, 0xF5, 0xBD, 0xE7, 0xF6, 0x8C}}; +static constexpr IID + IID_ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler{ + 0x4E8A3389, + 0xC9D8, + 0x4BD2, + {0xB6, 0xB5, 0x12, 0x4F, 0xEE, 0x6C, 0xC1, 0x4D}}; +static constexpr IID IID_ICoreWebView2PermissionRequestedEventHandler{ + 0x15E1C6A3, + 0xC72A, + 0x4DF3, + {0x91, 0xD7, 0xD0, 0x97, 0xFB, 0xEC, 0x6B, 0xFD}}; +static constexpr IID IID_ICoreWebView2WebMessageReceivedEventHandler{ + 0x57213F19, + 0x00E6, + 0x49FA, + {0x8E, 0x07, 0x89, 0x8E, 0xA0, 0x1E, 0xCB, 0xD2}}; + +#if WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL == 1 +enum class webview2_runtime_type { installed = 0, embedded = 1 }; + +namespace webview2_symbols { +using CreateWebViewEnvironmentWithOptionsInternal_t = + HRESULT(STDMETHODCALLTYPE *)( + bool, webview2_runtime_type, PCWSTR, IUnknown *, + ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler *); +using DllCanUnloadNow_t = HRESULT(STDMETHODCALLTYPE *)(); + +static constexpr auto CreateWebViewEnvironmentWithOptionsInternal = + library_symbol( + "CreateWebViewEnvironmentWithOptionsInternal"); +static constexpr auto DllCanUnloadNow = + library_symbol("DllCanUnloadNow"); +} // namespace webview2_symbols +#endif /* WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL */ + +#if WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK == 1 +namespace webview2_symbols { +using CreateCoreWebView2EnvironmentWithOptions_t = HRESULT(STDMETHODCALLTYPE *)( + PCWSTR, PCWSTR, ICoreWebView2EnvironmentOptions *, + ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler *); +using GetAvailableCoreWebView2BrowserVersionString_t = + HRESULT(STDMETHODCALLTYPE *)(PCWSTR, LPWSTR *); + +static constexpr auto CreateCoreWebView2EnvironmentWithOptions = + library_symbol( + "CreateCoreWebView2EnvironmentWithOptions"); +static constexpr auto GetAvailableCoreWebView2BrowserVersionString = + library_symbol( + "GetAvailableCoreWebView2BrowserVersionString"); +} // namespace webview2_symbols +#endif /* WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK */ + +class loader { +public: + HRESULT create_environment_with_options( + PCWSTR browser_dir, PCWSTR user_data_dir, + ICoreWebView2EnvironmentOptions *env_options, + ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler + *created_handler) const { +#if WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK == 1 + if (m_lib.is_loaded()) { + if (auto fn = m_lib.get( + webview2_symbols::CreateCoreWebView2EnvironmentWithOptions)) { + return fn(browser_dir, user_data_dir, env_options, created_handler); + } + } +#if WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL == 1 + return create_environment_with_options_impl(browser_dir, user_data_dir, + env_options, created_handler); +#else + return S_FALSE; +#endif +#else + return ::CreateCoreWebView2EnvironmentWithOptions( + browser_dir, user_data_dir, env_options, created_handler); +#endif /* WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK */ + } + + HRESULT + get_available_browser_version_string(PCWSTR browser_dir, + LPWSTR *version) const { +#if WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK == 1 + if (m_lib.is_loaded()) { + if (auto fn = m_lib.get( + webview2_symbols::GetAvailableCoreWebView2BrowserVersionString)) { + return fn(browser_dir, version); + } + } +#if WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL == 1 + return get_available_browser_version_string_impl(browser_dir, version); +#else + return S_FALSE; +#endif +#else + return ::GetAvailableCoreWebView2BrowserVersionString(browser_dir, version); +#endif /* WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK */ + } + +private: +#if WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL == 1 + struct client_info_t { + bool found = false; + std::wstring dll_path; + std::wstring version; + webview2_runtime_type runtime_type; + }; + + HRESULT create_environment_with_options_impl( + PCWSTR browser_dir, PCWSTR user_data_dir, + ICoreWebView2EnvironmentOptions *env_options, + ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler + *created_handler) const { + auto found_client = find_available_client(browser_dir); + if (!found_client.found) { + return -1; + } + auto client_dll = native_library(found_client.dll_path.c_str()); + if (auto fn = client_dll.get( + webview2_symbols::CreateWebViewEnvironmentWithOptionsInternal)) { + return fn(true, found_client.runtime_type, user_data_dir, env_options, + created_handler); + } + if (auto fn = client_dll.get(webview2_symbols::DllCanUnloadNow)) { + if (!fn()) { + client_dll.detach(); + } + } + return ERROR_SUCCESS; + } + + HRESULT + get_available_browser_version_string_impl(PCWSTR browser_dir, + LPWSTR *version) const { + if (!version) { + return -1; + } + auto found_client = find_available_client(browser_dir); + if (!found_client.found) { + return -1; + } + auto info_length_bytes = + found_client.version.size() * sizeof(found_client.version[0]); + auto info = static_cast(CoTaskMemAlloc(info_length_bytes)); + if (!info) { + return -1; + } + CopyMemory(info, found_client.version.c_str(), info_length_bytes); + *version = info; + return 0; + } + + client_info_t find_available_client(PCWSTR browser_dir) const { + if (browser_dir) { + return find_embedded_client(api_version, browser_dir); + } + auto found_client = + find_installed_client(api_version, true, default_release_channel_guid); + if (!found_client.found) { + found_client = find_installed_client(api_version, false, + default_release_channel_guid); + } + return found_client; + } + + std::wstring make_client_dll_path(const std::wstring &dir) const { + auto dll_path = dir; + if (!dll_path.empty()) { + auto last_char = dir[dir.size() - 1]; + if (last_char != L'\\' && last_char != L'/') { + dll_path += L'\\'; + } + } + dll_path += L"EBWebView\\"; +#if defined(_M_X64) || defined(__x86_64__) + dll_path += L"x64"; +#elif defined(_M_IX86) || defined(__i386__) + dll_path += L"x86"; +#elif defined(_M_ARM64) || defined(__aarch64__) + dll_path += L"arm64"; +#else +#error WebView2 integration for this platform is not yet supported. +#endif + dll_path += L"\\EmbeddedBrowserWebView.dll"; + return dll_path; + } + + client_info_t + find_installed_client(unsigned int min_api_version, bool system, + const std::wstring &release_channel) const { + std::wstring sub_key = client_state_reg_sub_key; + sub_key += release_channel; + auto root_key = system ? HKEY_LOCAL_MACHINE : HKEY_CURRENT_USER; + reg_key key(root_key, sub_key, 0, KEY_READ | KEY_WOW64_32KEY); + if (!key.is_open()) { + return {}; + } + auto ebwebview_value = key.query_string(L"EBWebView"); + + auto client_version_string = + get_last_native_path_component(ebwebview_value); + auto client_version = parse_version(client_version_string); + if (client_version[2] < min_api_version) { + // Our API version is greater than the runtime API version. + return {}; + } + + auto client_dll_path = make_client_dll_path(ebwebview_value); + return {true, client_dll_path, client_version_string, + webview2_runtime_type::installed}; + } + + client_info_t find_embedded_client(unsigned int min_api_version, + const std::wstring &dir) const { + auto client_dll_path = make_client_dll_path(dir); + + auto client_version_string = get_file_version_string(client_dll_path); + auto client_version = parse_version(client_version_string); + if (client_version[2] < min_api_version) { + // Our API version is greater than the runtime API version. + return {}; + } + + return {true, client_dll_path, client_version_string, + webview2_runtime_type::embedded}; + } + + // The minimum WebView2 API version we need regardless of the SDK release + // actually used. The number comes from the SDK release version, + // e.g. 1.0.1150.38. To be safe the SDK should have a number that is greater + // than or equal to this number. The Edge browser webview client must + // have a number greater than or equal to this number. + static constexpr unsigned int api_version = 1150; + + static constexpr auto client_state_reg_sub_key = + L"SOFTWARE\\Microsoft\\EdgeUpdate\\ClientState\\"; + + // GUID for the stable release channel. + static constexpr auto stable_release_guid = + L"{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}"; + + static constexpr auto default_release_channel_guid = stable_release_guid; +#endif /* WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL */ + +#if WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK == 1 + native_library m_lib{L"WebView2Loader.dll"}; +#endif +}; + +namespace cast_info { +static constexpr auto controller_completed = + cast_info_t{ + IID_ICoreWebView2CreateCoreWebView2ControllerCompletedHandler}; + +static constexpr auto environment_completed = + cast_info_t{ + IID_ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler}; + +static constexpr auto message_received = + cast_info_t{ + IID_ICoreWebView2WebMessageReceivedEventHandler}; + +static constexpr auto permission_requested = + cast_info_t{ + IID_ICoreWebView2PermissionRequestedEventHandler}; +} // namespace cast_info +} // namespace mswebview2 + class webview2_com_handler : public ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler, public ICoreWebView2CreateCoreWebView2ControllerCompletedHandler, @@ -1153,18 +2341,56 @@ public: return 0; } HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, LPVOID *ppv) { + using namespace mswebview2::cast_info; + if (!ppv) { return E_POINTER; } - *ppv = nullptr; + + // All of the COM interfaces we implement should be added here regardless + // of whether they are required. + // This is just to be on the safe side in case the WebView2 Runtime ever + // requests a pointer to an interface we implement. + // The WebView2 Runtime must at the very least be able to get a pointer to + // ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler when we use + // our custom WebView2 loader implementation, and observations have shown + // that it is the only interface requested in this case. None have been + // observed to be requested when using the official WebView2 loader. + + if (cast_if_equal_iid(riid, controller_completed, ppv) || + cast_if_equal_iid(riid, environment_completed, ppv) || + cast_if_equal_iid(riid, message_received, ppv) || + cast_if_equal_iid(riid, permission_requested, ppv)) { + return S_OK; + } + return E_NOINTERFACE; } HRESULT STDMETHODCALLTYPE Invoke(HRESULT res, ICoreWebView2Environment *env) { - env->CreateCoreWebView2Controller(m_window, this); + if (SUCCEEDED(res)) { + res = env->CreateCoreWebView2Controller(m_window, this); + if (SUCCEEDED(res)) { + return S_OK; + } + } + try_create_environment(); return S_OK; } HRESULT STDMETHODCALLTYPE Invoke(HRESULT res, ICoreWebView2Controller *controller) { + if (FAILED(res)) { + // See try_create_environment() regarding + // HRESULT_FROM_WIN32(ERROR_INVALID_STATE). + // The result is E_ABORT if the parent window has been destroyed already. + switch (res) { + case HRESULT_FROM_WIN32(ERROR_INVALID_STATE): + case E_ABORT: + return S_OK; + } + try_create_environment(); + return S_OK; + } + ICoreWebView2 *webview; ::EventRegistrationToken token; controller->get_CoreWebView2(&webview); @@ -1184,8 +2410,9 @@ public: CoTaskMemFree(message); return S_OK; } - HRESULT STDMETHODCALLTYPE Invoke( - ICoreWebView2 *sender, ICoreWebView2PermissionRequestedEventArgs *args) { + HRESULT STDMETHODCALLTYPE + Invoke(ICoreWebView2 * /*sender*/, + ICoreWebView2PermissionRequestedEventArgs *args) { COREWEBVIEW2_PERMISSION_KIND kind; args->get_PermissionKind(&kind); if (kind == COREWEBVIEW2_PERMISSION_KIND_CLIPBOARD_READ) { @@ -1194,11 +2421,64 @@ public: return S_OK; } + // Checks whether the specified IID equals the IID of the specified type and + // if so casts the "this" pointer to T and returns it. Returns nullptr on + // mismatching IIDs. + // If ppv is specified then the pointer will also be assigned to *ppv. + template + T *cast_if_equal_iid(REFIID riid, const cast_info_t &info, + LPVOID *ppv = nullptr) noexcept { + T *ptr = nullptr; + if (IsEqualIID(riid, info.iid)) { + ptr = static_cast(this); + ptr->AddRef(); + } + if (ppv) { + *ppv = ptr; + } + return ptr; + } + + // Set the function that will perform the initiating logic for creating + // the WebView2 environment. + void set_attempt_handler(std::function attempt_handler) noexcept { + m_attempt_handler = attempt_handler; + } + + // Retry creating a WebView2 environment. + // The initiating logic for creating the environment is defined by the + // caller of set_attempt_handler(). + void try_create_environment() noexcept { + // WebView creation fails with HRESULT_FROM_WIN32(ERROR_INVALID_STATE) if + // a running instance using the same user data folder exists, and the + // Environment objects have different EnvironmentOptions. + // Source: https://docs.microsoft.com/en-us/microsoft-edge/webview2/reference/win32/icorewebview2environment?view=webview2-1.0.1150.38 + if (m_attempts < m_max_attempts) { + ++m_attempts; + auto res = m_attempt_handler(); + if (SUCCEEDED(res)) { + return; + } + // Not entirely sure if this error code only applies to + // CreateCoreWebView2Controller so we check here as well. + if (res == HRESULT_FROM_WIN32(ERROR_INVALID_STATE)) { + return; + } + try_create_environment(); + return; + } + // Give up. + m_cb(nullptr, nullptr); + } + private: HWND m_window; msg_cb_t m_msgCb; webview2_com_handler_cb_t m_cb; std::atomic m_ref_count{1}; + std::function m_attempt_handler; + unsigned int m_max_attempts = 5; + unsigned int m_attempts = 0; }; class win32_edge_engine { @@ -1207,12 +2487,16 @@ public: if (!is_webview2_available()) { return; } - if (!m_com_init.is_initialized()) { - return; - } - enable_dpi_awareness(); - if (window == nullptr) { - HINSTANCE hInstance = GetModuleHandle(nullptr); + + HINSTANCE hInstance = GetModuleHandle(nullptr); + + if (!window) { + m_com_init = {COINIT_APARTMENTTHREADED}; + if (!m_com_init.is_initialized()) { + return; + } + enable_dpi_awareness(); + HICON icon = (HICON)LoadImage( hInstance, IDI_APPLICATION, IMAGE_ICON, GetSystemMetrics(SM_CXICON), GetSystemMetrics(SM_CYICON), LR_DEFAULTCOLOR); @@ -1223,49 +2507,138 @@ public: wc.hInstance = hInstance; wc.lpszClassName = L"webview"; wc.hIcon = icon; - wc.lpfnWndProc = - (WNDPROC)(+[](HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) -> LRESULT { - auto w = (win32_edge_engine *)GetWindowLongPtr(hwnd, GWLP_USERDATA); - switch (msg) { - case WM_SIZE: - w->resize(hwnd); - break; - case WM_CLOSE: - DestroyWindow(hwnd); - break; - case WM_DESTROY: - w->terminate(); - break; - case WM_GETMINMAXINFO: { - auto lpmmi = (LPMINMAXINFO)lp; - if (w == nullptr) { - return 0; - } - if (w->m_maxsz.x > 0 && w->m_maxsz.y > 0) { - lpmmi->ptMaxSize = w->m_maxsz; - lpmmi->ptMaxTrackSize = w->m_maxsz; - } - if (w->m_minsz.x > 0 && w->m_minsz.y > 0) { - lpmmi->ptMinTrackSize = w->m_minsz; - } - } break; - default: - return DefWindowProcW(hwnd, msg, wp, lp); - } + wc.lpfnWndProc = (WNDPROC)(+[](HWND hwnd, UINT msg, WPARAM wp, + LPARAM lp) -> LRESULT { + win32_edge_engine *w{}; + + if (msg == WM_NCCREATE) { + auto *lpcs{reinterpret_cast(lp)}; + w = static_cast(lpcs->lpCreateParams); + w->m_window = hwnd; + SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast(w)); + enable_non_client_dpi_scaling_if_needed(hwnd); + apply_window_theme(hwnd); + } else { + w = reinterpret_cast( + GetWindowLongPtrW(hwnd, GWLP_USERDATA)); + } + + if (!w) { + return DefWindowProcW(hwnd, msg, wp, lp); + } + + switch (msg) { + case WM_SIZE: + w->resize_widget(); + break; + case WM_CLOSE: + DestroyWindow(hwnd); + break; + case WM_DESTROY: + if (w->dec_window_count() <= 0) { + w->terminate(); + } + break; + case WM_GETMINMAXINFO: { + auto lpmmi = (LPMINMAXINFO)lp; + if (w == nullptr) { return 0; - }); + } + if (w->m_maxsz.x > 0 && w->m_maxsz.y > 0) { + lpmmi->ptMaxSize = w->m_maxsz; + lpmmi->ptMaxTrackSize = w->m_maxsz; + } + if (w->m_minsz.x > 0 && w->m_minsz.y > 0) { + lpmmi->ptMinTrackSize = w->m_minsz; + } + } break; + case 0x02E4 /*WM_GETDPISCALEDSIZE*/: { + auto dpi = static_cast(wp); + auto *size{reinterpret_cast(lp)}; + *size = w->get_scaled_size(w->m_dpi, dpi); + return TRUE; + } + case 0x02E0 /*WM_DPICHANGED*/: { + // Windows 10: The size we get here is exactly what we supplied to WM_GETDPISCALEDSIZE. + // Windows 11: The size we get here is NOT what we supplied to WM_GETDPISCALEDSIZE. + // Due to this difference, don't use the suggested bounds. + auto dpi = static_cast(HIWORD(wp)); + w->on_dpi_changed(dpi); + break; + } + case WM_SETTINGCHANGE: { + auto *area = reinterpret_cast(lp); + if (area) { + w->on_system_setting_change(area); + } + break; + } + default: + return DefWindowProcW(hwnd, msg, wp, lp); + } + return 0; + }); RegisterClassExW(&wc); - m_window = CreateWindowW(L"webview", L"", WS_OVERLAPPEDWINDOW, - CW_USEDEFAULT, CW_USEDEFAULT, 640, 480, nullptr, - nullptr, hInstance, nullptr); + + CreateWindowW(L"webview", L"", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, + CW_USEDEFAULT, 0, 0, nullptr, nullptr, hInstance, this); if (m_window == nullptr) { return; } - SetWindowLongPtr(m_window, GWLP_USERDATA, (LONG_PTR)this); + inc_window_count(); + + m_dpi = get_window_dpi(m_window); + constexpr const int initial_width = 640; + constexpr const int initial_height = 480; + set_size(initial_width, initial_height, WEBVIEW_HINT_NONE); } else { m_window = *(static_cast(window)); + m_dpi = get_window_dpi(m_window); } + // Create a message-only window for internal messaging. + WNDCLASSEXW message_wc{}; + message_wc.cbSize = sizeof(WNDCLASSEX); + message_wc.hInstance = hInstance; + message_wc.lpszClassName = L"webview_message"; + message_wc.lpfnWndProc = (WNDPROC)(+[](HWND hwnd, UINT msg, WPARAM wp, + LPARAM lp) -> LRESULT { + win32_edge_engine *w{}; + + if (msg == WM_NCCREATE) { + auto *lpcs{reinterpret_cast(lp)}; + w = static_cast(lpcs->lpCreateParams); + w->m_message_window = hwnd; + SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast(w)); + } else { + w = reinterpret_cast( + GetWindowLongPtrW(hwnd, GWLP_USERDATA)); + } + + if (!w) { + return DefWindowProcW(hwnd, msg, wp, lp); + } + + switch (msg) { + case WM_APP: + if (auto f = (dispatch_fn_t *)(lp)) { + (*f)(); + delete f; + } + break; + case WM_DESTROY: + w->m_message_window = nullptr; + SetWindowLongPtrW(hwnd, GWLP_USERDATA, 0); + break; + default: + return DefWindowProcW(hwnd, msg, wp, lp); + } + return 0; + }); + RegisterClassExW(&message_wc); + CreateWindowExW(0, L"webview_message", nullptr, 0, 0, 0, 0, 0, HWND_MESSAGE, + nullptr, hInstance, this); + ShowWindow(m_window, SW_SHOW); UpdateWindow(m_window); SetFocus(m_window); @@ -1274,7 +2647,7 @@ public: std::bind(&win32_edge_engine::on_message, this, std::placeholders::_1); embed(m_window, debug, cb); - resize(m_window); + resize_widget(); m_controller->MoveFocus(COREWEBVIEW2_MOVE_FOCUS_REASON_PROGRAMMATIC); } @@ -1300,26 +2673,15 @@ public: void run() { MSG msg; - BOOL res; - while ((res = GetMessage(&msg, nullptr, 0, 0)) != -1) { - if (msg.hwnd) { - TranslateMessage(&msg); - DispatchMessage(&msg); - continue; - } - if (msg.message == WM_APP) { - auto f = (dispatch_fn_t *)(msg.lParam); - (*f)(); - delete f; - } else if (msg.message == WM_QUIT) { - return; - } + while (GetMessageW(&msg, nullptr, 0, 0) > 0) { + TranslateMessage(&msg); + DispatchMessageW(&msg); } } void *window() { return (void *)m_window; } void terminate() { PostQuitMessage(0); } void dispatch(dispatch_fn_t f) { - PostThreadMessage(m_main_thread, WM_APP, 0, (LPARAM) new dispatch_fn_t(f)); + PostMessageW(m_message_window, WM_APP, 0, (LPARAM) new dispatch_fn_t(f)); } void set_title(const std::string &title) { @@ -1342,15 +2704,15 @@ public: m_minsz.x = width; m_minsz.y = height; } else { - RECT r; - r.left = r.top = 0; - r.right = width; - r.bottom = height; - AdjustWindowRect(&r, WS_OVERLAPPEDWINDOW, 0); - SetWindowPos( - m_window, NULL, r.left, r.top, r.right - r.left, r.bottom - r.top, - SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOMOVE | SWP_FRAMECHANGED); - resize(m_window); + auto dpi = get_window_dpi(m_window); + m_dpi = dpi; + auto scaled_size = + scale_size(width, height, get_default_window_dpi(), dpi); + auto frame_size = + make_window_frame_size(m_window, scaled_size.cx, scaled_size.cy, dpi); + SetWindowPos(m_window, nullptr, 0, 0, frame_size.cx, frame_size.cy, + SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOMOVE | + SWP_FRAMECHANGED); } } @@ -1379,11 +2741,12 @@ private: flag.test_and_set(); wchar_t currentExePath[MAX_PATH]; - GetModuleFileNameW(NULL, currentExePath, MAX_PATH); + GetModuleFileNameW(nullptr, currentExePath, MAX_PATH); wchar_t *currentExeName = PathFindFileNameW(currentExePath); wchar_t dataPath[MAX_PATH]; - if (!SUCCEEDED(SHGetFolderPathW(NULL, CSIDL_APPDATA, NULL, 0, dataPath))) { + if (!SUCCEEDED( + SHGetFolderPathW(nullptr, CSIDL_APPDATA, nullptr, 0, dataPath))) { return false; } wchar_t userDataFolder[MAX_PATH]; @@ -1392,24 +2755,37 @@ private: m_com_handler = new webview2_com_handler( wnd, cb, [&](ICoreWebView2Controller *controller, ICoreWebView2 *webview) { + if (!controller || !webview) { + flag.clear(); + return; + } controller->AddRef(); webview->AddRef(); m_controller = controller; m_webview = webview; flag.clear(); }); - HRESULT res = CreateCoreWebView2EnvironmentWithOptions( - nullptr, userDataFolder, nullptr, m_com_handler); - if (res != S_OK) { + + m_com_handler->set_attempt_handler([&] { + return m_webview2_loader.create_environment_with_options( + nullptr, userDataFolder, nullptr, m_com_handler); + }); + m_com_handler->try_create_environment(); + + // Pump the message loop until WebView2 has finished initialization. + MSG msg; + while (flag.test_and_set() && GetMessageW(&msg, nullptr, 0, 0) >= 0) { + if (msg.message == WM_QUIT) { + return false; + } + TranslateMessage(&msg); + DispatchMessageW(&msg); + } + if (!m_controller || !m_webview) { return false; } - MSG msg = {}; - while (flag.test_and_set() && GetMessage(&msg, NULL, 0, 0)) { - TranslateMessage(&msg); - DispatchMessage(&msg); - } ICoreWebView2Settings *settings = nullptr; - res = m_webview->get_Settings(&settings); + auto res = m_webview->get_Settings(&settings); if (res != S_OK) { return false; } @@ -1417,23 +2793,27 @@ private: if (res != S_OK) { return false; } + res = settings->put_IsStatusBarEnabled(FALSE); + if (res != S_OK) { + return false; + } init("window.external={invoke:s=>window.chrome.webview.postMessage(s)}"); return true; } - void resize(HWND wnd) { + void resize_widget() { if (m_controller == nullptr) { return; } RECT bounds; - GetClientRect(wnd, &bounds); + GetClientRect(m_window, &bounds); m_controller->put_Bounds(bounds); } - static bool is_webview2_available() noexcept { + bool is_webview2_available() const noexcept { LPWSTR version_info = nullptr; - auto res = - GetAvailableCoreWebView2BrowserVersionString(nullptr, &version_info); + auto res = m_webview2_loader.get_available_browser_version_string( + nullptr, &version_info); // The result will be equal to HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) // if the WebView2 runtime is not installed. auto ok = SUCCEEDED(res) && version_info; @@ -1443,19 +2823,66 @@ private: return ok; } + void on_dpi_changed(int dpi) { + auto scaled_size = get_scaled_size(m_dpi, dpi); + auto frame_size = + make_window_frame_size(m_window, scaled_size.cx, scaled_size.cy, dpi); + SetWindowPos(m_window, nullptr, 0, 0, frame_size.cx, frame_size.cy, + SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOMOVE | SWP_FRAMECHANGED); + m_dpi = dpi; + } + + SIZE get_size() const { + RECT bounds; + GetClientRect(m_window, &bounds); + auto width = bounds.right - bounds.left; + auto height = bounds.bottom - bounds.top; + return {width, height}; + } + + SIZE get_scaled_size(int from_dpi, int to_dpi) const { + auto size = get_size(); + return scale_size(size.cx, size.cy, from_dpi, to_dpi); + } + + void on_system_setting_change(const wchar_t *area) { + // Detect light/dark mode change in system. + if (lstrcmpW(area, L"ImmersiveColorSet") == 0) { + apply_window_theme(m_window); + } + } + + static std::atomic_uint &window_ref_count() { + static std::atomic_uint ref_count{0}; + return ref_count; + } + + static unsigned int inc_window_count() { return ++window_ref_count(); } + + static unsigned int dec_window_count() { + auto &count = window_ref_count(); + if (count > 0) { + return --count; + } + return 0; + } + virtual void on_message(const std::string &msg) = 0; // The app is expected to call CoInitializeEx before // CreateCoreWebView2EnvironmentWithOptions. // Source: https://docs.microsoft.com/en-us/microsoft-edge/webview2/reference/win32/webview2-idl#createcorewebview2environmentwithoptions - com_init_wrapper m_com_init{COINIT_APARTMENTTHREADED}; - HWND m_window = NULL; + com_init_wrapper m_com_init; + HWND m_window = nullptr; + HWND m_message_window = nullptr; POINT m_minsz = POINT{0, 0}; POINT m_maxsz = POINT{0, 0}; DWORD m_main_thread = GetCurrentThreadId(); ICoreWebView2 *m_webview = nullptr; ICoreWebView2Controller *m_controller = nullptr; webview2_com_handler *m_com_handler = nullptr; + mswebview2::loader m_webview2_loader; + int m_dpi{}; }; } // namespace detail @@ -1474,7 +2901,7 @@ public: : browser_engine(debug, wnd) {} void navigate(const std::string &url) { - if (url == "") { + if (url.empty()) { browser_engine::navigate("about:blank"); return; } @@ -1484,71 +2911,31 @@ public: using binding_t = std::function; class binding_ctx_t { public: - binding_ctx_t(binding_t *callback, void *arg, bool sync = true) - : callback(callback), arg(arg), sync(sync) {} + binding_ctx_t(binding_t callback, void *arg) + : callback(callback), arg(arg) {} // This function is called upon execution of the bound JS function - binding_t *callback; + binding_t callback; // This user-supplied argument is passed to the callback void *arg; - // This boolean expresses whether or not this binding is synchronous or asynchronous - // Async bindings require the user to call the resolve function, sync bindings don't - bool sync; }; using sync_binding_t = std::function; - using sync_binding_ctx_t = std::pair; // Synchronous bind void bind(const std::string &name, sync_binding_t fn) { - if (bindings.count(name) == 0) { - bindings[name] = new binding_ctx_t( - new binding_t( - [](const std::string &seq, const std::string &req, void *arg) { - auto pair = static_cast(arg); - pair->first->resolve(seq, 0, pair->second(req)); - }), - new sync_binding_ctx_t(this, fn)); - bind_js(name); - } + auto wrapper = [this, fn](const std::string &seq, const std::string &req, + void * /*arg*/) { resolve(seq, 0, fn(req)); }; + bind(name, wrapper, nullptr); } // Asynchronous bind - void bind(const std::string &name, binding_t f, void *arg) { - if (bindings.count(name) == 0) { - bindings[name] = new binding_ctx_t(new binding_t(f), arg, false); - bind_js(name); + void bind(const std::string &name, binding_t fn, void *arg) { + // NOLINTNEXTLINE(readability-container-contains): contains() requires C++20 + if (bindings.count(name) > 0) { + return; } - } - - void unbind(const std::string &name) { - if (bindings.find(name) != bindings.end()) { - auto js = "delete window['" + name + "'];"; - init(js); - eval(js); - delete bindings[name]->callback; - if (bindings[name]->sync) { - delete static_cast(bindings[name]->arg); - } - delete bindings[name]; - bindings.erase(name); - } - } - - void resolve(const std::string &seq, int status, const std::string &result) { - dispatch([seq, status, result, this]() { - if (status == 0) { - eval("window._rpc[" + seq + "].resolve(" + result + - "); delete window._rpc[" + seq + "]"); - } else { - eval("window._rpc[" + seq + "].reject(" + result + - "); delete window._rpc[" + seq + "]"); - } - }); - } - -private: - void bind_js(const std::string &name) { - auto js = "(function() { var name = '" + name + "';" + R"( + bindings.emplace(name, binding_ctx_t(fn, arg)); + auto js = "(function() { var name = '" + name + "';" + R""( var RPC = window._rpc = (window._rpc || {nextSeq: 1}); window[name] = function() { var seq = RPC.nextSeq++; @@ -1565,23 +2952,70 @@ private: })); return promise; } - })())"; + })())""; init(js); eval(js); } + void unbind(const std::string &name) { + auto found = bindings.find(name); + if (found != bindings.end()) { + auto js = "delete window['" + name + "'];"; + init(js); + eval(js); + bindings.erase(found); + } + } + + void resolve(const std::string &seq, int status, const std::string &result) { + // NOLINTNEXTLINE(modernize-avoid-bind): Lambda with move requires C++14 + dispatch(std::bind( + [seq, status, this](std::string escaped_result) { + std::string js; + js += "(function(){var seq = \""; + js += seq; + js += "\";\n"; + js += "var status = "; + js += std::to_string(status); + js += ";\n"; + js += "var result = "; + js += escaped_result; + js += R"js(; +var promise = window._rpc[seq]; +delete window._rpc[seq]; +if (result !== undefined) { + try { + result = JSON.parse(result); + } catch { + promise.reject(new Error("Failed to parse binding result as JSON")); + return; + } +} +if (status === 0) { + promise.resolve(result); +} else { + promise.reject(result); +} +})())js"; + eval(js); + }, + result.empty() ? "undefined" : detail::json_escape(result))); + } + +private: void on_message(const std::string &msg) { auto seq = detail::json_parse(msg, "id", 0); auto name = detail::json_parse(msg, "method", 0); auto args = detail::json_parse(msg, "params", 0); - if (bindings.find(name) == bindings.end()) { + auto found = bindings.find(name); + if (found == bindings.end()) { return; } - auto fn = bindings[name]; - (*fn->callback)(seq, args, fn->arg); + const auto &context = found->second; + context.callback(seq, args, context.arg); } - std::map bindings; + std::map bindings; }; } // namespace webview @@ -1661,6 +3095,10 @@ WEBVIEW_API void webview_return(webview_t w, const char *seq, int status, static_cast(w)->resolve(seq, status, result); } +WEBVIEW_API const webview_version_info_t *webview_version(void) { + return &webview::detail::library_version_info; +} + #endif /* WEBVIEW_HEADER */ #endif /* __cplusplus */ #endif /* WEBVIEW_H */