From 594d19e21602922a8a997d5aa5635b098e4a9b09 Mon Sep 17 00:00:00 2001 From: SanicBTW Date: Sun, 18 Feb 2024 09:00:36 +0100 Subject: [PATCH] v0.0.5 --- README.md | 19 +- haxelib.json | 4 +- internal/WebViewHelper.cpp | 14 + internal/vendor/webview.h | 1320 ++++++++++++++++++-------- source/webview/WebView.hx | 71 +- source/webview/internal/WVExterns.hx | 49 +- 6 files changed, 1008 insertions(+), 469 deletions(-) diff --git a/README.md b/README.md index 6496935..d143f9a 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,25 @@ or with git for the latest potentially unstable updates. haxelib git HxWebView https://github.com/SanicBTW/HxWebView.git ``` +## Linux Usage +In order to use the library in Linux you must have `webkit2gtk` and `gtk3` installed in your system. + +You can check [this](https://github.com/webview/webview?tab=readme-ov-file#linux-and-bsd) file to see the specific name libraries for your distro. + +You MUST include `NO_PRECOMPILED_HEADERS` to your defines in order to compile without any error. + +### Regarding embedding +With the current header file, you should be able to embed the WebView into an existing window in any platform but it requires using widgets. + +As I currently don't know how to use widgets on Haxe, embedding is somewhat a difficult topic. + +However I will keep looking for a way to do it and keep y'all updated. + ### Usage examples Check out the [examples folder](https://github.com/SanicBTW/HxWebView/tree/master/examples) for examples on how you can use these webview bindings, the examples are the same as the [official](https://github.com/webview/webview/tree/master/examples) ones. ### Licensing -`HxWebView` is made available via the [MIT](https://github.com/SanicBTW/HxWebView/blob/master/LICENSE) license, the same license as [webview](https://github.com/webview/webview/blob/master/LICENSE). \ No newline at end of file +`HxWebView` is made available via the [MIT](https://github.com/SanicBTW/HxWebView/blob/master/LICENSE) license, the same license as [webview](https://github.com/webview/webview/blob/master/LICENSE). + +--- +Using [15/02](https://github.com/webview/webview/commit/c4833a42d30fecac6d8cbe5e4932dd4eed6bcab3) header file \ No newline at end of file diff --git a/haxelib.json b/haxelib.json index 17c1a8b..b30cae1 100644 --- a/haxelib.json +++ b/haxelib.json @@ -4,8 +4,8 @@ "license": "MIT", "tags": ["haxe", "webview", "cpp", "binds", "bindings", "native", "desktop", "cross"], "description": "Haxe cross-platform desktop bindings for webview.", - "version": "0.0.4", - "releasenote": "Lime support, HaxeFlixel example.", + "version": "0.0.5", + "releasenote": "Updated header file and cleaned some code.", "contributors": ["SanicBTW", "Vortex"], "classPath": "source", "dependencies": { diff --git a/internal/WebViewHelper.cpp b/internal/WebViewHelper.cpp index b95c175..7ba6494 100644 --- a/internal/WebViewHelper.cpp +++ b/internal/WebViewHelper.cpp @@ -36,6 +36,20 @@ Dynamic hx_webview_version() return out; } +// Fix for webview_get_native_handle +// Had to go with ints since I was breaking my head with the type of enum n shi (it was giving me ObjectPtr) +void *hx_get_native_handle(webview_t w, int kind) +{ + switch (kind) + { + case 0: return webview_get_native_handle(w, WEBVIEW_NATIVE_HANDLE_KIND_UI_WINDOW); + case 1: return webview_get_native_handle(w, WEBVIEW_NATIVE_HANDLE_KIND_UI_WIDGET); + case 2: return webview_get_native_handle(w, WEBVIEW_NATIVE_HANDLE_KIND_BROWSER_CONTROLLER); + default: return nullptr; + } + return nullptr; +} + // Wrapper for webview_dispatch using hxDispatchFunc = std::function; diff --git a/internal/vendor/webview.h b/internal/vendor/webview.h index 4fc1558..a684aa7 100644 --- a/internal/vendor/webview.h +++ b/internal/vendor/webview.h @@ -109,14 +109,24 @@ 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). 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 -// passed here. Returns null on failure. Creation can fail for various reasons -// such as when required runtime dependencies are missing or when window creation +// Creates a new webview instance. If the debug parameter is non-zero, +// developer tools are enabled if supported by the backend. The optional window +// parameter can be a native window handle, i.e. GtkWindow pointer (GTK), +// NSWindow pointer (Cocoa) or HWND (Win32). If the window handle is +// non-null, the webview widget is embedded into the given window, and the +// caller is expected to assume responsibility for the window as well as +// application lifecycle. If the window handle is null, a new window is created +// and both the window and application lifecycle are managed by the webview +// instance. Returns null on failure. Creation can fail for various reasons such +// as when required runtime dependencies are missing or when window creation // fails. +// Remarks: +// - Win32: The function also accepts a pointer to HWND (Win32) in the window +// parameter for backward compatibility. +// - Win32/WebView2: CoInitializeEx should be called with +// COINIT_APARTMENTTHREADED before attempting to call this function with an +// existing window. Omitting this step may cause WebView2 initialization to +// fail. WEBVIEW_API webview_t webview_create(int debug, void *window); // Destroys a webview and closes the native window. @@ -135,11 +145,27 @@ 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 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. +// Returns the native handle of the window associated with the webview instance. +// The handle can be a GtkWindow pointer (GTK), NSWindow pointer (Cocoa) or +// HWND (Win32). WEBVIEW_API void *webview_get_window(webview_t w); +// Native handle kind. The actual type depends on the backend. +typedef enum { + // Top-level window. GtkWindow pointer (GTK), NSWindow pointer (Cocoa) or HWND (Win32). + WEBVIEW_NATIVE_HANDLE_KIND_UI_WINDOW, + // Browser widget. GtkWidget pointer (GTK), NSView pointer (Cocoa) or HWND (Win32). + WEBVIEW_NATIVE_HANDLE_KIND_UI_WIDGET, + // Browser controller. WebKitWebView pointer (WebKitGTK), WKWebView pointer (Cocoa/WebKit) or + // ICoreWebView2Controller pointer (Win32/WebView2). + WEBVIEW_NATIVE_HANDLE_KIND_BROWSER_CONTROLLER +} webview_native_handle_kind_t; + +// Returns a native handle of choice. +// @since 0.11 +WEBVIEW_API void *webview_get_native_handle(webview_t w, + webview_native_handle_kind_t kind); + // Updates the title of the native window. Must be called from the UI thread. WEBVIEW_API void webview_set_title(webview_t w, const char *title); @@ -230,8 +256,10 @@ WEBVIEW_API const webview_version_info_t *webview_version(void); WEBVIEW_DEPRECATED("Private API should not be used") #endif +#include #include #include +#include #include #include #include @@ -242,6 +270,13 @@ WEBVIEW_API const webview_version_info_t *webview_version(void); #include +#if defined(_WIN32) +#define WIN32_LEAN_AND_MEAN +#include +#else +#include +#endif + namespace webview { using dispatch_fn_t = std::function; @@ -255,6 +290,58 @@ constexpr const webview_version_info_t library_version_info{ 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 { @@ -397,27 +484,24 @@ inline int json_parse_c(const char *s, size_t sz, const char *key, size_t keysz, return -1; } -constexpr bool is_json_special_char(unsigned int c) { - return c == '"' || c == '\\'; +constexpr bool is_json_special_char(char c) { + return c == '"' || c == '\\' || c == '\b' || c == '\f' || c == '\n' || + c == '\r' || c == '\t'; } -constexpr bool is_control_char(unsigned int c) { - return c <= 0x1f || (c >= 0x7f && c <= 0x9f); -} +constexpr bool is_ascii_control_char(char c) { return c >= 0 && c <= 0x1f; } 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); + size_t required_length = add_quotes ? 2 : 0; for (auto c : s) { - auto uc = static_cast(c); - if (is_json_special_char(uc)) { + if (is_json_special_char(c)) { // '\' and a single following character required_length += 2; continue; } - if (is_control_char(uc)) { + if (is_ascii_control_char(c)) { // '\', 'u', 4 digits required_length += 6; continue; @@ -432,13 +516,23 @@ inline std::string json_escape(const std::string &s, bool add_quotes = true) { } // Copy string while escaping characters. for (auto c : s) { - auto uc = static_cast(c); - if (is_json_special_char(uc)) { + if (is_json_special_char(c)) { + static constexpr char special_escape_table[256] = + "\0\0\0\0\0\0\0\0btn\0fr\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\"\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\\"; result += '\\'; - result += c; + // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-constant-array-index) + result += special_escape_table[static_cast(c)]; continue; } - if (is_control_char(uc)) { + if (is_ascii_control_char(c)) { + // Escape as \u00xx + static constexpr char hex_alphabet[]{"0123456789abcdef"}; + auto uc = static_cast(c); auto h = (uc >> 4) & 0x0f; auto l = uc & 0x0f; result += "\\u00"; @@ -453,6 +547,8 @@ inline std::string json_escape(const std::string &s, bool add_quotes = true) { if (add_quotes) { result += '"'; } + // Should have calculated the exact amount of memory needed + assert(required_length == result.size()); return result; } @@ -537,6 +633,300 @@ 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) { *this = std::move(other); } + + native_library &operator=(native_library &&other) { + if (this == &other) { + return *this; + } + m_handle = other.m_handle; + other.m_handle = nullptr; + return *this; + } + + // 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{}; +}; + +class engine_base { +public: + virtual ~engine_base() = default; + + void navigate(const std::string &url) { + if (url.empty()) { + navigate_impl("about:blank"); + return; + } + navigate_impl(url); + } + + using binding_t = std::function; + class binding_ctx_t { + public: + 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; + // This user-supplied argument is passed to the callback + void *arg; + }; + + using sync_binding_t = std::function; + + // Synchronous bind + void bind(const std::string &name, sync_binding_t fn) { + 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 fn, void *arg) { + // NOLINTNEXTLINE(readability-container-contains): contains() requires C++20 + if (bindings.count(name) > 0) { + return; + } + 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++; + var promise = new Promise(function(resolve, reject) { + RPC[seq] = { + resolve: resolve, + reject: reject, + }; + }); + window.external.invoke(JSON.stringify({ + id: seq, + method: name, + params: Array.prototype.slice.call(arguments), + })); + 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" : json_escape(result))); + } + + void *window() { return window_impl(); } + void *widget() { return widget_impl(); } + void *browser_controller() { return browser_controller_impl(); }; + void run() { run_impl(); } + void terminate() { terminate_impl(); } + void dispatch(std::function f) { dispatch_impl(f); } + void set_title(const std::string &title) { set_title_impl(title); } + + void set_size(int width, int height, int hints) { + set_size_impl(width, height, hints); + } + + void set_html(const std::string &html) { set_html_impl(html); } + void init(const std::string &js) { init_impl(js); } + void eval(const std::string &js) { eval_impl(js); } + +protected: + virtual void navigate_impl(const std::string &url) = 0; + virtual void *window_impl() = 0; + virtual void *widget_impl() = 0; + virtual void *browser_controller_impl() = 0; + virtual void run_impl() = 0; + virtual void terminate_impl() = 0; + virtual void dispatch_impl(std::function f) = 0; + virtual void set_title_impl(const std::string &title) = 0; + virtual void set_size_impl(int width, int height, int hints) = 0; + virtual void set_html_impl(const std::string &html) = 0; + virtual void init_impl(const std::string &js) = 0; + virtual void eval_impl(const std::string &js) = 0; + + virtual void on_message(const std::string &msg) { + auto seq = json_parse(msg, "id", 0); + auto name = json_parse(msg, "method", 0); + auto args = json_parse(msg, "params", 0); + auto found = bindings.find(name); + if (found == bindings.end()) { + return; + } + const auto &context = found->second; + context.callback(seq, args, context.arg); + } + + virtual void on_window_created() { inc_window_count(); } + + virtual void on_window_destroyed() { + if (dec_window_count() <= 0) { + terminate(); + } + } + +private: + 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; + } + + std::map bindings; +}; + } // namespace detail WEBVIEW_DEPRECATED_PRIVATE @@ -574,32 +964,155 @@ 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 { -class gtk_webkit_engine { +// 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 engine_base { public: gtk_webkit_engine(bool debug, void *window) - : m_window(static_cast(window)) { - if (!m_window) { + : m_window(static_cast(window)), m_owns_window{!window} { + if (m_owns_window) { if (gtk_init_check(nullptr, nullptr) == FALSE) { return; } m_window = gtk_window_new(GTK_WINDOW_TOPLEVEL); - inc_window_count(); + on_window_created(); 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(); - } + w->on_window_destroyed(); }), this); } + webkit_dmabuf::apply_webkit_dmabuf_workaround(); // Initialize webview widget m_webview = webkit_web_view_new(); WebKitUserContentManager *manager = @@ -619,7 +1132,7 @@ class gtk_webkit_engine { "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)); @@ -630,13 +1143,36 @@ class gtk_webkit_engine { webkit_settings_set_enable_developer_extras(settings, true); } - gtk_widget_show_all(m_window); + if (m_owns_window) { + gtk_widget_grab_focus(GTK_WIDGET(m_webview)); + gtk_widget_show_all(m_window); + } + } + + gtk_webkit_engine(const gtk_webkit_engine &) = delete; + gtk_webkit_engine &operator=(const gtk_webkit_engine &) = delete; + gtk_webkit_engine(gtk_webkit_engine &&) = delete; + gtk_webkit_engine &operator=(gtk_webkit_engine &&) = delete; + + virtual ~gtk_webkit_engine() { + if (m_webview) { + gtk_widget_destroy(GTK_WIDGET(m_webview)); + m_webview = nullptr; + } + if (m_window) { + if (m_owns_window) { + gtk_window_close(GTK_WINDOW(m_window)); + } + m_window = nullptr; + } } - virtual ~gtk_webkit_engine() = default; - void *window() { return (void *)m_window; } - void run() { gtk_main(); } - void terminate() { gtk_main_quit(); } - void dispatch(std::function f) { + + void *window_impl() override { return (void *)m_window; } + void *widget_impl() override { return (void *)m_webview; } + void *browser_controller_impl() override { return (void *)m_webview; }; + void run_impl() override { gtk_main(); } + void terminate_impl() override { gtk_main_quit(); } + void dispatch_impl(std::function f) override { g_idle_add_full(G_PRIORITY_HIGH_IDLE, (GSourceFunc)([](void *f) -> int { (*static_cast(f))(); return G_SOURCE_REMOVE; @@ -645,11 +1181,11 @@ class gtk_webkit_engine { [](void *f) { delete static_cast(f); }); } - void set_title(const std::string &title) { + void set_title_impl(const std::string &title) override { gtk_window_set_title(GTK_WINDOW(m_window), title.c_str()); } - void set_size(int width, int height, int hints) { + void set_size_impl(int width, int height, int hints) override { gtk_window_set_resizable(GTK_WINDOW(m_window), hints != WEBVIEW_HINT_FIXED); if (hints == WEBVIEW_HINT_NONE) { gtk_window_resize(GTK_WINDOW(m_window), width, height); @@ -666,16 +1202,16 @@ class gtk_webkit_engine { } } - void navigate(const std::string &url) { + void navigate_impl(const std::string &url) override { webkit_web_view_load_uri(WEBKIT_WEB_VIEW(m_webview), url.c_str()); } - void set_html(const std::string &html) { + void set_html_impl(const std::string &html) override { webkit_web_view_load_html(WEBKIT_WEB_VIEW(m_webview), html.c_str(), nullptr); } - void init(const std::string &js) { + void init_impl(const std::string &js) override { WebKitUserContentManager *manager = webkit_web_view_get_user_content_manager(WEBKIT_WEB_VIEW(m_webview)); webkit_user_content_manager_add_script( @@ -685,14 +1221,24 @@ class gtk_webkit_engine { nullptr, nullptr)); } - void eval(const std::string &js) { - webkit_web_view_run_javascript(WEBKIT_WEB_VIEW(m_webview), js.c_str(), - nullptr, nullptr, nullptr); + void eval_impl(const std::string &js) override { + 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: - virtual void on_message(const std::string &msg) = 0; - static char *get_string_from_js_result(WebKitJavascriptResult *r) { char *s; #if (WEBKIT_MAJOR_VERSION == 2 && WEBKIT_MINOR_VERSION >= 22) || \ @@ -711,23 +1257,38 @@ class gtk_webkit_engine { return s; } - static std::atomic_uint &window_ref_count() { - static std::atomic_uint ref_count{0}; - return ref_count; - } + static const native_library &get_webkit_library() { + static const native_library non_loaded_lib; + static native_library loaded_lib; - static unsigned int inc_window_count() { return ++window_ref_count(); } + if (loaded_lib.is_loaded()) { + return loaded_lib; + } - static unsigned int dec_window_count() { - auto &count = window_ref_count(); - if (count > 0) { - return --count; + 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; } - return 0; + + loaded_lib = native_library(*found); + + auto loaded = loaded_lib.is_loaded(); + if (!loaded) { + return non_loaded_lib; + } + + return loaded_lib; } - GtkWidget *m_window; - GtkWidget *m_webview; + bool m_owns_window{}; + GtkWidget *m_window{}; + GtkWidget *m_webview{}; }; } // namespace detail @@ -826,7 +1387,7 @@ inline id operator"" _str(const char *s, std::size_t) { return objc::msg_send("NSString"_cls, "stringWithUTF8String:"_sel, s); } -class cocoa_wkwebview_engine { +class cocoa_wkwebview_engine : public engine_base { public: cocoa_wkwebview_engine(bool debug, void *window) : m_debug{debug}, m_window{static_cast(window)}, m_owns_window{ @@ -834,35 +1395,74 @@ class cocoa_wkwebview_engine { auto app = get_shared_application(); // See comments related to application lifecycle in create_app_delegate(). if (!m_owns_window) { - create_window(); + set_up_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(); + set_up_window(); } else { - delegate = create_app_delegate(); - objc_setAssociatedObject(delegate, "webview", (id)this, + m_app_delegate = create_app_delegate(); + objc_setAssociatedObject(m_app_delegate, "webview", (id)this, OBJC_ASSOCIATION_ASSIGN); - objc::msg_send(app, "setDelegate:"_sel, delegate); + objc::msg_send(app, "setDelegate:"_sel, m_app_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); + // Skip the main loop if this isn't the first instance of this class + // because the launch event is only sent once. Instead, proceed to + // create a window. + if (get_and_set_is_first_instance()) { + objc::msg_send(app, "run"_sel); + } else { + set_up_window(); + } } } } - virtual ~cocoa_wkwebview_engine() = default; - void *window() { return (void *)m_window; } - void terminate() { stop_run_loop(); } - void run() { + + cocoa_wkwebview_engine(const cocoa_wkwebview_engine &) = delete; + cocoa_wkwebview_engine &operator=(const cocoa_wkwebview_engine &) = delete; + cocoa_wkwebview_engine(cocoa_wkwebview_engine &&) = delete; + cocoa_wkwebview_engine &operator=(cocoa_wkwebview_engine &&) = delete; + + virtual ~cocoa_wkwebview_engine() { + if (m_window) { + if (m_webview) { + if (m_webview == objc::msg_send(m_window, "contentView"_sel)) { + objc::msg_send(m_window, "setContentView:"_sel, nullptr); + } + m_webview = nullptr; + } + objc::msg_send(m_window, "setDelegate:"_sel, nullptr); + objc::msg_send(m_window, "close"_sel); + m_window = nullptr; + } + if (m_window_delegate) { + objc::msg_send(m_window_delegate, "release"_sel, nullptr); + m_window_delegate = nullptr; + } + if (m_app_delegate) { + auto app = get_shared_application(); + objc::msg_send(app, "setDelegate:"_sel, nullptr); + // Make sure to release the delegate we created. + objc::msg_send(m_app_delegate, "release"_sel, nullptr); + m_app_delegate = nullptr; + } + } + + void *window_impl() override { return (void *)m_window; } + void *widget_impl() override { return (void *)m_webview; } + void *browser_controller_impl() override { return (void *)m_webview; }; + void terminate_impl() override { stop_run_loop(); } + void run_impl() override { auto app = get_shared_application(); objc::msg_send(app, "run"_sel); } - void dispatch(std::function f) { + void dispatch_impl(std::function f) override { dispatch_async_f(dispatch_get_main_queue(), new dispatch_fn_t(f), (dispatch_function_t)([](void *arg) { auto f = static_cast(arg); @@ -870,13 +1470,13 @@ class cocoa_wkwebview_engine { delete f; })); } - void set_title(const std::string &title) { + void set_title_impl(const std::string &title) override { objc::msg_send(m_window, "setTitle:"_sel, objc::msg_send("NSString"_cls, "stringWithUTF8String:"_sel, title.c_str())); } - void set_size(int width, int height, int hints) { + void set_size_impl(int width, int height, int hints) override { auto style = static_cast( NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable); @@ -898,7 +1498,7 @@ class cocoa_wkwebview_engine { } objc::msg_send(m_window, "center"_sel); } - void navigate(const std::string &url) { + void navigate_impl(const std::string &url) override { objc::autoreleasepool pool; auto nsurl = objc::msg_send( @@ -910,7 +1510,7 @@ class cocoa_wkwebview_engine { m_webview, "loadRequest:"_sel, objc::msg_send("NSURLRequest"_cls, "requestWithURL:"_sel, nsurl)); } - void set_html(const std::string &html) { + void set_html_impl(const std::string &html) override { objc::autoreleasepool pool; objc::msg_send(m_webview, "loadHTMLString:baseURL:"_sel, objc::msg_send("NSString"_cls, @@ -918,7 +1518,7 @@ class cocoa_wkwebview_engine { html.c_str()), nullptr); } - void init(const std::string &js) { + void init_impl(const std::string &js) override { // Equivalent Obj-C: // [m_manager addUserScript:[[WKUserScript alloc] initWithSource:[NSString stringWithUTF8String:js.c_str()] injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES]] objc::msg_send( @@ -930,7 +1530,7 @@ class cocoa_wkwebview_engine { js.c_str()), WKUserScriptInjectionTimeAtDocumentStart, YES)); } - void eval(const std::string &js) { + void eval_impl(const std::string &js) override { objc::msg_send(m_webview, "evaluateJavaScript:completionHandler:"_sel, objc::msg_send("NSString"_cls, "stringWithUTF8String:"_sel, @@ -939,7 +1539,6 @@ class cocoa_wkwebview_engine { } private: - virtual void on_message(const std::string &msg) = 0; id create_app_delegate() { constexpr auto class_name = "WebviewAppDelegate"; // Avoid crash due to registering same class twice @@ -952,27 +1551,15 @@ class cocoa_wkwebview_engine { 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 { + (IMP)(+[](id, SEL, id) -> BOOL { return NO; }), "c@:@"); + 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); - return w->on_application_should_terminate(self, sender); + w->on_application_did_finish_launching(self, app); }), - "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@:@"); - } + "v@:@"); objc_registerClassPair(cls); } return objc::msg_send((id)cls, "new"_sel); @@ -1048,6 +1635,25 @@ class cocoa_wkwebview_engine { } return objc::msg_send((id)cls, "new"_sel); } + static id create_window_delegate() { + constexpr auto class_name = "WebviewNSWindowDelegate"; + // 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("NSWindowDelegate")); + class_addMethod(cls, "windowWillClose:"_sel, + (IMP)(+[](id self, SEL, id notification) { + auto window = + objc::msg_send(notification, "object"_sel); + auto w = get_associated_webview(self); + w->on_window_will_close(self, window); + }), + "v@:@"); + objc_registerClassPair(cls); + } + return objc::msg_send((id)cls, "new"_sel); + } static id get_shared_application() { return objc::msg_send("NSApplication"_cls, "sharedApplication"_sel); } @@ -1096,9 +1702,13 @@ class cocoa_wkwebview_engine { objc::msg_send(app, "activateIgnoringOtherApps:"_sel, YES); } - create_window(); + set_up_window(); + } + void on_window_will_close(id /*delegate*/, id window) { + m_window = nullptr; + dispatch([this] { on_window_destroyed(); }); } - void create_window() { + void set_up_window() { // Main window if (m_owns_window) { m_window = objc::msg_send("NSWindow"_cls, "alloc"_sel); @@ -1106,10 +1716,29 @@ class cocoa_wkwebview_engine { m_window = objc::msg_send( m_window, "initWithContentRect:styleMask:backing:defer:"_sel, CGRectMake(0, 0, 0, 0), style, NSBackingStoreBuffered, NO); + + auto m_window_delegate = create_window_delegate(); + objc_setAssociatedObject(m_window_delegate, "webview", (id)this, + OBJC_ASSOCIATION_ASSIGN); + objc::msg_send(m_window, "setDelegate:"_sel, m_window_delegate); + + on_window_created(); + } + + set_up_web_view(); + + objc::msg_send(m_window, "setContentView:"_sel, m_webview); + + if (m_owns_window) { + objc::msg_send(m_window, "makeKeyAndOrderFront:"_sel, nullptr); } + } + void set_up_web_view() { + objc::autoreleasepool arp; - // Webview auto config = objc::msg_send("WKWebViewConfiguration"_cls, "new"_sel); + objc::msg_send(config, "autorelease"_sel); + m_manager = objc::msg_send(config, "userContentController"_sel); m_webview = objc::msg_send("WKWebView"_cls, "alloc"_sel); @@ -1181,20 +1810,6 @@ class cocoa_wkwebview_engine { }, }; )""); - objc::msg_send(m_window, "setContentView:"_sel, m_webview); - - 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(); @@ -1210,12 +1825,22 @@ class cocoa_wkwebview_engine { type, CGPointMake(0, 0), 0, 0, 0, nullptr, 0, 0, 0); objc::msg_send(app, "postEvent:atStart:"_sel, event, YES); } + static bool get_and_set_is_first_instance() noexcept { + static std::atomic_bool first{true}; + bool temp = first; + if (temp) { + first = false; + } + return temp; + } - bool m_debug; - id m_window; - id m_webview; - id m_manager; - bool m_owns_window; + bool m_debug{}; + id m_app_delegate{}; + id m_window_delegate{}; + id m_window{}; + id m_webview{}; + id m_manager{}; + bool m_owns_window{}; }; } // namespace detail @@ -1248,64 +1873,14 @@ using browser_engine = detail::cocoa_wkwebview_engine; #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 { -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; - } - } - // 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(); -} +#pragma comment(lib, "user32.lib") +#pragma comment(lib, "version.lib") +#endif + +namespace webview { +namespace detail { + +using msg_cb_t = std::function; // 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. @@ -1401,6 +1976,9 @@ class com_init_wrapper { com_init_wrapper(com_init_wrapper &&other) { *this = std::move(other); } com_init_wrapper &operator=(com_init_wrapper &&other) { + if (this == &other) { + return *this; + } m_initialized = std::exchange(other.m_initialized, false); return *this; } @@ -1411,65 +1989,6 @@ class com_init_wrapper { bool m_initialized = false; }; -// 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: - explicit native_library(const wchar_t *name) : m_handle(LoadLibraryW(name)) {} - - ~native_library() { - if (m_handle) { - FreeLibrary(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; - - // 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()) { -#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 - } - return nullptr; - } - - // Returns true if the library is currently loaded; otherwise false. - bool is_loaded() const { return !!m_handle; } - - void detach() { m_handle = nullptr; } - -private: - HMODULE m_handle = nullptr; -}; - namespace ntdll_symbols { using RtlGetVersion_t = unsigned int /*NTSTATUS*/ (WINAPI *)(RTL_OSVERSIONINFOW *); @@ -2249,16 +2768,16 @@ class webview2_com_handler unsigned int m_attempts = 0; }; -class win32_edge_engine { +class win32_edge_engine : public engine_base { public: - win32_edge_engine(bool debug, void *window) { + win32_edge_engine(bool debug, void *window) : m_owns_window{!window} { if (!is_webview2_available()) { return; } HINSTANCE hInstance = GetModuleHandle(nullptr); - if (!window) { + if (m_owns_window) { m_com_init = {COINIT_APARTMENTTHREADED}; if (!m_com_init.is_initialized()) { return; @@ -2269,6 +2788,7 @@ class win32_edge_engine { hInstance, IDI_APPLICATION, IMAGE_ICON, GetSystemMetrics(SM_CXICON), GetSystemMetrics(SM_CYICON), LR_DEFAULTCOLOR); + // Create a top-level window. WNDCLASSEXW wc; ZeroMemory(&wc, sizeof(WNDCLASSEX)); wc.cbSize = sizeof(WNDCLASSEX); @@ -2303,15 +2823,12 @@ class win32_edge_engine { DestroyWindow(hwnd); break; case WM_DESTROY: - if (w->dec_window_count() <= 0) { - w->terminate(); - } + w->m_window = nullptr; + SetWindowLongPtrW(hwnd, GWLP_USERDATA, 0); + w->on_window_destroyed(); 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; @@ -2341,6 +2858,11 @@ class win32_edge_engine { } break; } + case WM_ACTIVATE: + if (LOWORD(wp) != WA_INACTIVE) { + w->focus_webview(); + } + break; default: return DefWindowProcW(hwnd, msg, wp, lp); } @@ -2353,17 +2875,59 @@ class win32_edge_engine { if (m_window == nullptr) { return; } - inc_window_count(); + on_window_created(); 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_window = IsWindow(static_cast(window)) + ? static_cast(window) + : *(static_cast(window)); m_dpi = get_window_dpi(m_window); } + // Create a window that WebView2 will be embedded into. + WNDCLASSEXW widget_wc{}; + widget_wc.cbSize = sizeof(WNDCLASSEX); + widget_wc.hInstance = hInstance; + widget_wc.lpszClassName = L"webview_widget"; + widget_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_widget = 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_SIZE: + w->resize_webview(); + break; + case WM_DESTROY: + w->m_widget = nullptr; + SetWindowLongPtrW(hwnd, GWLP_USERDATA, 0); + break; + default: + return DefWindowProcW(hwnd, msg, wp, lp); + } + return 0; + }); + RegisterClassExW(&widget_wc); + CreateWindowExW(WS_EX_CONTROLPARENT, L"webview_widget", nullptr, WS_CHILD, + 0, 0, 0, 0, m_window, nullptr, hInstance, this); + // Create a message-only window for internal messaging. WNDCLASSEXW message_wc{}; message_wc.cbSize = sizeof(WNDCLASSEX); @@ -2407,16 +2971,16 @@ class win32_edge_engine { 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); + if (m_owns_window) { + ShowWindow(m_window, SW_SHOW); + UpdateWindow(m_window); + SetFocus(m_window); + } auto cb = std::bind(&win32_edge_engine::on_message, this, std::placeholders::_1); - embed(m_window, debug, cb); - resize_widget(); - m_controller->MoveFocus(COREWEBVIEW2_MOVE_FOCUS_REASON_PROGRAMMATIC); + embed(m_widget, debug, cb); } virtual ~win32_edge_engine() { @@ -2432,6 +2996,20 @@ class win32_edge_engine { m_controller->Release(); m_controller = nullptr; } + if (m_message_window) { + DestroyWindow(m_message_window); + m_message_window = nullptr; + } + if (m_widget) { + DestroyWindow(m_widget); + m_widget = nullptr; + } + if (m_window) { + if (m_owns_window) { + DestroyWindow(m_window); + } + m_window = nullptr; + } } win32_edge_engine(const win32_edge_engine &other) = delete; @@ -2439,24 +3017,37 @@ class win32_edge_engine { win32_edge_engine(win32_edge_engine &&other) = delete; win32_edge_engine &operator=(win32_edge_engine &&other) = delete; - void run() { + void run_impl() { + m_running_main_loop = true; MSG msg; while (GetMessageW(&msg, nullptr, 0, 0) > 0) { TranslateMessage(&msg); DispatchMessageW(&msg); } + m_running_main_loop = false; + } + void *window_impl() override { return (void *)m_window; } + void *widget_impl() override { return (void *)m_widget; } + void *browser_controller_impl() override { return (void *)m_controller; } + void terminate_impl() override { + // Prevent unintentionally posting the quit message multiple times in the + // following scenario: + // 1. Run loop starts. + // 2. User code requests the run loop to stop. + // 3. Destructor of this class wants to stop the run loop. + if (m_running_main_loop) { + PostQuitMessage(0); + } } - void *window() { return (void *)m_window; } - void terminate() { PostQuitMessage(0); } - void dispatch(dispatch_fn_t f) { + void dispatch_impl(dispatch_fn_t f) override { PostMessageW(m_message_window, WM_APP, 0, (LPARAM) new dispatch_fn_t(f)); } - void set_title(const std::string &title) { + void set_title_impl(const std::string &title) override { SetWindowTextW(m_window, widen_string(title).c_str()); } - void set_size(int width, int height, int hints) { + void set_size_impl(int width, int height, int hints) override { auto style = GetWindowLong(m_window, GWL_STYLE); if (hints == WEBVIEW_HINT_FIXED) { style &= ~(WS_THICKFRAME | WS_MAXIMIZEBOX); @@ -2484,22 +3075,22 @@ class win32_edge_engine { } } - void navigate(const std::string &url) { + void navigate_impl(const std::string &url) override { auto wurl = widen_string(url); m_webview->Navigate(wurl.c_str()); } - void init(const std::string &js) { + void init_impl(const std::string &js) override { auto wjs = widen_string(js); m_webview->AddScriptToExecuteOnDocumentCreated(wjs.c_str(), nullptr); } - void eval(const std::string &js) { + void eval_impl(const std::string &js) override { auto wjs = widen_string(js); m_webview->ExecuteScript(wjs.c_str(), nullptr); } - void set_html(const std::string &html) { + void set_html_impl(const std::string &html) override { m_webview->NavigateToString(widen_string(html).c_str()); } @@ -2541,14 +3132,21 @@ class win32_edge_engine { m_com_handler->try_create_environment(); // Pump the message loop until WebView2 has finished initialization. + m_running_main_loop = true; + bool got_quit_msg = false; MSG msg; while (flag.test_and_set() && GetMessageW(&msg, nullptr, 0, 0) >= 0) { if (msg.message == WM_QUIT) { - return false; + got_quit_msg = true; + break; } TranslateMessage(&msg); DispatchMessageW(&msg); } + m_running_main_loop = false; + if (got_quit_msg) { + return false; + } if (!m_controller || !m_webview) { return false; } @@ -2566,16 +3164,39 @@ class win32_edge_engine { return false; } init("window.external={invoke:s=>window.chrome.webview.postMessage(s)}"); + resize_webview(); + m_controller->put_IsVisible(TRUE); + ShowWindow(m_widget, SW_SHOW); + UpdateWindow(m_widget); + if (m_owns_window) { + focus_webview(); + } return true; } void resize_widget() { - if (m_controller == nullptr) { - return; + if (m_widget) { + RECT r{}; + if (GetClientRect(GetParent(m_widget), &r)) { + MoveWindow(m_widget, r.left, r.top, r.right - r.left, r.bottom - r.top, + TRUE); + } + } + } + + void resize_webview() { + if (m_widget && m_controller) { + RECT bounds{}; + if (GetClientRect(m_widget, &bounds)) { + m_controller->put_Bounds(bounds); + } + } + } + + void focus_webview() { + if (m_controller) { + m_controller->MoveFocus(COREWEBVIEW2_MOVE_FOCUS_REASON_PROGRAMMATIC); } - RECT bounds; - GetClientRect(m_window, &bounds); - m_controller->put_Bounds(bounds); } bool is_webview2_available() const noexcept { @@ -2620,28 +3241,12 @@ class win32_edge_engine { } } - 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; HWND m_window = nullptr; + HWND m_widget = nullptr; HWND m_message_window = nullptr; POINT m_minsz = POINT{0, 0}; POINT m_maxsz = POINT{0, 0}; @@ -2651,6 +3256,8 @@ class win32_edge_engine { webview2_com_handler *m_com_handler = nullptr; mswebview2::loader m_webview2_loader; int m_dpi{}; + bool m_owns_window{}; + bool m_running_main_loop{}; }; } // namespace detail @@ -2662,129 +3269,7 @@ using browser_engine = detail::win32_edge_engine; #endif /* WEBVIEW_GTK, WEBVIEW_COCOA, WEBVIEW_EDGE */ namespace webview { - -class webview : public browser_engine { -public: - webview(bool debug = false, void *wnd = nullptr) - : browser_engine(debug, wnd) {} - - void navigate(const std::string &url) { - if (url.empty()) { - browser_engine::navigate("about:blank"); - return; - } - browser_engine::navigate(url); - } - - using binding_t = std::function; - class binding_ctx_t { - public: - 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; - // This user-supplied argument is passed to the callback - void *arg; - }; - - using sync_binding_t = std::function; - - // Synchronous bind - void bind(const std::string &name, sync_binding_t fn) { - 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 fn, void *arg) { - // NOLINTNEXTLINE(readability-container-contains): contains() requires C++20 - if (bindings.count(name) > 0) { - return; - } - 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++; - var promise = new Promise(function(resolve, reject) { - RPC[seq] = { - resolve: resolve, - reject: reject, - }; - }); - window.external.invoke(JSON.stringify({ - id: seq, - method: name, - params: Array.prototype.slice.call(arguments), - })); - 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); - auto found = bindings.find(name); - if (found == bindings.end()) { - return; - } - const auto &context = found->second; - context.callback(seq, args, context.arg); - } - - std::map bindings; -}; +using webview = browser_engine; } // namespace webview WEBVIEW_API webview_t webview_create(int debug, void *wnd) { @@ -2817,6 +3302,21 @@ WEBVIEW_API void *webview_get_window(webview_t w) { return static_cast(w)->window(); } +WEBVIEW_API void *webview_get_native_handle(webview_t w, + webview_native_handle_kind_t kind) { + auto *w_ = static_cast(w); + switch (kind) { + case WEBVIEW_NATIVE_HANDLE_KIND_UI_WINDOW: + return w_->window(); + case WEBVIEW_NATIVE_HANDLE_KIND_UI_WIDGET: + return w_->widget(); + case WEBVIEW_NATIVE_HANDLE_KIND_BROWSER_CONTROLLER: + return w_->browser_controller(); + default: + return nullptr; + } +} + WEBVIEW_API void webview_set_title(webview_t w, const char *title) { static_cast(w)->set_title(title); } diff --git a/source/webview/WebView.hx b/source/webview/WebView.hx index 650b490..a86ca8c 100644 --- a/source/webview/WebView.hx +++ b/source/webview/WebView.hx @@ -15,12 +15,24 @@ class WebView * Depending on the platform, a GtkWindow, NSWindow or HWND pointer can be * passed on the window argument. * + * If the window handle is non-null, the webview + * widget is embedded into the given window, and the + * caller is expected to assume responsibility for the window as well as + * application lifecycle + * * Creation can fail for various reasons * such as when required runtime dependencies are missing or when window creation * fails. * + * Remarks: + * - Win32: The function also accepts a pointer to HWND (Win32) in the window + * parameter for backward compatibility. + * - Win32/WebView2: CoInitializeEx should be called with + * COINIT_APARTMENTTHREADED before attempting to call this function with an + * existing window. Omitting this step may cause WebView2 initialization to fail. + * * @param debug If true, developer tools will be enabled (if the platform supports them). - * @param window [Pointer to the native window handle (NOT WORKING)] If a pointer is passed, then child webview will be embedded into the given parent window, otherwise a new window is created. + * @param window [Pointer to the native window handle] If a pointer is passed, then child webview will be embedded into the given parent window, otherwise a new window is created. */ public function new(debug:Bool = false, ?window:WindowPtr) { @@ -84,13 +96,13 @@ class WebView } /** - * Returns a native window handle pointer. + * Returns the native handle of the window associated with the webview instance. * - * When using a GTK backend the pointer is a GtkWindow pointer. + * When using a GTK backend the pointer is a GtkWindow handle. * - * When using a Cocoa backend the pointer is a NSWindow pointer. + * When using a Cocoa backend the pointer is a NSWindow handle. * - * When using a Win32 backend the pointer is a HWND pointer. + * When using a Win32 backend the pointer is a HWND handle. */ public function getWindow():WindowPtr { @@ -100,6 +112,19 @@ class WebView return WVExterns.webview_get_window(handle); } + /** + * Returns the native handle of choice + * + * @since 0.11 + */ + public function getNativeHandle(kind:WebViewNativeHandleKind):WindowPtr + { + if (handle == null) + return null; + + return WVExterns.webview_get_native_handle(handle, cast kind); + } + /** * Updates the title of the native window. * @@ -260,6 +285,9 @@ class WebView // Moved types here to avoid doing import webview.internal.WVTypes +// I learnt that a void pointer is a data type which acts like a dynamic and you need to cast it in need to retrieve the data +typedef WindowPtr = Pointer; + // Holds the elements of a MAJOR.MINOR.PATCH version number. typedef WebViewVersion = { @@ -285,12 +313,6 @@ typedef WebViewInfo = var build_metadata:String; } -// I learnt that a void pointer is a data type which acts like a dynamic and you need to cast it in need to retrieve the data -typedef WindowPtr = Pointer; - -// Used in webview_dispatch -typedef DispatchFunc = (w:WindowPtr, arg:Dynamic)->Void; - // Window size hints enum abstract WebViewSizeHint(Int) to Int from Int { @@ -300,5 +322,30 @@ enum abstract WebViewSizeHint(Int) to Int from Int var FIXED = 3; } +// Native handle kind. The actual type depends on the backend. +// An integer enum for the cpp fix, check WebViewHelper.cpp. +// @since 0.11 +enum abstract WebViewNativeHandleKind(Int) to Int from Int +{ + /** + * Top-level window. GtkWindow pointer (GTK), NSWindow pointer (Cocoa) or HWND (Win32). + */ + var WEBVIEW_NATIVE_HANDLE_KIND_UI_WINDOW = 0; + + /** + * Browser widget. GtkWidget pointer (GTK), NSView pointer (Cocoa) or HWND (Win32). + */ + var WEBVIEW_NATIVE_HANDLE_KIND_UI_WIDGET = 1; + + /** + * Browser controller. WebKitWebView pointer (WebKitGTK), WKWebView pointer (Cocoa/WebKit) or + * ICoreWebView2Controller pointer (Win32/WebView2). + */ + var WEBVIEW_NATIVE_HANDLE_KIND_BROWSER_CONTROLLER = 2; +} + +// Used in webview_dispatch +typedef DispatchFunc = (w:WindowPtr, arg:Dynamic)->Void; + // Used in webview_bind -typedef BindFunc = (seq:String, req:String, arg:Dynamic)->Void; \ No newline at end of file +typedef BindFunc = (seq:String, req:String, arg:Dynamic)->Void; diff --git a/source/webview/internal/WVExterns.hx b/source/webview/internal/WVExterns.hx index 2226897..cbf0073 100644 --- a/source/webview/internal/WVExterns.hx +++ b/source/webview/internal/WVExterns.hx @@ -12,92 +12,53 @@ import webview.WebView; @:include('internal/imports.h') extern class WVExterns { - // Creates a new webview instance. If debug is non-zero - developer tools will - // 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 - // passed here. Returns null on failure. Creation can fail for various reasons - // such as when required runtime dependencies are missing or when window creation - // fails. @:native('webview_create') private static function webview_create(debug:Int, ?window:WindowPtr):WindowPtr; - // Destroys a webview and closes the native window. @:native('webview_destroy') private static function webview_destroy(w:WindowPtr):Void; - // Runs the main loop until it's terminated. After this function exits - you - // must destroy the webview. @:native('webview_run') private static function webview_run(w:WindowPtr):Void; - // Stops the main loop. It is safe to call this function from another other - // background thread. @:native('webview_terminate') private static function webview_terminate(w:WindowPtr):Void; - // Posts a function to be executed on the main thread. You normally do not need - // to call this function, unless you want to tweak the native window. @:native('hx_webview_dispatch') private static function webview_dispatch(w:WindowPtr, fn:DispatchFunc, arg:Dynamic):Void; - // 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. @:native('webview_get_window') private static function webview_get_window(w:WindowPtr):WindowPtr; - // Updates the title of the native window. Must be called from the UI thread. + // Returns a native handle of choice. + // @since 0.11 + @:native('hx_get_native_handle') + private static function webview_get_native_handle(w:WindowPtr, kind:WebViewNativeHandleKind):WindowPtr; + @:native('webview_set_title') private static function webview_set_title(w:WindowPtr, title:ConstCharStar):Void; - // Updates the size of the native window. See WEBVIEW_HINT constants. @:native('webview_set_size') private static function webview_set_size(w:WindowPtr, width:Int, height:Int, hints:WebViewSizeHint):Void; - // Navigates webview to the given URL. URL may be a properly encoded data URI. - // Examples: - // webview_navigate(w, "https://github.com/webview/webview"); - // webview_navigate(w, "data:text/html,%3Ch1%3EHello%3C%2Fh1%3E"); - // webview_navigate(w, "data:text/html;base64,PGgxPkhlbGxvPC9oMT4="); @:native('webview_navigate') private static function webview_navigate(w:WindowPtr, url:ConstCharStar):Void; - // Set webview HTML directly. - // Example: webview_set_html(w, "

Hello

"); @:native('webview_set_html') private static function webview_set_html(w:WindowPtr, html:ConstCharStar):Void; - // Injects JavaScript code at the initialization of the new page. Every time - // the webview will open a new page - this initialization code will be - // executed. It is guaranteed that code is executed before window.onload. @:native('webview_init') private static function webview_init(w:WindowPtr, js:ConstCharStar):Void; - // Evaluates arbitrary JavaScript code. Evaluation happens asynchronously, also - // the result of the expression is ignored. Use RPC bindings if you want to - // receive notifications about the results of the evaluation. @:native('webview_eval') private static function webview_eval(w:WindowPtr, js:ConstCharStar):Void; - // Binds a native C callback so that it will appear under the given name as a - // 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. @:native('hx_webview_bind') private static function webview_bind(w:WindowPtr, name:ConstCharStar, fn:BindFunc, arg:Dynamic):Void; - // Removes a native C callback that was previously set by webview_bind. @:native('webview_unbind') private static function webview_unbind(w:WindowPtr, name:ConstCharStar):Void; - // 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". @:native('webview_return') private static function webview_return(w:WindowPtr, seq:ConstCharStar, status:Int, result:ConstCharStar):Void;