3598 lines
116 KiB
C++
3598 lines
116 KiB
C++
/*
|
||
* MIT License
|
||
*
|
||
* Copyright (c) 2017 Serge Zaitsev
|
||
* Copyright (c) 2022 Steffen André Langnes
|
||
*
|
||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||
* of this software and associated documentation files (the "Software"), to deal
|
||
* in the Software without restriction, including without limitation the rights
|
||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||
* copies of the Software, and to permit persons to whom the Software is
|
||
* furnished to do so, subject to the following conditions:
|
||
*
|
||
* The above copyright notice and this permission notice shall be included in
|
||
* all copies or substantial portions of the Software.
|
||
*
|
||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||
* SOFTWARE.
|
||
*/
|
||
|
||
/// @file webview.h
|
||
|
||
#ifndef WEBVIEW_H
|
||
#define WEBVIEW_H
|
||
|
||
/**
|
||
* Used to specify function linkage such as extern, inline, etc.
|
||
*
|
||
* When @c WEBVIEW_API is not already defined, the defaults are as follows:
|
||
*
|
||
* - @c inline when compiling C++ code.
|
||
* - @c extern when compiling C code.
|
||
*
|
||
* The following macros can be used to automatically set an appropriate
|
||
* value for @c WEBVIEW_API:
|
||
*
|
||
* - Define @c WEBVIEW_BUILD_SHARED when building a shared library.
|
||
* - Define @c WEBVIEW_SHARED when using a shared library.
|
||
* - Define @c WEBVIEW_STATIC when building or using a static library.
|
||
*/
|
||
#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
|
||
|
||
/// @name Version
|
||
/// @{
|
||
|
||
#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
|
||
|
||
/// @}
|
||
|
||
/// @name Used internally
|
||
/// @{
|
||
|
||
/// 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)
|
||
|
||
/// @}
|
||
|
||
/// @name Version
|
||
/// @{
|
||
|
||
/// 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;
|
||
|
||
/// Pointer to a webview instance.
|
||
typedef void *webview_t;
|
||
|
||
/// Native handle kind. The actual type depends on the backend.
|
||
typedef enum {
|
||
/// Top-level window. @c GtkWindow pointer (GTK), @c NSWindow pointer (Cocoa)
|
||
/// or @c HWND (Win32).
|
||
WEBVIEW_NATIVE_HANDLE_KIND_UI_WINDOW,
|
||
/// Browser widget. @c GtkWidget pointer (GTK), @c NSView pointer (Cocoa) or
|
||
/// @c HWND (Win32).
|
||
WEBVIEW_NATIVE_HANDLE_KIND_UI_WIDGET,
|
||
/// Browser controller. @c WebKitWebView pointer (WebKitGTK), @c WKWebView
|
||
/// pointer (Cocoa/WebKit) or @c ICoreWebView2Controller pointer
|
||
/// (Win32/WebView2).
|
||
WEBVIEW_NATIVE_HANDLE_KIND_BROWSER_CONTROLLER
|
||
} webview_native_handle_kind_t;
|
||
|
||
/// Window size hints
|
||
typedef enum {
|
||
/// Width and height are default size.
|
||
WEBVIEW_HINT_NONE,
|
||
/// Width and height are minimum bounds.
|
||
WEBVIEW_HINT_MIN,
|
||
/// Width and height are maximum bounds.
|
||
WEBVIEW_HINT_MAX,
|
||
/// Window size can not be changed by a user.
|
||
WEBVIEW_HINT_FIXED
|
||
} webview_hint_t;
|
||
|
||
#ifdef __cplusplus
|
||
extern "C" {
|
||
#endif
|
||
|
||
/**
|
||
* Creates a new webview instance.
|
||
*
|
||
* @param debug Enable developer tools if supported by the backend.
|
||
* @param window Optional native window handle, i.e. @c GtkWindow pointer
|
||
* @c NSWindow pointer (Cocoa) or @c HWND (Win32). If 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.
|
||
* @remark Win32: The function also accepts a pointer to @c HWND (Win32) in the
|
||
* window parameter for backward compatibility.
|
||
* @remark Win32/WebView2: @c CoInitializeEx should be called with
|
||
* @c COINIT_APARTMENTTHREADED before attempting to call this function
|
||
* with an existing window. Omitting this step may cause WebView2
|
||
* initialization to fail.
|
||
* @return @c NULL on failure. Creation can fail for various reasons such
|
||
* as when required runtime dependencies are missing or when window
|
||
* creation fails.
|
||
*/
|
||
WEBVIEW_API webview_t webview_create(int debug, void *window);
|
||
|
||
/**
|
||
* Destroys a webview instance and closes the native window.
|
||
*
|
||
* @param w The webview instance.
|
||
*/
|
||
WEBVIEW_API void webview_destroy(webview_t w);
|
||
|
||
/**
|
||
* Runs the main loop until it's terminated.
|
||
*
|
||
* @param w The webview instance.
|
||
*/
|
||
WEBVIEW_API void webview_run(webview_t w);
|
||
|
||
/**
|
||
* Stops the main loop. It is safe to call this function from another other
|
||
* background thread.
|
||
*
|
||
* @param w The webview instance.
|
||
*/
|
||
WEBVIEW_API void webview_terminate(webview_t w);
|
||
|
||
/**
|
||
* Schedules a function to be invoked on the thread with the run/event loop.
|
||
* Use this function e.g. to interact with the library or native handles.
|
||
*
|
||
* @param w The webview instance.
|
||
* @param fn The function to be invoked.
|
||
* @param arg An optional argument passed along to the callback function.
|
||
*/
|
||
WEBVIEW_API void
|
||
webview_dispatch(webview_t w, void (*fn)(webview_t w, void *arg), void *arg);
|
||
|
||
/**
|
||
* Returns the native handle of the window associated with the webview instance.
|
||
* The handle can be a @c GtkWindow pointer (GTK), @c NSWindow pointer (Cocoa)
|
||
* or @c HWND (Win32).
|
||
*
|
||
* @param w The webview instance.
|
||
* @return The handle of the native window.
|
||
*/
|
||
WEBVIEW_API void *webview_get_window(webview_t w);
|
||
|
||
/**
|
||
* Get a native handle of choice.
|
||
*
|
||
* @param w The webview instance.
|
||
* @param kind The kind of handle to retrieve.
|
||
* @return The native handle or @c NULL.
|
||
* @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.
|
||
*
|
||
* @param w The webview instance.
|
||
* @param title The new title.
|
||
*/
|
||
WEBVIEW_API void webview_set_title(webview_t w, const char *title);
|
||
|
||
/**
|
||
* Updates the size of the native window.
|
||
*
|
||
* @param w The webview instance.
|
||
* @param width New width.
|
||
* @param height New height.
|
||
* @param hints Size hints.
|
||
*/
|
||
WEBVIEW_API void webview_set_size(webview_t w, int width, int height,
|
||
webview_hint_t hints);
|
||
|
||
/**
|
||
* Navigates webview to the given URL. URL may be a properly encoded data URI.
|
||
*
|
||
* Example:
|
||
* @code{.c}
|
||
* 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=");
|
||
* @endcode
|
||
*
|
||
* @param w The webview instance.
|
||
* @param url URL.
|
||
*/
|
||
WEBVIEW_API void webview_navigate(webview_t w, const char *url);
|
||
|
||
/**
|
||
* Load HTML content into the webview.
|
||
*
|
||
* Example:
|
||
* @code{.c}
|
||
* webview_set_html(w, "<h1>Hello</h1>");
|
||
* @endcode
|
||
*
|
||
* @param w The webview instance.
|
||
* @param html HTML content.
|
||
*/
|
||
WEBVIEW_API void webview_set_html(webview_t w, const char *html);
|
||
|
||
/**
|
||
* Injects JavaScript code to be executed immediately upon loading a page.
|
||
* The code will be executed before @c window.onload.
|
||
*
|
||
* @param w The webview instance.
|
||
* @param js JS content.
|
||
*/
|
||
WEBVIEW_API void webview_init(webview_t w, const char *js);
|
||
|
||
/**
|
||
* Evaluates arbitrary JavaScript code.
|
||
*
|
||
* Use bindings if you need to communicate the result of the evaluation.
|
||
*
|
||
* @param w The webview instance.
|
||
* @param js JS content.
|
||
*/
|
||
WEBVIEW_API void webview_eval(webview_t w, const char *js);
|
||
|
||
/**
|
||
* Binds a function pointer to a new global JavaScript function.
|
||
*
|
||
* Internally, JS glue code is injected to create the JS function by the
|
||
* given name. The callback function is passed a sequential request
|
||
* identifier, a request string and a user-provided argument. The request
|
||
* string is a JSON array of the arguments passed to the JS function.
|
||
*
|
||
* @param w The webview instance.
|
||
* @param name Name of the JS function.
|
||
* @param fn Callback function.
|
||
* @param arg User argument.
|
||
*/
|
||
WEBVIEW_API void webview_bind(webview_t w, const char *name,
|
||
void (*fn)(const char *seq, const char *req,
|
||
void *arg),
|
||
void *arg);
|
||
|
||
/**
|
||
* Removes a binding created with webview_bind().
|
||
*
|
||
* @param w The webview instance.
|
||
* @param name Name of the binding.
|
||
*/
|
||
WEBVIEW_API void webview_unbind(webview_t w, const char *name);
|
||
|
||
/**
|
||
* Responds to a binding call from the JS side.
|
||
*
|
||
* @param w The webview instance.
|
||
* @param seq The sequence number of the binding call. Pass along the value
|
||
* received in the binding handler (see webview_bind()).
|
||
* @param status A status of zero tells the JS side that the binding call was
|
||
* succesful; any other value indicates an error.
|
||
* @param result The result of the binding call to be returned to the JS side.
|
||
* This must either be a valid JSON value or an empty string for
|
||
* the primitive JS value @c 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
|
||
}
|
||
|
||
#ifndef WEBVIEW_HEADER
|
||
|
||
#if !defined(WEBVIEW_GTK) && !defined(WEBVIEW_COCOA) && !defined(WEBVIEW_EDGE)
|
||
#if defined(__APPLE__)
|
||
#define WEBVIEW_COCOA
|
||
#elif defined(__unix__)
|
||
#define WEBVIEW_GTK
|
||
#elif defined(_WIN32)
|
||
#define WEBVIEW_EDGE
|
||
#else
|
||
#error "please, specify webview backend"
|
||
#endif
|
||
#endif
|
||
|
||
#ifndef WEBVIEW_DEPRECATED
|
||
#if __cplusplus >= 201402L
|
||
#define WEBVIEW_DEPRECATED(reason) [[deprecated(reason)]]
|
||
#elif defined(_MSC_VER)
|
||
#define WEBVIEW_DEPRECATED(reason) __declspec(deprecated(reason))
|
||
#else
|
||
#define WEBVIEW_DEPRECATED(reason) __attribute__((deprecated(reason)))
|
||
#endif
|
||
#endif
|
||
|
||
#ifndef WEBVIEW_DEPRECATED_PRIVATE
|
||
#define WEBVIEW_DEPRECATED_PRIVATE \
|
||
WEBVIEW_DEPRECATED("Private API should not be used")
|
||
#endif
|
||
|
||
#include <algorithm>
|
||
#include <array>
|
||
#include <atomic>
|
||
#include <cassert>
|
||
#include <cstdint>
|
||
#include <functional>
|
||
#include <future>
|
||
#include <map>
|
||
#include <string>
|
||
#include <utility>
|
||
#include <vector>
|
||
|
||
#include <cstring>
|
||
|
||
#if defined(_WIN32)
|
||
#define WIN32_LEAN_AND_MEAN
|
||
#include <windows.h>
|
||
#else
|
||
#include <dlfcn.h>
|
||
#endif
|
||
|
||
namespace webview {
|
||
|
||
using dispatch_fn_t = std::function<void()>;
|
||
|
||
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<int>(input.size());
|
||
auto required_length =
|
||
MultiByteToWideChar(cp, flags, input_c, input_length, nullptr, 0);
|
||
if (required_length > 0) {
|
||
std::wstring output(static_cast<std::size_t>(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<int>(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<std::size_t>(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 {
|
||
JSON_STATE_VALUE,
|
||
JSON_STATE_LITERAL,
|
||
JSON_STATE_STRING,
|
||
JSON_STATE_ESCAPE,
|
||
JSON_STATE_UTF8
|
||
} state = JSON_STATE_VALUE;
|
||
const char *k = nullptr;
|
||
int index = 1;
|
||
int depth = 0;
|
||
int utf8_bytes = 0;
|
||
|
||
*value = nullptr;
|
||
*valuesz = 0;
|
||
|
||
if (key == nullptr) {
|
||
index = static_cast<decltype(index)>(keysz);
|
||
if (index < 0) {
|
||
return -1;
|
||
}
|
||
keysz = 0;
|
||
}
|
||
|
||
for (; sz > 0; s++, sz--) {
|
||
enum {
|
||
JSON_ACTION_NONE,
|
||
JSON_ACTION_START,
|
||
JSON_ACTION_END,
|
||
JSON_ACTION_START_STRUCT,
|
||
JSON_ACTION_END_STRUCT
|
||
} action = JSON_ACTION_NONE;
|
||
auto c = static_cast<unsigned char>(*s);
|
||
switch (state) {
|
||
case JSON_STATE_VALUE:
|
||
if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == ',' ||
|
||
c == ':') {
|
||
continue;
|
||
} else if (c == '"') {
|
||
action = JSON_ACTION_START;
|
||
state = JSON_STATE_STRING;
|
||
} else if (c == '{' || c == '[') {
|
||
action = JSON_ACTION_START_STRUCT;
|
||
} else if (c == '}' || c == ']') {
|
||
action = JSON_ACTION_END_STRUCT;
|
||
} else if (c == 't' || c == 'f' || c == 'n' || c == '-' ||
|
||
(c >= '0' && c <= '9')) {
|
||
action = JSON_ACTION_START;
|
||
state = JSON_STATE_LITERAL;
|
||
} else {
|
||
return -1;
|
||
}
|
||
break;
|
||
case JSON_STATE_LITERAL:
|
||
if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == ',' ||
|
||
c == ']' || c == '}' || c == ':') {
|
||
state = JSON_STATE_VALUE;
|
||
s--;
|
||
sz++;
|
||
action = JSON_ACTION_END;
|
||
} else if (c < 32 || c > 126) {
|
||
return -1;
|
||
} // fallthrough
|
||
case JSON_STATE_STRING:
|
||
if (c < 32 || (c > 126 && c < 192)) {
|
||
return -1;
|
||
} else if (c == '"') {
|
||
action = JSON_ACTION_END;
|
||
state = JSON_STATE_VALUE;
|
||
} else if (c == '\\') {
|
||
state = JSON_STATE_ESCAPE;
|
||
} else if (c >= 192 && c < 224) {
|
||
utf8_bytes = 1;
|
||
state = JSON_STATE_UTF8;
|
||
} else if (c >= 224 && c < 240) {
|
||
utf8_bytes = 2;
|
||
state = JSON_STATE_UTF8;
|
||
} else if (c >= 240 && c < 247) {
|
||
utf8_bytes = 3;
|
||
state = JSON_STATE_UTF8;
|
||
} else if (c >= 128 && c < 192) {
|
||
return -1;
|
||
}
|
||
break;
|
||
case JSON_STATE_ESCAPE:
|
||
if (c == '"' || c == '\\' || c == '/' || c == 'b' || c == 'f' ||
|
||
c == 'n' || c == 'r' || c == 't' || c == 'u') {
|
||
state = JSON_STATE_STRING;
|
||
} else {
|
||
return -1;
|
||
}
|
||
break;
|
||
case JSON_STATE_UTF8:
|
||
if (c < 128 || c > 191) {
|
||
return -1;
|
||
}
|
||
utf8_bytes--;
|
||
if (utf8_bytes == 0) {
|
||
state = JSON_STATE_STRING;
|
||
}
|
||
break;
|
||
default:
|
||
return -1;
|
||
}
|
||
|
||
if (action == JSON_ACTION_END_STRUCT) {
|
||
depth--;
|
||
}
|
||
|
||
if (depth == 1) {
|
||
if (action == JSON_ACTION_START || action == JSON_ACTION_START_STRUCT) {
|
||
if (index == 0) {
|
||
*value = s;
|
||
} else if (keysz > 0 && index == 1) {
|
||
k = s;
|
||
} else {
|
||
index--;
|
||
}
|
||
} else if (action == JSON_ACTION_END ||
|
||
action == JSON_ACTION_END_STRUCT) {
|
||
if (*value != nullptr && index == 0) {
|
||
*valuesz = (size_t)(s + 1 - *value);
|
||
return 0;
|
||
} 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 = nullptr;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (action == JSON_ACTION_START_STRUCT) {
|
||
depth++;
|
||
}
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
constexpr bool is_json_special_char(char c) {
|
||
return c == '"' || c == '\\' || c == '\b' || c == '\f' || c == '\n' ||
|
||
c == '\r' || c == '\t';
|
||
}
|
||
|
||
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) {
|
||
// Calculate the size of the resulting string.
|
||
// Add space for the double quotes.
|
||
size_t required_length = add_quotes ? 2 : 0;
|
||
for (auto c : s) {
|
||
if (is_json_special_char(c)) {
|
||
// '\' and a single following character
|
||
required_length += 2;
|
||
continue;
|
||
}
|
||
if (is_ascii_control_char(c)) {
|
||
// '\', '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) {
|
||
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 += '\\';
|
||
// NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-constant-array-index)
|
||
result += special_escape_table[static_cast<unsigned char>(c)];
|
||
continue;
|
||
}
|
||
if (is_ascii_control_char(c)) {
|
||
// Escape as \u00xx
|
||
static constexpr char hex_alphabet[]{"0123456789abcdef"};
|
||
auto uc = static_cast<unsigned char>(c);
|
||
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 += '"';
|
||
}
|
||
// Should have calculated the exact amount of memory needed
|
||
assert(required_length == result.size());
|
||
return result;
|
||
}
|
||
|
||
inline int json_unescape(const char *s, size_t n, char *out) {
|
||
int r = 0;
|
||
if (*s++ != '"') {
|
||
return -1;
|
||
}
|
||
while (n > 2) {
|
||
char c = *s;
|
||
if (c == '\\') {
|
||
s++;
|
||
n--;
|
||
switch (*s) {
|
||
case 'b':
|
||
c = '\b';
|
||
break;
|
||
case 'f':
|
||
c = '\f';
|
||
break;
|
||
case 'n':
|
||
c = '\n';
|
||
break;
|
||
case 'r':
|
||
c = '\r';
|
||
break;
|
||
case 't':
|
||
c = '\t';
|
||
break;
|
||
case '\\':
|
||
c = '\\';
|
||
break;
|
||
case '/':
|
||
c = '/';
|
||
break;
|
||
case '\"':
|
||
c = '\"';
|
||
break;
|
||
default: // TODO: support unicode decoding
|
||
return -1;
|
||
}
|
||
}
|
||
if (out != nullptr) {
|
||
*out++ = c;
|
||
}
|
||
s++;
|
||
n--;
|
||
r++;
|
||
}
|
||
if (*s != '"') {
|
||
return -1;
|
||
}
|
||
if (out != nullptr) {
|
||
*out = '\0';
|
||
}
|
||
return r;
|
||
}
|
||
|
||
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.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,
|
||
&value_sz);
|
||
}
|
||
if (value != nullptr) {
|
||
if (value[0] != '"') {
|
||
return {value, value_sz};
|
||
}
|
||
int n = json_unescape(value, value_sz, nullptr);
|
||
if (n > 0) {
|
||
char *decoded = new char[n + 1];
|
||
json_unescape(value, value_sz, decoded);
|
||
std::string result(decoded, n);
|
||
delete[] decoded;
|
||
return result;
|
||
}
|
||
}
|
||
return "";
|
||
}
|
||
|
||
// Holds a symbol name and associated type for code clarity.
|
||
template <typename T> 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) noexcept { *this = std::move(other); }
|
||
|
||
native_library &operator=(native_library &&other) noexcept {
|
||
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>
|
||
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<typename Symbol::type>(
|
||
GetProcAddress(m_handle, symbol.get_name()));
|
||
#ifdef __GNUC__
|
||
#pragma GCC diagnostic pop
|
||
#endif
|
||
#else
|
||
return reinterpret_cast<typename Symbol::type>(
|
||
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<void(std::string, std::string, void *)>;
|
||
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<std::string(std::string)>;
|
||
|
||
// 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 += ";\
|
||
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);\
|
||
}\
|
||
})()";
|
||
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<void()> f) { dispatch_impl(f); }
|
||
void set_title(const std::string &title) { set_title_impl(title); }
|
||
|
||
void set_size(int width, int height, webview_hint_t 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<void()> f) = 0;
|
||
virtual void set_title_impl(const std::string &title) = 0;
|
||
virtual void set_size_impl(int width, int height, webview_hint_t 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(bool skip_termination = false) {
|
||
if (dec_window_count() <= 0) {
|
||
if (!skip_termination) {
|
||
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<std::string, binding_ctx_t> bindings;
|
||
};
|
||
|
||
} // namespace detail
|
||
|
||
WEBVIEW_DEPRECATED_PRIVATE
|
||
inline int json_parse_c(const char *s, size_t sz, const char *key, size_t keysz,
|
||
const char **value, size_t *valuesz) {
|
||
return detail::json_parse_c(s, sz, key, keysz, value, valuesz);
|
||
}
|
||
|
||
WEBVIEW_DEPRECATED_PRIVATE
|
||
inline std::string json_escape(const std::string &s) {
|
||
return detail::json_escape(s);
|
||
}
|
||
|
||
WEBVIEW_DEPRECATED_PRIVATE
|
||
inline int json_unescape(const char *s, size_t n, char *out) {
|
||
return detail::json_unescape(s, n, out);
|
||
}
|
||
|
||
WEBVIEW_DEPRECATED_PRIVATE
|
||
inline std::string json_parse(const std::string &s, const std::string &key,
|
||
const int index) {
|
||
return detail::json_parse(s, key, index);
|
||
}
|
||
|
||
} // namespace webview
|
||
|
||
#if defined(WEBVIEW_GTK)
|
||
//
|
||
// ====================================================================
|
||
//
|
||
// This implementation uses webkit2gtk backend. It requires gtk+3.0 and
|
||
// webkit2gtk-4.1 libraries. Proper compiler flags can be retrieved via:
|
||
//
|
||
// pkg-config --cflags --libs gtk+-3.0 webkit2gtk-4.1
|
||
//
|
||
// ====================================================================
|
||
//
|
||
#include <cstdlib>
|
||
|
||
#include <JavaScriptCore/JavaScript.h>
|
||
#include <gtk/gtk.h>
|
||
#include <webkit2/webkit2.h>
|
||
|
||
#ifdef GDK_WINDOWING_X11
|
||
#include <gdk/gdkx.h>
|
||
#endif
|
||
|
||
#include <fcntl.h>
|
||
#include <sys/stat.h>
|
||
|
||
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); // NOLINT(misc-const-correctness)
|
||
#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_t>(
|
||
"webkit_web_view_evaluate_javascript");
|
||
constexpr auto webkit_web_view_run_javascript =
|
||
library_symbol<webkit_web_view_run_javascript_t>(
|
||
"webkit_web_view_run_javascript");
|
||
} // namespace webkit_symbols
|
||
|
||
class gtk_webkit_engine : public engine_base {
|
||
public:
|
||
gtk_webkit_engine(bool debug, void *window)
|
||
: m_owns_window{!window}, m_window(static_cast<GtkWidget *>(window)) {
|
||
if (m_owns_window) {
|
||
if (gtk_init_check(nullptr, nullptr) == FALSE) {
|
||
return;
|
||
}
|
||
m_window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
|
||
on_window_created();
|
||
g_signal_connect(G_OBJECT(m_window), "destroy",
|
||
G_CALLBACK(+[](GtkWidget *, gpointer arg) {
|
||
auto *w = static_cast<gtk_webkit_engine *>(arg);
|
||
// Widget destroyed along with window.
|
||
w->m_webview = nullptr;
|
||
w->m_window = nullptr;
|
||
w->on_window_destroyed();
|
||
}),
|
||
this);
|
||
}
|
||
webkit_dmabuf::apply_webkit_dmabuf_workaround();
|
||
// Initialize webview widget
|
||
m_webview = webkit_web_view_new();
|
||
WebKitUserContentManager *manager =
|
||
webkit_web_view_get_user_content_manager(WEBKIT_WEB_VIEW(m_webview));
|
||
g_signal_connect(manager, "script-message-received::external",
|
||
G_CALLBACK(+[](WebKitUserContentManager *,
|
||
WebKitJavascriptResult *r, gpointer arg) {
|
||
auto *w = static_cast<gtk_webkit_engine *>(arg);
|
||
char *s = get_string_from_js_result(r);
|
||
w->on_message(s);
|
||
g_free(s);
|
||
}),
|
||
this);
|
||
webkit_user_content_manager_register_script_message_handler(manager,
|
||
"external");
|
||
init("window.external={invoke:function(s){window.webkit.messageHandlers."
|
||
"external.postMessage(s);}}");
|
||
|
||
gtk_container_add(GTK_CONTAINER(m_window), GTK_WIDGET(m_webview));
|
||
gtk_widget_show(GTK_WIDGET(m_webview));
|
||
|
||
WebKitSettings *settings =
|
||
webkit_web_view_get_settings(WEBKIT_WEB_VIEW(m_webview));
|
||
webkit_settings_set_javascript_can_access_clipboard(settings, true);
|
||
if (debug) {
|
||
webkit_settings_set_enable_write_console_messages_to_stdout(settings,
|
||
true);
|
||
webkit_settings_set_enable_developer_extras(settings, true);
|
||
}
|
||
|
||
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) {
|
||
// Disconnect handlers to avoid callbacks invoked during destruction.
|
||
g_signal_handlers_disconnect_by_data(GTK_WINDOW(m_window), this);
|
||
gtk_window_close(GTK_WINDOW(m_window));
|
||
on_window_destroyed(true);
|
||
}
|
||
m_window = nullptr;
|
||
}
|
||
if (m_owns_window) {
|
||
// Needed for the window to close immediately.
|
||
deplete_run_loop_event_queue();
|
||
}
|
||
}
|
||
|
||
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 {
|
||
dispatch_impl([] { gtk_main_quit(); });
|
||
}
|
||
void dispatch_impl(std::function<void()> f) override {
|
||
g_idle_add_full(G_PRIORITY_HIGH_IDLE, (GSourceFunc)([](void *f) -> int {
|
||
(*static_cast<dispatch_fn_t *>(f))();
|
||
return G_SOURCE_REMOVE;
|
||
}),
|
||
new std::function<void()>(f),
|
||
[](void *f) { delete static_cast<dispatch_fn_t *>(f); });
|
||
}
|
||
|
||
void set_title_impl(const std::string &title) override {
|
||
gtk_window_set_title(GTK_WINDOW(m_window), title.c_str());
|
||
}
|
||
|
||
void set_size_impl(int width, int height, webview_hint_t 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);
|
||
} else if (hints == WEBVIEW_HINT_FIXED) {
|
||
gtk_widget_set_size_request(m_window, width, height);
|
||
} else {
|
||
GdkGeometry g;
|
||
g.min_width = g.max_width = width;
|
||
g.min_height = g.max_height = height;
|
||
GdkWindowHints h =
|
||
(hints == WEBVIEW_HINT_MIN ? GDK_HINT_MIN_SIZE : GDK_HINT_MAX_SIZE);
|
||
// This defines either MIN_SIZE, or MAX_SIZE, but not both:
|
||
gtk_window_set_geometry_hints(GTK_WINDOW(m_window), nullptr, &g, h);
|
||
}
|
||
}
|
||
|
||
void navigate_impl(const std::string &url) override {
|
||
webkit_web_view_load_uri(WEBKIT_WEB_VIEW(m_webview), url.c_str());
|
||
}
|
||
|
||
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_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(
|
||
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_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<gssize>(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:
|
||
static char *get_string_from_js_result(WebKitJavascriptResult *r) {
|
||
char *s;
|
||
#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, nullptr);
|
||
size_t n = JSStringGetMaximumUTF8CStringSize(js);
|
||
s = g_new(char, n);
|
||
JSStringGetUTF8CString(js, s, n);
|
||
JSStringRelease(js);
|
||
#endif
|
||
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<const char *, 2> lib_names{"libwebkit2gtk-4.1.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;
|
||
}
|
||
|
||
// Blocks while depleting the run loop of events.
|
||
void deplete_run_loop_event_queue() {
|
||
bool done{};
|
||
dispatch([&] { done = true; });
|
||
while (!done) {
|
||
gtk_main_iteration();
|
||
}
|
||
}
|
||
|
||
bool m_owns_window{};
|
||
GtkWidget *m_window{};
|
||
GtkWidget *m_webview{};
|
||
};
|
||
|
||
} // namespace detail
|
||
|
||
using browser_engine = detail::gtk_webkit_engine;
|
||
|
||
} // namespace webview
|
||
|
||
#elif defined(WEBVIEW_COCOA)
|
||
|
||
//
|
||
// ====================================================================
|
||
//
|
||
// This implementation uses Cocoa WKWebView backend on macOS. It is
|
||
// written using ObjC runtime and uses WKWebView class as a browser runtime.
|
||
// You should pass "-framework Webkit" flag to the compiler.
|
||
//
|
||
// ====================================================================
|
||
//
|
||
|
||
#include <CoreGraphics/CoreGraphics.h>
|
||
#include <objc/NSObjCRuntime.h>
|
||
#include <objc/objc-runtime.h>
|
||
|
||
namespace webview {
|
||
namespace detail {
|
||
namespace objc {
|
||
|
||
// A convenient template function for unconditionally casting the specified
|
||
// C-like function into a function that can be called with the given return
|
||
// type and arguments. Caller takes full responsibility for ensuring that
|
||
// the function call is valid. It is assumed that the function will not
|
||
// throw exceptions.
|
||
template <typename Result, typename Callable, typename... Args>
|
||
Result invoke(Callable callable, Args... args) noexcept {
|
||
return reinterpret_cast<Result (*)(Args...)>(callable)(args...);
|
||
}
|
||
|
||
// Calls objc_msgSend.
|
||
template <typename Result, typename... Args>
|
||
Result msg_send(Args... args) noexcept {
|
||
return invoke<Result>(objc_msgSend, args...);
|
||
}
|
||
|
||
// Wrapper around NSAutoreleasePool that drains the pool on destruction.
|
||
class autoreleasepool {
|
||
public:
|
||
autoreleasepool()
|
||
: m_pool(msg_send<id>(objc_getClass("NSAutoreleasePool"),
|
||
sel_registerName("new"))) {}
|
||
|
||
~autoreleasepool() {
|
||
if (m_pool) {
|
||
msg_send<void>(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{};
|
||
};
|
||
|
||
inline id autoreleased(id object) {
|
||
msg_send<void>(object, sel_registerName("autorelease"));
|
||
return object;
|
||
}
|
||
|
||
} // namespace objc
|
||
|
||
enum NSBackingStoreType : NSUInteger { NSBackingStoreBuffered = 2 };
|
||
|
||
enum NSWindowStyleMask : NSUInteger {
|
||
NSWindowStyleMaskTitled = 1,
|
||
NSWindowStyleMaskClosable = 2,
|
||
NSWindowStyleMaskMiniaturizable = 4,
|
||
NSWindowStyleMaskResizable = 8
|
||
};
|
||
|
||
enum NSApplicationActivationPolicy : NSInteger {
|
||
NSApplicationActivationPolicyRegular = 0
|
||
};
|
||
|
||
enum WKUserScriptInjectionTime : NSInteger {
|
||
WKUserScriptInjectionTimeAtDocumentStart = 0
|
||
};
|
||
|
||
enum NSModalResponse : NSInteger { NSModalResponseOK = 1 };
|
||
|
||
// Convenient conversion of string literals.
|
||
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<id>("NSString"_cls, "stringWithUTF8String:"_sel, s);
|
||
}
|
||
|
||
class cocoa_wkwebview_engine : public engine_base {
|
||
public:
|
||
cocoa_wkwebview_engine(bool debug, void *window)
|
||
: m_debug{debug}, m_window{static_cast<id>(window)}, m_owns_window{
|
||
!window} {
|
||
auto app = get_shared_application();
|
||
// See comments related to application lifecycle in create_app_delegate().
|
||
if (!m_owns_window) {
|
||
set_up_window();
|
||
} else {
|
||
// Only set the app delegate if it hasn't already been set.
|
||
auto delegate = objc::msg_send<id>(app, "delegate"_sel);
|
||
if (delegate) {
|
||
set_up_window();
|
||
} else {
|
||
m_app_delegate = create_app_delegate();
|
||
objc_setAssociatedObject(m_app_delegate, "webview", (id)this,
|
||
OBJC_ASSOCIATION_ASSIGN);
|
||
objc::msg_send<void>(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.
|
||
// 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<void>(app, "run"_sel);
|
||
} else {
|
||
set_up_window();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
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() {
|
||
objc::autoreleasepool arp;
|
||
if (m_window) {
|
||
if (m_webview) {
|
||
if (m_webview == objc::msg_send<id>(m_window, "contentView"_sel)) {
|
||
objc::msg_send<void>(m_window, "setContentView:"_sel, nullptr);
|
||
}
|
||
objc::msg_send<void>(m_webview, "release"_sel);
|
||
m_webview = nullptr;
|
||
}
|
||
if (m_owns_window) {
|
||
// Replace delegate to avoid callbacks and other bad things during
|
||
// destruction.
|
||
objc::msg_send<void>(m_window, "setDelegate:"_sel, nullptr);
|
||
objc::msg_send<void>(m_window, "close"_sel);
|
||
on_window_destroyed(true);
|
||
}
|
||
m_window = nullptr;
|
||
}
|
||
if (m_window_delegate) {
|
||
objc::msg_send<void>(m_window_delegate, "release"_sel);
|
||
m_window_delegate = nullptr;
|
||
}
|
||
if (m_app_delegate) {
|
||
auto app = get_shared_application();
|
||
objc::msg_send<void>(app, "setDelegate:"_sel, nullptr);
|
||
// Make sure to release the delegate we created.
|
||
objc::msg_send<void>(m_app_delegate, "release"_sel);
|
||
m_app_delegate = nullptr;
|
||
}
|
||
if (m_owns_window) {
|
||
// Needed for the window to close immediately.
|
||
deplete_run_loop_event_queue();
|
||
}
|
||
// TODO: Figure out why m_manager is still alive after the autoreleasepool
|
||
// has been drained.
|
||
}
|
||
|
||
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<void>(app, "run"_sel);
|
||
}
|
||
void dispatch_impl(std::function<void()> f) override {
|
||
dispatch_async_f(dispatch_get_main_queue(), new dispatch_fn_t(f),
|
||
(dispatch_function_t)([](void *arg) {
|
||
auto f = static_cast<dispatch_fn_t *>(arg);
|
||
(*f)();
|
||
delete f;
|
||
}));
|
||
}
|
||
void set_title_impl(const std::string &title) override {
|
||
objc::autoreleasepool arp;
|
||
|
||
objc::msg_send<void>(m_window, "setTitle:"_sel,
|
||
objc::msg_send<id>("NSString"_cls,
|
||
"stringWithUTF8String:"_sel,
|
||
title.c_str()));
|
||
}
|
||
void set_size_impl(int width, int height, webview_hint_t hints) override {
|
||
objc::autoreleasepool arp;
|
||
|
||
auto style = static_cast<NSWindowStyleMask>(
|
||
NSWindowStyleMaskTitled | NSWindowStyleMaskClosable |
|
||
NSWindowStyleMaskMiniaturizable);
|
||
if (hints != WEBVIEW_HINT_FIXED) {
|
||
style =
|
||
static_cast<NSWindowStyleMask>(style | NSWindowStyleMaskResizable);
|
||
}
|
||
objc::msg_send<void>(m_window, "setStyleMask:"_sel, style);
|
||
|
||
if (hints == WEBVIEW_HINT_MIN) {
|
||
objc::msg_send<void>(m_window, "setContentMinSize:"_sel,
|
||
CGSizeMake(width, height));
|
||
} else if (hints == WEBVIEW_HINT_MAX) {
|
||
objc::msg_send<void>(m_window, "setContentMaxSize:"_sel,
|
||
CGSizeMake(width, height));
|
||
} else {
|
||
objc::msg_send<void>(m_window, "setFrame:display:animate:"_sel,
|
||
CGRectMake(0, 0, width, height), YES, NO);
|
||
}
|
||
objc::msg_send<void>(m_window, "center"_sel);
|
||
}
|
||
void navigate_impl(const std::string &url) override {
|
||
objc::autoreleasepool arp;
|
||
|
||
auto nsurl = objc::msg_send<id>(
|
||
"NSURL"_cls, "URLWithString:"_sel,
|
||
objc::msg_send<id>("NSString"_cls, "stringWithUTF8String:"_sel,
|
||
url.c_str()));
|
||
|
||
objc::msg_send<void>(
|
||
m_webview, "loadRequest:"_sel,
|
||
objc::msg_send<id>("NSURLRequest"_cls, "requestWithURL:"_sel, nsurl));
|
||
}
|
||
void set_html_impl(const std::string &html) override {
|
||
objc::autoreleasepool arp;
|
||
objc::msg_send<void>(m_webview, "loadHTMLString:baseURL:"_sel,
|
||
objc::msg_send<id>("NSString"_cls,
|
||
"stringWithUTF8String:"_sel,
|
||
html.c_str()),
|
||
nullptr);
|
||
}
|
||
void init_impl(const std::string &js) override {
|
||
objc::autoreleasepool arp;
|
||
auto script = objc::autoreleased(objc::msg_send<id>(
|
||
objc::msg_send<id>("WKUserScript"_cls, "alloc"_sel),
|
||
"initWithSource:injectionTime:forMainFrameOnly:"_sel,
|
||
objc::msg_send<id>("NSString"_cls, "stringWithUTF8String:"_sel,
|
||
js.c_str()),
|
||
WKUserScriptInjectionTimeAtDocumentStart, YES));
|
||
objc::msg_send<void>(m_manager, "addUserScript:"_sel, script);
|
||
}
|
||
void eval_impl(const std::string &js) override {
|
||
objc::autoreleasepool arp;
|
||
objc::msg_send<void>(m_webview, "evaluateJavaScript:completionHandler:"_sel,
|
||
objc::msg_send<id>("NSString"_cls,
|
||
"stringWithUTF8String:"_sel,
|
||
js.c_str()),
|
||
nullptr);
|
||
}
|
||
|
||
private:
|
||
id create_app_delegate() {
|
||
objc::autoreleasepool arp;
|
||
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 NO; }), "c@:@");
|
||
class_addMethod(cls, "applicationDidFinishLaunching:"_sel,
|
||
(IMP)(+[](id self, SEL, id notification) {
|
||
auto app =
|
||
objc::msg_send<id>(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>((id)cls, "new"_sel);
|
||
}
|
||
id create_script_message_handler() {
|
||
objc::autoreleasepool arp;
|
||
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<const char *>(
|
||
objc::msg_send<id>(msg, "body"_sel), "UTF8String"_sel));
|
||
}),
|
||
"v@:@@");
|
||
objc_registerClassPair(cls);
|
||
}
|
||
auto instance = objc::msg_send<id>((id)cls, "new"_sel);
|
||
objc_setAssociatedObject(instance, "webview", (id)this,
|
||
OBJC_ASSOCIATION_ASSIGN);
|
||
return instance;
|
||
}
|
||
static id create_webkit_ui_delegate() {
|
||
objc::autoreleasepool arp;
|
||
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<BOOL>(parameters, "allowsMultipleSelection"_sel);
|
||
auto allows_directories =
|
||
objc::msg_send<BOOL>(parameters, "allowsDirectories"_sel);
|
||
|
||
// Show a panel for selecting files.
|
||
auto panel = objc::msg_send<id>("NSOpenPanel"_cls, "openPanel"_sel);
|
||
objc::msg_send<void>(panel, "setCanChooseFiles:"_sel, YES);
|
||
objc::msg_send<void>(panel, "setCanChooseDirectories:"_sel,
|
||
allows_directories);
|
||
objc::msg_send<void>(panel, "setAllowsMultipleSelection:"_sel,
|
||
allows_multiple_selection);
|
||
auto modal_response =
|
||
objc::msg_send<NSModalResponse>(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<id>(panel, "URLs"_sel)
|
||
: nullptr;
|
||
|
||
// Invoke the completion handler block.
|
||
auto sig = objc::msg_send<id>(
|
||
"NSMethodSignature"_cls, "signatureWithObjCTypes:"_sel, "v@?@");
|
||
auto invocation = objc::msg_send<id>(
|
||
"NSInvocation"_cls, "invocationWithMethodSignature:"_sel, sig);
|
||
objc::msg_send<void>(invocation, "setTarget:"_sel,
|
||
completion_handler);
|
||
objc::msg_send<void>(invocation, "setArgument:atIndex:"_sel, &urls,
|
||
1);
|
||
objc::msg_send<void>(invocation, "invoke"_sel);
|
||
}),
|
||
"v@:@@@@");
|
||
objc_registerClassPair(cls);
|
||
}
|
||
return objc::msg_send<id>((id)cls, "new"_sel);
|
||
}
|
||
static id create_window_delegate() {
|
||
objc::autoreleasepool arp;
|
||
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<id>(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>((id)cls, "new"_sel);
|
||
}
|
||
static id get_shared_application() {
|
||
return objc::msg_send<id>("NSApplication"_cls, "sharedApplication"_sel);
|
||
}
|
||
static cocoa_wkwebview_engine *get_associated_webview(id object) {
|
||
auto w =
|
||
(cocoa_wkwebview_engine *)objc_getAssociatedObject(object, "webview");
|
||
assert(w);
|
||
return w;
|
||
}
|
||
static id get_main_bundle() noexcept {
|
||
return objc::msg_send<id>("NSBundle"_cls, "mainBundle"_sel);
|
||
}
|
||
static bool is_app_bundled() noexcept {
|
||
auto bundle = get_main_bundle();
|
||
if (!bundle) {
|
||
return false;
|
||
}
|
||
auto bundle_path = objc::msg_send<id>(bundle, "bundlePath"_sel);
|
||
auto bundled =
|
||
objc::msg_send<BOOL>(bundle_path, "hasSuffix:"_sel, ".app"_str);
|
||
return !!bundled;
|
||
}
|
||
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
|
||
// otherwise not. Activating the app even when it has been launched from
|
||
// Finder does not seem to be harmful but calling this function is rarely
|
||
// needed as proper activation is normally taken care of for us.
|
||
// Bundled apps have a default activation policy of
|
||
// NSApplicationActivationPolicyRegular while non-bundled apps have a
|
||
// default activation policy of NSApplicationActivationPolicyProhibited.
|
||
if (!is_app_bundled()) {
|
||
// "setActivationPolicy:" must be invoked before
|
||
// "activateIgnoringOtherApps:" for activation to work.
|
||
objc::msg_send<void>(app, "setActivationPolicy:"_sel,
|
||
NSApplicationActivationPolicyRegular);
|
||
// Activate the app regardless of other active apps.
|
||
// This can be obtrusive so we only do it when necessary.
|
||
objc::msg_send<void>(app, "activateIgnoringOtherApps:"_sel, YES);
|
||
}
|
||
|
||
set_up_window();
|
||
}
|
||
void on_window_will_close(id /*delegate*/, id /*window*/) {
|
||
// Widget destroyed along with window.
|
||
m_webview = nullptr;
|
||
m_window = nullptr;
|
||
dispatch([this] { on_window_destroyed(); });
|
||
}
|
||
void set_up_window() {
|
||
objc::autoreleasepool arp;
|
||
|
||
// Main window
|
||
if (m_owns_window) {
|
||
m_window = objc::msg_send<id>("NSWindow"_cls, "alloc"_sel);
|
||
auto style = NSWindowStyleMaskTitled;
|
||
m_window = objc::msg_send<id>(
|
||
m_window, "initWithContentRect:styleMask:backing:defer:"_sel,
|
||
CGRectMake(0, 0, 0, 0), style, NSBackingStoreBuffered, NO);
|
||
|
||
m_window_delegate = create_window_delegate();
|
||
objc_setAssociatedObject(m_window_delegate, "webview", (id)this,
|
||
OBJC_ASSOCIATION_ASSIGN);
|
||
objc::msg_send<void>(m_window, "setDelegate:"_sel, m_window_delegate);
|
||
|
||
on_window_created();
|
||
}
|
||
|
||
set_up_web_view();
|
||
|
||
objc::msg_send<void>(m_window, "setContentView:"_sel, m_webview);
|
||
|
||
if (m_owns_window) {
|
||
objc::msg_send<void>(m_window, "makeKeyAndOrderFront:"_sel, nullptr);
|
||
}
|
||
}
|
||
void set_up_web_view() {
|
||
objc::autoreleasepool arp;
|
||
|
||
auto config = objc::autoreleased(
|
||
objc::msg_send<id>("WKWebViewConfiguration"_cls, "new"_sel));
|
||
|
||
m_manager = objc::msg_send<id>(config, "userContentController"_sel);
|
||
m_webview = objc::msg_send<id>("WKWebView"_cls, "alloc"_sel);
|
||
|
||
auto preferences = objc::msg_send<id>(config, "preferences"_sel);
|
||
auto yes_value =
|
||
objc::msg_send<id>("NSNumber"_cls, "numberWithBool:"_sel, YES);
|
||
|
||
if (m_debug) {
|
||
// Equivalent Obj-C:
|
||
// [[config preferences] setValue:@YES forKey:@"developerExtrasEnabled"];
|
||
objc::msg_send<id>(preferences, "setValue:forKey:"_sel, yes_value,
|
||
"developerExtrasEnabled"_str);
|
||
}
|
||
|
||
// Equivalent Obj-C:
|
||
// [[config preferences] setValue:@YES forKey:@"fullScreenEnabled"];
|
||
objc::msg_send<id>(preferences, "setValue:forKey:"_sel, yes_value,
|
||
"fullScreenEnabled"_str);
|
||
|
||
// Equivalent Obj-C:
|
||
// [[config preferences] setValue:@YES forKey:@"javaScriptCanAccessClipboard"];
|
||
objc::msg_send<id>(preferences, "setValue:forKey:"_sel, yes_value,
|
||
"javaScriptCanAccessClipboard"_str);
|
||
|
||
// Equivalent Obj-C:
|
||
// [[config preferences] setValue:@YES forKey:@"DOMPasteAllowed"];
|
||
objc::msg_send<id>(preferences, "setValue:forKey:"_sel, yes_value,
|
||
"DOMPasteAllowed"_str);
|
||
|
||
auto ui_delegate = objc::autoreleased(create_webkit_ui_delegate());
|
||
objc::msg_send<void>(m_webview, "initWithFrame:configuration:"_sel,
|
||
CGRectMake(0, 0, 0, 0), config);
|
||
objc::msg_send<void>(m_webview, "setUIDelegate:"_sel, ui_delegate);
|
||
|
||
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<void>(
|
||
m_webview, "setInspectable:"_sel,
|
||
objc::msg_send<id>("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 =
|
||
objc::autoreleased(create_script_message_handler());
|
||
objc::msg_send<void>(m_manager, "addScriptMessageHandler:name:"_sel,
|
||
script_message_handler, "external"_str);
|
||
|
||
init(R""(
|
||
window.external = {
|
||
invoke: function(s) {
|
||
window.webkit.messageHandlers.external.postMessage(s);
|
||
},
|
||
};
|
||
)"");
|
||
}
|
||
void stop_run_loop() {
|
||
objc::autoreleasepool arp;
|
||
auto app = get_shared_application();
|
||
// Request the run loop to stop. This doesn't immediately stop the loop.
|
||
objc::msg_send<void>(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<id>(
|
||
"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<void>(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;
|
||
}
|
||
|
||
// Blocks while depleting the run loop of events.
|
||
void deplete_run_loop_event_queue() {
|
||
objc::autoreleasepool arp;
|
||
auto app = get_shared_application();
|
||
bool done{};
|
||
dispatch([&] { done = true; });
|
||
auto mask = NSUIntegerMax; // NSEventMaskAny
|
||
// NSDefaultRunLoopMode
|
||
auto mode = objc::msg_send<id>("NSString"_cls, "stringWithUTF8String:"_sel,
|
||
"kCFRunLoopDefaultMode");
|
||
while (!done) {
|
||
objc::autoreleasepool arp;
|
||
auto event = objc::msg_send<id>(
|
||
app, "nextEventMatchingMask:untilDate:inMode:dequeue:"_sel, mask,
|
||
nullptr, mode, YES);
|
||
if (event) {
|
||
objc::msg_send<void>(app, "sendEvent:"_sel, event);
|
||
}
|
||
}
|
||
}
|
||
|
||
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
|
||
|
||
using browser_engine = detail::cocoa_wkwebview_engine;
|
||
|
||
} // namespace webview
|
||
|
||
#elif defined(WEBVIEW_EDGE)
|
||
|
||
//
|
||
// ====================================================================
|
||
//
|
||
// This implementation uses Win32 API to create a native window. It
|
||
// uses Edge/Chromium webview2 backend as a browser engine.
|
||
//
|
||
// ====================================================================
|
||
//
|
||
|
||
#define WIN32_LEAN_AND_MEAN
|
||
#include <shlobj.h>
|
||
#include <shlwapi.h>
|
||
#include <stdlib.h>
|
||
#include <windows.h>
|
||
|
||
#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 {
|
||
namespace detail {
|
||
|
||
using msg_cb_t = std::function<void(const std::string)>;
|
||
|
||
// 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 <typename T>
|
||
std::array<unsigned int, 4>
|
||
parse_version(const std::basic_string<T> &version) noexcept {
|
||
auto parse_component = [](auto sb, auto se) -> unsigned int {
|
||
try {
|
||
auto n = std::stol(std::basic_string<T>(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<unsigned int, 4> components{};
|
||
while (sb != end && se != end && ci < components.size()) {
|
||
if (*se == static_cast<T>('.')) {
|
||
components[ci++] = parse_component(sb, se);
|
||
sb = ++se;
|
||
continue;
|
||
}
|
||
++se;
|
||
}
|
||
if (sb < se && ci < components.size()) {
|
||
components[ci] = parse_component(sb, se);
|
||
}
|
||
return components;
|
||
}
|
||
|
||
template <typename T, std::size_t Length>
|
||
auto parse_version(const T (&version)[Length]) noexcept {
|
||
return parse_version(std::basic_string<T>(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();
|
||
}
|
||
std::vector<char> info_buffer;
|
||
info_buffer.reserve(info_buffer_length);
|
||
if (!GetFileVersionInfoW(file_path.c_str(), 0, info_buffer_length,
|
||
info_buffer.data())) {
|
||
return std::wstring();
|
||
}
|
||
auto sub_block = L"\\StringFileInfo\\040904B0\\ProductVersion";
|
||
LPWSTR version = nullptr;
|
||
unsigned int version_length = 0;
|
||
if (!VerQueryValueW(info_buffer.data(), sub_block,
|
||
reinterpret_cast<LPVOID *>(&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.
|
||
// RPC_E_CHANGED_MODE means that CoInitializeEx was already called with
|
||
// a different concurrency model.
|
||
switch (CoInitializeEx(nullptr, dwCoInit)) {
|
||
case S_OK:
|
||
case S_FALSE:
|
||
m_initialized = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
~com_init_wrapper() {
|
||
if (m_initialized) {
|
||
CoUninitialize();
|
||
m_initialized = false;
|
||
}
|
||
}
|
||
|
||
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) { *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;
|
||
}
|
||
|
||
bool is_initialized() const { return m_initialized; }
|
||
|
||
private:
|
||
bool m_initialized = false;
|
||
};
|
||
|
||
namespace ntdll_symbols {
|
||
using RtlGetVersion_t =
|
||
unsigned int /*NTSTATUS*/ (WINAPI *)(RTL_OSVERSIONINFOW *);
|
||
|
||
constexpr auto RtlGetVersion = library_symbol<RtlGetVersion_t>("RtlGetVersion");
|
||
} // namespace ntdll_symbols
|
||
|
||
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<DPI_AWARENESS_CONTEXT> 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
|
||
};
|
||
|
||
constexpr auto SetProcessDpiAwarenessContext =
|
||
library_symbol<SetProcessDpiAwarenessContext_t>(
|
||
"SetProcessDpiAwarenessContext");
|
||
constexpr auto SetProcessDPIAware =
|
||
library_symbol<SetProcessDPIAware_t>("SetProcessDPIAware");
|
||
constexpr auto GetDpiForWindow =
|
||
library_symbol<GetDpiForWindow_t>("GetDpiForWindow");
|
||
constexpr auto EnableNonClientDpiScaling =
|
||
library_symbol<EnableNonClientDpiScaling_t>("EnableNonClientDpiScaling");
|
||
constexpr auto AdjustWindowRectExForDpi =
|
||
library_symbol<AdjustWindowRectExForDpi_t>("AdjustWindowRectExForDpi");
|
||
constexpr auto GetWindowDpiAwarenessContext =
|
||
library_symbol<GetWindowDpiAwarenessContext_t>(
|
||
"GetWindowDpiAwarenessContext");
|
||
constexpr auto AreDpiAwarenessContextsEqual =
|
||
library_symbol<AreDpiAwarenessContextsEqual_t>(
|
||
"AreDpiAwarenessContextsEqual");
|
||
} // namespace user32_symbols
|
||
|
||
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_t>("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_t>("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) {
|
||
RegCloseKey(m_handle);
|
||
m_handle = nullptr;
|
||
}
|
||
}
|
||
|
||
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;
|
||
|
||
bool is_open() const { return !!m_handle; }
|
||
bool get_handle() const { return m_handle; }
|
||
|
||
template <typename Container>
|
||
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<LPBYTE>(&result[0]);
|
||
status =
|
||
RegQueryValueExW(m_handle, name, nullptr, nullptr, buf, &buf_length);
|
||
if (status != ERROR_SUCCESS) {
|
||
result.resize(0);
|
||
return;
|
||
}
|
||
}
|
||
|
||
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<char> data;
|
||
query_bytes(name, data);
|
||
if (data.size() < sizeof(DWORD)) {
|
||
return default_value;
|
||
}
|
||
return static_cast<unsigned int>(*reinterpret_cast<DWORD *>(data.data()));
|
||
}
|
||
|
||
private:
|
||
HKEY m_handle = nullptr;
|
||
};
|
||
|
||
// 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<int>(vi.dwBuildNumber) - static_cast<int>(build);
|
||
}
|
||
return static_cast<int>(vi.dwMinorVersion) - static_cast<int>(minor);
|
||
}
|
||
return static_cast<int>(vi.dwMajorVersion) - static_cast<int>(major);
|
||
}
|
||
return false;
|
||
}
|
||
|
||
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)) {
|
||
auto dpi_awareness =
|
||
reinterpret_cast<user32_symbols::DPI_AWARENESS_CONTEXT>(
|
||
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;
|
||
}
|
||
if (auto shcore = native_library(L"shcore.dll")) {
|
||
if (auto fn = shcore.get(shcore_symbols::SetProcessDpiAwareness)) {
|
||
auto result = fn(shcore_symbols::PROCESS_PER_MONITOR_DPI_AWARE);
|
||
return result == S_OK || result == E_ACCESSDENIED;
|
||
}
|
||
}
|
||
if (auto fn = user32.get(user32_symbols::SetProcessDPIAware)) {
|
||
return !!fn();
|
||
}
|
||
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_CONTEXT>(
|
||
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<int>(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<UINT>(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 <typename T>
|
||
std::basic_string<T>
|
||
get_last_native_path_component(const std::basic_string<T> &path) {
|
||
auto pos = path.find_last_of(static_cast<T>('\\'));
|
||
if (pos != std::basic_string<T>::npos) {
|
||
return path.substr(pos + 1);
|
||
}
|
||
return std::basic_string<T>();
|
||
}
|
||
#endif /* WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL */
|
||
|
||
template <typename T> 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_t>(
|
||
"CreateWebViewEnvironmentWithOptionsInternal");
|
||
static constexpr auto DllCanUnloadNow =
|
||
library_symbol<DllCanUnloadNow_t>("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_t>(
|
||
"CreateCoreWebView2EnvironmentWithOptions");
|
||
static constexpr auto GetAvailableCoreWebView2BrowserVersionString =
|
||
library_symbol<GetAvailableCoreWebView2BrowserVersionString_t>(
|
||
"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<LPWSTR>(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<ICoreWebView2CreateCoreWebView2ControllerCompletedHandler>{
|
||
IID_ICoreWebView2CreateCoreWebView2ControllerCompletedHandler};
|
||
|
||
static constexpr auto environment_completed =
|
||
cast_info_t<ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler>{
|
||
IID_ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler};
|
||
|
||
static constexpr auto message_received =
|
||
cast_info_t<ICoreWebView2WebMessageReceivedEventHandler>{
|
||
IID_ICoreWebView2WebMessageReceivedEventHandler};
|
||
|
||
static constexpr auto permission_requested =
|
||
cast_info_t<ICoreWebView2PermissionRequestedEventHandler>{
|
||
IID_ICoreWebView2PermissionRequestedEventHandler};
|
||
} // namespace cast_info
|
||
} // namespace mswebview2
|
||
|
||
class webview2_com_handler
|
||
: public ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler,
|
||
public ICoreWebView2CreateCoreWebView2ControllerCompletedHandler,
|
||
public ICoreWebView2WebMessageReceivedEventHandler,
|
||
public ICoreWebView2PermissionRequestedEventHandler {
|
||
using webview2_com_handler_cb_t =
|
||
std::function<void(ICoreWebView2Controller *, ICoreWebView2 *webview)>;
|
||
|
||
public:
|
||
webview2_com_handler(HWND hwnd, msg_cb_t msgCb, webview2_com_handler_cb_t cb)
|
||
: m_window(hwnd), m_msgCb(msgCb), m_cb(cb) {}
|
||
|
||
virtual ~webview2_com_handler() = default;
|
||
webview2_com_handler(const webview2_com_handler &other) = delete;
|
||
webview2_com_handler &operator=(const webview2_com_handler &other) = delete;
|
||
webview2_com_handler(webview2_com_handler &&other) = delete;
|
||
webview2_com_handler &operator=(webview2_com_handler &&other) = delete;
|
||
|
||
ULONG STDMETHODCALLTYPE AddRef() { return ++m_ref_count; }
|
||
ULONG STDMETHODCALLTYPE Release() {
|
||
if (m_ref_count > 1) {
|
||
return --m_ref_count;
|
||
}
|
||
delete this;
|
||
return 0;
|
||
}
|
||
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, LPVOID *ppv) {
|
||
using namespace mswebview2::cast_info;
|
||
|
||
if (!ppv) {
|
||
return E_POINTER;
|
||
}
|
||
|
||
// 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) {
|
||
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);
|
||
webview->add_WebMessageReceived(this, &token);
|
||
webview->add_PermissionRequested(this, &token);
|
||
|
||
m_cb(controller, webview);
|
||
return S_OK;
|
||
}
|
||
HRESULT STDMETHODCALLTYPE Invoke(
|
||
ICoreWebView2 *sender, ICoreWebView2WebMessageReceivedEventArgs *args) {
|
||
LPWSTR message;
|
||
args->TryGetWebMessageAsString(&message);
|
||
m_msgCb(narrow_string(message));
|
||
sender->PostWebMessageAsString(message);
|
||
|
||
CoTaskMemFree(message);
|
||
return S_OK;
|
||
}
|
||
HRESULT STDMETHODCALLTYPE
|
||
Invoke(ICoreWebView2 * /*sender*/,
|
||
ICoreWebView2PermissionRequestedEventArgs *args) {
|
||
COREWEBVIEW2_PERMISSION_KIND kind;
|
||
args->get_PermissionKind(&kind);
|
||
if (kind == COREWEBVIEW2_PERMISSION_KIND_CLIPBOARD_READ) {
|
||
args->put_State(COREWEBVIEW2_PERMISSION_STATE_ALLOW);
|
||
}
|
||
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 <typename T>
|
||
T *cast_if_equal_iid(REFIID riid, const cast_info_t<T> &info,
|
||
LPVOID *ppv = nullptr) noexcept {
|
||
T *ptr = nullptr;
|
||
if (IsEqualIID(riid, info.iid)) {
|
||
ptr = static_cast<T *>(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<HRESULT()> 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<ULONG> m_ref_count{1};
|
||
std::function<HRESULT()> m_attempt_handler;
|
||
unsigned int m_max_attempts = 5;
|
||
unsigned int m_attempts = 0;
|
||
};
|
||
|
||
class win32_edge_engine : public engine_base {
|
||
public:
|
||
win32_edge_engine(bool debug, void *window) : m_owns_window{!window} {
|
||
if (!is_webview2_available()) {
|
||
return;
|
||
}
|
||
|
||
HINSTANCE hInstance = GetModuleHandle(nullptr);
|
||
|
||
if (m_owns_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);
|
||
|
||
// Create a top-level window.
|
||
WNDCLASSEXW wc;
|
||
ZeroMemory(&wc, sizeof(WNDCLASSEX));
|
||
wc.cbSize = sizeof(WNDCLASSEX);
|
||
wc.hInstance = hInstance;
|
||
wc.lpszClassName = L"webview";
|
||
wc.hIcon = icon;
|
||
wc.lpfnWndProc = (WNDPROC)(+[](HWND hwnd, UINT msg, WPARAM wp,
|
||
LPARAM lp) -> LRESULT {
|
||
win32_edge_engine *w{};
|
||
|
||
if (msg == WM_NCCREATE) {
|
||
auto *lpcs{reinterpret_cast<LPCREATESTRUCT>(lp)};
|
||
w = static_cast<win32_edge_engine *>(lpcs->lpCreateParams);
|
||
w->m_window = hwnd;
|
||
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(w));
|
||
enable_non_client_dpi_scaling_if_needed(hwnd);
|
||
apply_window_theme(hwnd);
|
||
} else {
|
||
w = reinterpret_cast<win32_edge_engine *>(
|
||
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:
|
||
w->m_window = nullptr;
|
||
SetWindowLongPtrW(hwnd, GWLP_USERDATA, 0);
|
||
w->on_window_destroyed();
|
||
break;
|
||
case WM_GETMINMAXINFO: {
|
||
auto lpmmi = (LPMINMAXINFO)lp;
|
||
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<int>(wp);
|
||
auto *size{reinterpret_cast<SIZE *>(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<int>(HIWORD(wp));
|
||
w->on_dpi_changed(dpi);
|
||
break;
|
||
}
|
||
case WM_SETTINGCHANGE: {
|
||
auto *area = reinterpret_cast<const wchar_t *>(lp);
|
||
if (area) {
|
||
w->on_system_setting_change(area);
|
||
}
|
||
break;
|
||
}
|
||
case WM_ACTIVATE:
|
||
if (LOWORD(wp) != WA_INACTIVE) {
|
||
w->focus_webview();
|
||
}
|
||
break;
|
||
default:
|
||
return DefWindowProcW(hwnd, msg, wp, lp);
|
||
}
|
||
return 0;
|
||
});
|
||
RegisterClassExW(&wc);
|
||
|
||
CreateWindowW(L"webview", L"", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT,
|
||
CW_USEDEFAULT, 0, 0, nullptr, nullptr, hInstance, this);
|
||
if (m_window == nullptr) {
|
||
return;
|
||
}
|
||
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 = IsWindow(static_cast<HWND>(window))
|
||
? static_cast<HWND>(window)
|
||
: *(static_cast<HWND *>(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<LPCREATESTRUCT>(lp)};
|
||
w = static_cast<win32_edge_engine *>(lpcs->lpCreateParams);
|
||
w->m_widget = hwnd;
|
||
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(w));
|
||
} else {
|
||
w = reinterpret_cast<win32_edge_engine *>(
|
||
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);
|
||
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<LPCREATESTRUCT>(lp)};
|
||
w = static_cast<win32_edge_engine *>(lpcs->lpCreateParams);
|
||
w->m_message_window = hwnd;
|
||
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(w));
|
||
} else {
|
||
w = reinterpret_cast<win32_edge_engine *>(
|
||
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);
|
||
|
||
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_widget, debug, cb);
|
||
}
|
||
|
||
virtual ~win32_edge_engine() {
|
||
if (m_com_handler) {
|
||
m_com_handler->Release();
|
||
m_com_handler = nullptr;
|
||
}
|
||
if (m_webview) {
|
||
m_webview->Release();
|
||
m_webview = nullptr;
|
||
}
|
||
if (m_controller) {
|
||
m_controller->Release();
|
||
m_controller = nullptr;
|
||
}
|
||
// Replace wndproc to avoid callbacks and other bad things during
|
||
// destruction.
|
||
auto wndproc = reinterpret_cast<LONG_PTR>(
|
||
+[](HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) -> LRESULT {
|
||
return DefWindowProcW(hwnd, msg, wp, lp);
|
||
});
|
||
if (m_widget) {
|
||
SetWindowLongPtrW(m_widget, GWLP_WNDPROC, wndproc);
|
||
}
|
||
if (m_window && m_owns_window) {
|
||
SetWindowLongPtrW(m_window, GWLP_WNDPROC, wndproc);
|
||
}
|
||
if (m_widget) {
|
||
DestroyWindow(m_widget);
|
||
m_widget = nullptr;
|
||
}
|
||
if (m_window) {
|
||
if (m_owns_window) {
|
||
DestroyWindow(m_window);
|
||
on_window_destroyed(true);
|
||
}
|
||
m_window = nullptr;
|
||
}
|
||
if (m_owns_window) {
|
||
// Not strictly needed for windows to close immediately but aligns
|
||
// behavior across backends.
|
||
deplete_run_loop_event_queue();
|
||
}
|
||
// We need the message window in order to deplete the event queue.
|
||
if (m_message_window) {
|
||
SetWindowLongPtrW(m_message_window, GWLP_WNDPROC, wndproc);
|
||
DestroyWindow(m_message_window);
|
||
m_message_window = nullptr;
|
||
}
|
||
}
|
||
|
||
win32_edge_engine(const win32_edge_engine &other) = delete;
|
||
win32_edge_engine &operator=(const win32_edge_engine &other) = delete;
|
||
win32_edge_engine(win32_edge_engine &&other) = delete;
|
||
win32_edge_engine &operator=(win32_edge_engine &&other) = delete;
|
||
|
||
void run_impl() override {
|
||
MSG msg;
|
||
while (GetMessageW(&msg, nullptr, 0, 0) > 0) {
|
||
TranslateMessage(&msg);
|
||
DispatchMessageW(&msg);
|
||
}
|
||
}
|
||
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 { PostQuitMessage(0); }
|
||
void dispatch_impl(dispatch_fn_t f) override {
|
||
PostMessageW(m_message_window, WM_APP, 0, (LPARAM) new dispatch_fn_t(f));
|
||
}
|
||
|
||
void set_title_impl(const std::string &title) override {
|
||
SetWindowTextW(m_window, widen_string(title).c_str());
|
||
}
|
||
|
||
void set_size_impl(int width, int height, webview_hint_t hints) override {
|
||
auto style = GetWindowLong(m_window, GWL_STYLE);
|
||
if (hints == WEBVIEW_HINT_FIXED) {
|
||
style &= ~(WS_THICKFRAME | WS_MAXIMIZEBOX);
|
||
} else {
|
||
style |= (WS_THICKFRAME | WS_MAXIMIZEBOX);
|
||
}
|
||
SetWindowLong(m_window, GWL_STYLE, style);
|
||
|
||
if (hints == WEBVIEW_HINT_MAX) {
|
||
m_maxsz.x = width;
|
||
m_maxsz.y = height;
|
||
} else if (hints == WEBVIEW_HINT_MIN) {
|
||
m_minsz.x = width;
|
||
m_minsz.y = height;
|
||
} else {
|
||
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);
|
||
}
|
||
}
|
||
|
||
void navigate_impl(const std::string &url) override {
|
||
auto wurl = widen_string(url);
|
||
m_webview->Navigate(wurl.c_str());
|
||
}
|
||
|
||
void init_impl(const std::string &js) override {
|
||
auto wjs = widen_string(js);
|
||
m_webview->AddScriptToExecuteOnDocumentCreated(wjs.c_str(), nullptr);
|
||
}
|
||
|
||
void eval_impl(const std::string &js) override {
|
||
auto wjs = widen_string(js);
|
||
m_webview->ExecuteScript(wjs.c_str(), nullptr);
|
||
}
|
||
|
||
void set_html_impl(const std::string &html) override {
|
||
m_webview->NavigateToString(widen_string(html).c_str());
|
||
}
|
||
|
||
private:
|
||
bool embed(HWND wnd, bool debug, msg_cb_t cb) {
|
||
std::atomic_flag flag = ATOMIC_FLAG_INIT;
|
||
flag.test_and_set();
|
||
|
||
wchar_t currentExePath[MAX_PATH];
|
||
GetModuleFileNameW(nullptr, currentExePath, MAX_PATH);
|
||
wchar_t *currentExeName = PathFindFileNameW(currentExePath);
|
||
|
||
wchar_t dataPath[MAX_PATH];
|
||
if (!SUCCEEDED(
|
||
SHGetFolderPathW(nullptr, CSIDL_APPDATA, nullptr, 0, dataPath))) {
|
||
return false;
|
||
}
|
||
wchar_t userDataFolder[MAX_PATH];
|
||
PathCombineW(userDataFolder, dataPath, currentExeName);
|
||
|
||
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();
|
||
});
|
||
|
||
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.
|
||
bool got_quit_msg = false;
|
||
MSG msg;
|
||
while (flag.test_and_set() && GetMessageW(&msg, nullptr, 0, 0) >= 0) {
|
||
if (msg.message == WM_QUIT) {
|
||
got_quit_msg = true;
|
||
break;
|
||
}
|
||
TranslateMessage(&msg);
|
||
DispatchMessageW(&msg);
|
||
}
|
||
if (got_quit_msg) {
|
||
return false;
|
||
}
|
||
if (!m_controller || !m_webview) {
|
||
return false;
|
||
}
|
||
ICoreWebView2Settings *settings = nullptr;
|
||
auto res = m_webview->get_Settings(&settings);
|
||
if (res != S_OK) {
|
||
return false;
|
||
}
|
||
res = settings->put_AreDevToolsEnabled(debug ? TRUE : FALSE);
|
||
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)}");
|
||
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_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);
|
||
}
|
||
}
|
||
|
||
bool is_webview2_available() const noexcept {
|
||
LPWSTR version_info = nullptr;
|
||
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;
|
||
if (version_info) {
|
||
CoTaskMemFree(version_info);
|
||
}
|
||
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);
|
||
}
|
||
}
|
||
|
||
// Blocks while depleting the run loop of events.
|
||
void deplete_run_loop_event_queue() {
|
||
bool done{};
|
||
dispatch([&] { done = true; });
|
||
while (!done) {
|
||
MSG msg;
|
||
if (GetMessageW(&msg, nullptr, 0, 0) > 0) {
|
||
TranslateMessage(&msg);
|
||
DispatchMessageW(&msg);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 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};
|
||
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{};
|
||
bool m_owns_window{};
|
||
};
|
||
|
||
} // namespace detail
|
||
|
||
using browser_engine = detail::win32_edge_engine;
|
||
|
||
} // namespace webview
|
||
|
||
#endif /* WEBVIEW_GTK, WEBVIEW_COCOA, WEBVIEW_EDGE */
|
||
|
||
namespace webview {
|
||
using webview = browser_engine;
|
||
} // namespace webview
|
||
|
||
WEBVIEW_API webview_t webview_create(int debug, void *wnd) {
|
||
auto w = new webview::webview(debug, wnd);
|
||
if (!w->window()) {
|
||
delete w;
|
||
return nullptr;
|
||
}
|
||
return w;
|
||
}
|
||
|
||
WEBVIEW_API void webview_destroy(webview_t w) {
|
||
delete static_cast<webview::webview *>(w);
|
||
}
|
||
|
||
WEBVIEW_API void webview_run(webview_t w) {
|
||
static_cast<webview::webview *>(w)->run();
|
||
}
|
||
|
||
WEBVIEW_API void webview_terminate(webview_t w) {
|
||
static_cast<webview::webview *>(w)->terminate();
|
||
}
|
||
|
||
WEBVIEW_API void webview_dispatch(webview_t w, void (*fn)(webview_t, void *),
|
||
void *arg) {
|
||
static_cast<webview::webview *>(w)->dispatch([=]() { fn(w, arg); });
|
||
}
|
||
|
||
WEBVIEW_API void *webview_get_window(webview_t w) {
|
||
return static_cast<webview::webview *>(w)->window();
|
||
}
|
||
|
||
WEBVIEW_API void *webview_get_native_handle(webview_t w,
|
||
webview_native_handle_kind_t kind) {
|
||
auto *w_ = static_cast<webview::webview *>(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<webview::webview *>(w)->set_title(title);
|
||
}
|
||
|
||
WEBVIEW_API void webview_set_size(webview_t w, int width, int height,
|
||
webview_hint_t hints) {
|
||
static_cast<webview::webview *>(w)->set_size(width, height, hints);
|
||
}
|
||
|
||
WEBVIEW_API void webview_navigate(webview_t w, const char *url) {
|
||
static_cast<webview::webview *>(w)->navigate(url);
|
||
}
|
||
|
||
WEBVIEW_API void webview_set_html(webview_t w, const char *html) {
|
||
static_cast<webview::webview *>(w)->set_html(html);
|
||
}
|
||
|
||
WEBVIEW_API void webview_init(webview_t w, const char *js) {
|
||
static_cast<webview::webview *>(w)->init(js);
|
||
}
|
||
|
||
WEBVIEW_API void webview_eval(webview_t w, const char *js) {
|
||
static_cast<webview::webview *>(w)->eval(js);
|
||
}
|
||
|
||
WEBVIEW_API void webview_bind(webview_t w, const char *name,
|
||
void (*fn)(const char *seq, const char *req,
|
||
void *arg),
|
||
void *arg) {
|
||
static_cast<webview::webview *>(w)->bind(
|
||
name,
|
||
[=](const std::string &seq, const std::string &req, void *arg) {
|
||
fn(seq.c_str(), req.c_str(), arg);
|
||
},
|
||
arg);
|
||
}
|
||
|
||
WEBVIEW_API void webview_unbind(webview_t w, const char *name) {
|
||
static_cast<webview::webview *>(w)->unbind(name);
|
||
}
|
||
|
||
WEBVIEW_API void webview_return(webview_t w, const char *seq, int status,
|
||
const char *result) {
|
||
static_cast<webview::webview *>(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 */
|