diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..163eb75
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,9 @@
+root = true
+
+[*.cr]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+indent_style = space
+indent_size = 2
+trim_trailing_whitespace = true
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..65db2fd
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+/docs/
+/lib/
+/bin/
+/.shards/
+*.dwarf
+# Libraries don't need dependency lock
+# Dependencies will be locked in applications that use them
+/shard.lock
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..765f0e9
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,6 @@
+language: crystal
+
+# Uncomment the following if you'd like Travis to run specs and check code formatting
+# script:
+# - crystal spec
+# - crystal tool format --check
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..b34727a
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2020 Ali Naqvi
+
+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.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..4afa161
--- /dev/null
+++ b/README.md
@@ -0,0 +1,153 @@
+# Crystal Webview
+
+Crystal language bindings for [zserge's Webview](https://github.com/zserge/webview) which is an excellent cross-platform single header webview library for C/C++ using Gtk, Cocoa or MSHTML/Edge repectively.
+
+**Webview** relys on default rendering engine of host Operating System, thus binaries generated with this Shard will be much more leaner as compared to [Electron](https://github.com/electron/electron) which bundles Chromium with each distribution.
+
+Shard Supports **Two-way bindings** between Crystal and JavaScript. You can invoke JS code via `Webview::Webview#eval` and calling Crystal code from JS is done via `WebView::Webview#bind` (refer to Example 3 for sample on how to invoke Crystal functions from JS)
+
+Webview supported platforms and the engines you can expect to render your application content are as follows:
+
+| Operating System | Browser Engine Used |
+| ---------------- | ------------------- |
+| OSX | Cocoa/WebKit |
+| Linux | Gtk-webkit2 |
+| Windows | MSHTML or EdgeHTML |
+
+## Pre-requisite
+1. Make sure you compile `ext/webview.cc` and save object file `webview.o` under respective OS folder in `ext` folder. Follow instructions for your specific OS
+
+### Mac compilation
+```
+cd ext
+c++ -c webview.cc -o darwin/webview.o -DWEBVIEW_COCOA=1 -DOBJC_OLD_DISPATCH_PROTOTYPES=1 -std=c++11
+```
+
+### Linux compilation
+If you're planning on targeting Linux you must ensure that Webkit2gtk is already installed and available for discovery via the pkg-config command.
+
+```
+cd ext
+c++ -c webview.cc -o linux/webview.o -DWEBVIEW_GTK=1 -std=c++11
+```
+
+
+## Installation
+
+1. Add the dependency to your `shard.yml`:
+
+ ```yaml
+ dependencies:
+ webview:
+ github: naqvis/webview
+ ```
+
+2. Run `shards install`
+
+## Usage
+
+### Example 1: Loading URL
+
+```crystal
+require "webview"
+
+wv = Webview.window(640, 480, Webview::SizeHints::NONE, "Hello WebView", "http://crystal-lang.org")
+wv.run
+wv.destroy
+```
+
+### Example 2: Loading HTML
+
+```crystal
+require "webview"
+
+html = <<-HTML
+data:text/html,
+
+Hello,World!
+
+
+
+
+
+
+
+
+ London
+
+ London is the capital city of England. It is the most populous city in the United Kingdom, with a metropolitan area of over 13 million inhabitants.
+ Standing on the River Thames, London has been a major settlement for two millennia, its history going back to its founding by the Romans, who named it Londinium.
+
+
Copyright © W3Schools.com
+
+
+
+HTML
+
+wv = Webview.window(640, 480, Webview::SizeHints::NONE, "Hello WebView", html)
+wv.run
+wv.destroy
+```
+
+### Example 3: Calling Crystal code from JavaScript
+```crystal
+require "webview"
+
+html = <<-HTML
+data:text/html,
+
+ hello
+
+
+HTML
+
+wv = Webview.window(640, 480, Webview::SizeHints::NONE, "Hello WebView", html, true)
+
+wv.bind("noop", Webview::JSProc.new { |a|
+ pp "Noop called with arguments: #{a}"
+ JSON::Any.new("noop")
+})
+
+wv.bind("add", Webview::JSProc.new { |a|
+ pp "add called with arguments: #{a}"
+ ret = 0_i64
+ a.each do |v|
+ ret += v.as_i64
+ end
+ JSON::Any.new(ret)
+})
+
+
+wv.run
+wv.destroy
+```
+
+
+## Contributing
+
+1. Fork it ()
+2. Create your feature branch (`git checkout -b my-new-feature`)
+3. Commit your changes (`git commit -am 'Add some feature'`)
+4. Push to the branch (`git push origin my-new-feature`)
+5. Create a new Pull Request
+
+## Contributors
+
+- [Ali Naqvi](https://github.com/naqvis) - creator and maintainer
diff --git a/ext/darwin/webview.o b/ext/darwin/webview.o
new file mode 100644
index 0000000..8adc52f
Binary files /dev/null and b/ext/darwin/webview.o differ
diff --git a/ext/webview.cc b/ext/webview.cc
new file mode 100644
index 0000000..28b64df
--- /dev/null
+++ b/ext/webview.cc
@@ -0,0 +1,2 @@
+#define WEBVIEW_IMPLEMENTATION
+#include "webview.h"
diff --git a/ext/webview.h b/ext/webview.h
new file mode 100644
index 0000000..e51839c
--- /dev/null
+++ b/ext/webview.h
@@ -0,0 +1,1228 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2017 Serge Zaitsev
+ *
+ * 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.
+ */
+#ifndef WEBVIEW_H
+#define WEBVIEW_H
+
+#ifndef WEBVIEW_API
+#define WEBVIEW_API extern
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef void *webview_t;
+
+// Creates a new webview instance. If debug is non-zero - developer tools will
+// be enabled (if the platform supports them). Window parameter can be a
+// pointer to the native window handle. If it's non-null - then child WebView
+// is embedded into the given parent window. Otherwise a new window is created.
+// Depending on the platform, a GtkWindow, NSWindow or HWND pointer can be
+// passed here.
+WEBVIEW_API webview_t webview_create(int debug, void *window);
+
+// Destroys a webview and closes the native window.
+WEBVIEW_API void webview_destroy(webview_t w);
+
+// Runs the main loop until it's terminated. After this function exits - you
+// must destroy the webview.
+WEBVIEW_API void webview_run(webview_t w);
+
+// Stops the main loop. It is safe to call this function from another other
+// background thread.
+WEBVIEW_API void webview_terminate(webview_t w);
+
+// Posts a function to be executed on the main thread. You normally do not need
+// to call this function, unless you want to tweak the native window.
+WEBVIEW_API void
+webview_dispatch(webview_t w, void (*fn)(webview_t w, void *arg), void *arg);
+
+// Returns a native window handle pointer. When using GTK backend the pointer
+// is GtkWindow pointer, when using Cocoa backend the pointer is NSWindow
+// pointer, when using Win32 backend the pointer is HWND pointer.
+WEBVIEW_API void *webview_get_window(webview_t w);
+
+// Updates the title of the native window. Must be called from the UI thread.
+WEBVIEW_API void webview_set_title(webview_t w, const char *title);
+
+// Window size hints
+#define WEBVIEW_HINT_NONE 0 // Width and height are default size
+#define WEBVIEW_HINT_MIN 1 // Width and height are minimum bounds
+#define WEBVIEW_HINT_MAX 2 // Width and height are maximum bounds
+#define WEBVIEW_HINT_FIXED 3 // Window size can not be changed by a user
+// Updates native window size. See WEBVIEW_HINT constants.
+WEBVIEW_API void webview_set_size(webview_t w, int width, int height,
+ int hints);
+
+// Navigates webview to the given URL. URL may be a data URI, i.e.
+// "data:text/text,...". It is often ok not to url-encode it
+// properly, webview will re-encode it for you.
+WEBVIEW_API void webview_navigate(webview_t w, const char *url);
+
+// Injects JavaScript code at the initialization of the new page. Every time
+// the webview will open a the new page - this initialization code will be
+// executed. It is guaranteed that code is executed before window.onload.
+WEBVIEW_API void webview_init(webview_t w, const char *js);
+
+// Evaluates arbitrary JavaScript code. Evaluation happens asynchronously, also
+// the result of the expression is ignored. Use RPC bindings if you want to
+// receive notifications about the results of the evaluation.
+WEBVIEW_API void webview_eval(webview_t w, const char *js);
+
+// Binds a native C callback so that it will appear under the given name as a
+// global JavaScript function. Internally it uses webview_init(). Callback
+// receives a request string and a user-provided argument pointer. Request
+// string is a JSON array of all the arguments passed to the JavaScript
+// function.
+WEBVIEW_API void webview_bind(webview_t w, const char *name,
+ void (*fn)(const char *seq, const char *req,
+ void *arg),
+ void *arg);
+
+// Allows to return a value from the native binding. Original request pointer
+// must be provided to help internal RPC engine match requests with responses.
+// If status is zero - result is expected to be a valid JSON result value.
+// If status is not zero - result is an error JSON object.
+WEBVIEW_API void webview_return(webview_t w, const char *seq, int status,
+ const char *result);
+
+#ifdef __cplusplus
+}
+#endif
+
+#ifndef WEBVIEW_HEADER
+
+#if !defined(WEBVIEW_GTK) && !defined(WEBVIEW_COCOA) && !defined(WEBVIEW_EDGE)
+#if defined(__linux__)
+#define WEBVIEW_GTK
+#elif defined(__APPLE__)
+#define WEBVIEW_COCOA
+#elif defined(_WIN32)
+#define WEBVIEW_EDGE
+#else
+#error "please, specify webview backend"
+#endif
+#endif
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+namespace webview {
+using dispatch_fn_t = std::function;
+
+inline std::string url_encode(const std::string s) {
+ std::string encoded;
+ for (unsigned int i = 0; i < s.length(); i++) {
+ auto c = s[i];
+ if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
+ encoded = encoded + c;
+ } else {
+ char hex[4];
+ snprintf(hex, sizeof(hex), "%%%02x", c);
+ encoded = encoded + hex;
+ }
+ }
+ return encoded;
+}
+
+inline std::string url_decode(const std::string s) {
+ std::string decoded;
+ for (unsigned int i = 0; i < s.length(); i++) {
+ if (s[i] == '%') {
+ int n;
+ n = std::stoul(s.substr(i + 1, 2), nullptr, 16);
+ decoded = decoded + static_cast(n);
+ i = i + 2;
+ } else if (s[i] == '+') {
+ decoded = decoded + ' ';
+ } else {
+ decoded = decoded + s[i];
+ }
+ }
+ return decoded;
+}
+
+inline std::string html_from_uri(const std::string s) {
+ if (s.substr(0, 15) == "data:text/html,") {
+ return url_decode(s.substr(15));
+ }
+ return "";
+}
+
+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 = NULL;
+ int index = 1;
+ int depth = 0;
+ int utf8_bytes = 0;
+
+ if (key == NULL) {
+ index = keysz;
+ keysz = 0;
+ }
+
+ *value = NULL;
+ *valuesz = 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;
+ unsigned char c = *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 != NULL && index == 0) {
+ *valuesz = (size_t)(s + 1 - *value);
+ return 0;
+ } else if (keysz > 0 && k != NULL) {
+ if (keysz == (size_t)(s - k - 1) && memcmp(key, k + 1, keysz) == 0) {
+ index = 0;
+ } else {
+ index = 2;
+ }
+ k = NULL;
+ }
+ }
+ }
+
+ if (action == JSON_ACTION_START_STRUCT) {
+ depth++;
+ }
+ }
+ return -1;
+}
+
+inline std::string json_escape(std::string s) {
+ // TODO: implement
+ return '"' + s + '"';
+}
+
+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 != NULL) {
+ *out++ = c;
+ }
+ s++;
+ n--;
+ r++;
+ }
+ if (*s != '"') {
+ return -1;
+ }
+ if (out != NULL) {
+ *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 == "") {
+ 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 std::string(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 "";
+}
+
+} // namespace webview
+
+#if defined(WEBVIEW_GTK)
+//
+// ====================================================================
+//
+// This implementation uses webkit2gtk backend. It requires gtk+3.0 and
+// webkit2gtk-4.0 libraries. Proper compiler flags can be retrieved via:
+//
+// pkg-config --cflags --libs gtk+-3.0 webkit2gtk-4.0
+//
+// ====================================================================
+//
+#include
+#include
+#include
+
+namespace webview {
+
+class gtk_webkit_engine {
+public:
+ gtk_webkit_engine(bool debug, void *window)
+ : m_window(static_cast(window)) {
+ gtk_init_check(0, NULL);
+ m_window = static_cast(window);
+ if (m_window == nullptr) {
+ m_window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
+ }
+ g_signal_connect(G_OBJECT(m_window), "destroy",
+ G_CALLBACK(+[](GtkWidget *, gpointer arg) {
+ static_cast(arg)->terminate();
+ }),
+ this);
+ // 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(arg);
+#if WEBKIT_MAJOR_VERSION >= 2 && WEBKIT_MINOR_VERSION >= 22
+ JSCValue *value =
+ webkit_javascript_result_get_js_value(r);
+ char *s = jsc_value_to_string(value);
+#else
+ JSGlobalContextRef ctx =
+ webkit_javascript_result_get_global_context(r);
+ JSValueRef value = webkit_javascript_result_get_value(r);
+ JSStringRef js = JSValueToStringCopy(ctx, value, NULL);
+ size_t n = JSStringGetMaximumUTF8CStringSize(js);
+ char *s = g_new(char, n);
+ JSStringGetUTF8CString(js, s, n);
+ JSStringRelease(js);
+#endif
+ 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_grab_focus(GTK_WIDGET(m_webview));
+
+ if (debug) {
+ WebKitSettings *settings =
+ webkit_web_view_get_settings(WEBKIT_WEB_VIEW(m_webview));
+ webkit_settings_set_enable_write_console_messages_to_stdout(settings,
+ true);
+ webkit_settings_set_enable_developer_extras(settings, true);
+ }
+
+ gtk_widget_show_all(m_window);
+ }
+ void *window() { return (void *)m_window; }
+ void run() { gtk_main(); }
+ void terminate() { gtk_main_quit(); }
+ void dispatch(std::function f) {
+ g_idle_add_full(G_PRIORITY_HIGH_IDLE, (GSourceFunc)([](void *f) -> int {
+ (*static_cast(f))();
+ return G_SOURCE_REMOVE;
+ }),
+ new std::function(f),
+ [](void *f) { delete static_cast(f); });
+ }
+
+ void set_title(const std::string title) {
+ gtk_window_set_title(GTK_WINDOW(m_window), title.c_str());
+ }
+
+ void set_size(int width, int height, int hints) {
+ 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(const std::string url) {
+ webkit_web_view_load_uri(WEBKIT_WEB_VIEW(m_webview), url.c_str());
+ }
+
+ void init(const std::string js) {
+ WebKitUserContentManager *manager =
+ webkit_web_view_get_user_content_manager(WEBKIT_WEB_VIEW(m_webview));
+ webkit_user_content_manager_add_script(
+ manager, webkit_user_script_new(
+ js.c_str(), WEBKIT_USER_CONTENT_INJECT_TOP_FRAME,
+ WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, NULL, NULL));
+ }
+
+ void eval(const std::string js) {
+ webkit_web_view_run_javascript(WEBKIT_WEB_VIEW(m_webview), js.c_str(), NULL,
+ NULL, NULL);
+ }
+
+private:
+ virtual void on_message(const std::string msg) = 0;
+ GtkWidget *m_window;
+ GtkWidget *m_webview;
+};
+
+using browser_engine = 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.
+//
+// ====================================================================
+//
+
+#define OBJC_OLD_DISPATCH_PROTOTYPES 1
+#include
+#include
+
+#define NSBackingStoreBuffered 2
+
+#define NSWindowStyleMaskResizable 8
+#define NSWindowStyleMaskMiniaturizable 4
+#define NSWindowStyleMaskTitled 1
+#define NSWindowStyleMaskClosable 2
+
+#define NSApplicationActivationPolicyRegular 0
+
+#define WKUserScriptInjectionTimeAtDocumentStart 0
+
+namespace webview {
+
+// Helpers to avoid too much typing
+id operator"" _cls(const char *s, std::size_t) { return (id)objc_getClass(s); }
+SEL operator"" _sel(const char *s, std::size_t) { return sel_registerName(s); }
+id operator"" _str(const char *s, std::size_t) {
+ return objc_msgSend("NSString"_cls, "stringWithUTF8String:"_sel, s);
+}
+
+class cocoa_wkwebview_engine {
+public:
+ cocoa_wkwebview_engine(bool debug, void *window) {
+ // Application
+ id app = objc_msgSend("NSApplication"_cls, "sharedApplication"_sel);
+ objc_msgSend(app, "setActivationPolicy:"_sel,
+ NSApplicationActivationPolicyRegular);
+
+ // Delegate
+ auto cls = objc_allocateClassPair((Class) "NSResponder"_cls, "AppDelegate", 0);
+ class_addProtocol(cls, objc_getProtocol("NSTouchBarProvider"));
+ class_addMethod(cls, "applicationShouldTerminateAfterLastWindowClosed:"_sel,
+ (IMP)(+[](id, SEL, id) -> BOOL { return 1; }), "c@:@");
+ class_addMethod(cls, "userContentController:didReceiveScriptMessage:"_sel,
+ (IMP)(+[](id self, SEL, id, id msg) {
+ auto w =
+ (cocoa_wkwebview_engine *)objc_getAssociatedObject(
+ self, "webview");
+ w->on_message((const char *)objc_msgSend(
+ objc_msgSend(msg, "body"_sel), "UTF8String"_sel));
+ }),
+ "v@:@@");
+ objc_registerClassPair(cls);
+
+ auto delegate = objc_msgSend((id)cls, "new"_sel);
+ objc_setAssociatedObject(delegate, "webview", (id)this,
+ OBJC_ASSOCIATION_ASSIGN);
+ objc_msgSend(app, sel_registerName("setDelegate:"), delegate);
+
+ // Main window
+ if (window == nullptr) {
+ m_window = objc_msgSend("NSWindow"_cls, "alloc"_sel);
+ m_window = objc_msgSend(
+ m_window, "initWithContentRect:styleMask:backing:defer:"_sel,
+ CGRectMake(0, 0, 0, 0), 0, NSBackingStoreBuffered, 0);
+ } else {
+ m_window = (id)window;
+ }
+
+ // Webview
+ auto config = objc_msgSend("WKWebViewConfiguration"_cls, "new"_sel);
+ m_manager = objc_msgSend(config, "userContentController"_sel);
+ m_webview = objc_msgSend("WKWebView"_cls, "alloc"_sel);
+ if (debug) {
+ objc_msgSend(objc_msgSend(config, "preferences"_sel),
+ "setValue:forKey:"_sel,
+ objc_msgSend("NSNumber"_cls, "numberWithBool:"_sel, 1),
+ "developerExtrasEnabled"_str);
+ }
+ objc_msgSend(m_webview, "initWithFrame:configuration:"_sel,
+ CGRectMake(0, 0, 0, 0), config);
+ objc_msgSend(m_manager, "addScriptMessageHandler:name:"_sel, delegate,
+ "external"_str);
+ init(R"script(
+ window.external = {
+ invoke: function(s) {
+ window.webkit.messageHandlers.external.postMessage(s);
+ },
+ };
+ )script");
+ objc_msgSend(m_window, "setContentView:"_sel, m_webview);
+ objc_msgSend(m_window, "makeKeyAndOrderFront:"_sel, nullptr);
+ }
+ ~cocoa_wkwebview_engine() { close(); }
+ void *window() { return (void *)m_window; }
+ void terminate() {
+ close();
+ objc_msgSend("NSApp"_cls, "terminate:"_sel, nullptr);
+ }
+ void run() {
+ id app = objc_msgSend("NSApplication"_cls, "sharedApplication"_sel);
+ dispatch([&]() { objc_msgSend(app, "activateIgnoringOtherApps:"_sel, 1); });
+ objc_msgSend(app, "run"_sel);
+ }
+ void dispatch(std::function f) {
+ dispatch_async_f(dispatch_get_main_queue(), new dispatch_fn_t(f),
+ (dispatch_function_t)([](void *arg) {
+ auto f = static_cast(arg);
+ (*f)();
+ delete f;
+ }));
+ }
+ void set_title(const std::string title) {
+ objc_msgSend(m_window, "setTitle:"_sel,
+ objc_msgSend("NSString"_cls, "stringWithUTF8String:"_sel,
+ title.c_str()));
+ }
+ void set_size(int width, int height, int hints) {
+ auto style = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable |
+ NSWindowStyleMaskMiniaturizable;
+ if (hints != WEBVIEW_HINT_FIXED) {
+ style = style | NSWindowStyleMaskResizable;
+ }
+ objc_msgSend(m_window, "setStyleMask:"_sel, style);
+
+ struct {
+ CGFloat width;
+ CGFloat height;
+ } size;
+ if (hints == WEBVIEW_HINT_MIN) {
+ size.width = width;
+ size.height = height;
+ objc_msgSend(m_window, "setContentMinSize:"_sel, size);
+ } else if (hints == WEBVIEW_HINT_MAX) {
+ size.width = width;
+ size.height = height;
+ objc_msgSend(m_window, "setContentMaxSize:"_sel, size);
+ } else {
+ objc_msgSend(m_window, "setFrame:display:animate:"_sel,
+ CGRectMake(0, 0, width, height), 1, 0);
+ }
+ }
+ void navigate(const std::string url) {
+ auto nsurl = objc_msgSend(
+ "NSURL"_cls, "URLWithString:"_sel,
+ objc_msgSend("NSString"_cls, "stringWithUTF8String:"_sel, url.c_str()));
+ objc_msgSend(
+ m_webview, "loadRequest:"_sel,
+ objc_msgSend("NSURLRequest"_cls, "requestWithURL:"_sel, nsurl));
+ }
+ void init(const std::string js) {
+ objc_msgSend(
+ m_manager, "addUserScript:"_sel,
+ objc_msgSend(objc_msgSend("WKUserScript"_cls, "alloc"_sel),
+ "initWithSource:injectionTime:forMainFrameOnly:"_sel,
+ objc_msgSend("NSString"_cls, "stringWithUTF8String:"_sel,
+ js.c_str()),
+ WKUserScriptInjectionTimeAtDocumentStart, 1));
+ }
+ void eval(const std::string js) {
+ objc_msgSend(
+ m_webview, "evaluateJavaScript:completionHandler:"_sel,
+ objc_msgSend("NSString"_cls, "stringWithUTF8String:"_sel, js.c_str()),
+ nullptr);
+ }
+
+private:
+ virtual void on_message(const std::string msg) = 0;
+ void close() { objc_msgSend(m_window, "close"_sel); }
+ id m_window;
+ id m_webview;
+ id m_manager;
+};
+
+using browser_engine = cocoa_wkwebview_engine;
+
+} // namespace webview
+
+#elif defined(WEBVIEW_EDGE)
+
+//
+// ====================================================================
+//
+// This implementation uses Win32 API to create a native window. It can
+// use either EdgeHTML or Edge/Chromium backend as a browser engine.
+//
+// ====================================================================
+//
+
+#define WIN32_LEAN_AND_MEAN
+#include
+
+#pragma comment(lib, "user32.lib")
+
+// EdgeHTML headers and libs
+#include
+#include
+#include
+#pragma comment(lib, "windowsapp")
+
+// Edge/Chromium headers and libs
+#include "webview2.h"
+#pragma comment(lib, "ole32.lib")
+#pragma comment(lib, "oleaut32.lib")
+
+namespace webview {
+
+using msg_cb_t = std::function;
+
+// Common interface for EdgeHTML and Edge/Chromium
+class browser {
+public:
+ virtual ~browser() = default;
+ virtual bool embed(HWND, bool, msg_cb_t) = 0;
+ virtual void navigate(const std::string url) = 0;
+ virtual void eval(const std::string js) = 0;
+ virtual void init(const std::string js) = 0;
+ virtual void resize(HWND) = 0;
+};
+
+//
+// EdgeHTML browser engine
+//
+using namespace winrt;
+using namespace Windows::Foundation;
+using namespace Windows::Web::UI;
+using namespace Windows::Web::UI::Interop;
+
+class edge_html : public browser {
+public:
+ bool embed(HWND wnd, bool debug, msg_cb_t cb) override {
+ init_apartment(winrt::apartment_type::single_threaded);
+ auto process = WebViewControlProcess();
+ auto op = process.CreateWebViewControlAsync(reinterpret_cast(wnd),
+ Rect());
+ if (op.Status() != AsyncStatus::Completed) {
+ handle h(CreateEvent(nullptr, false, false, nullptr));
+ op.Completed([h = h.get()](auto, auto) { SetEvent(h); });
+ HANDLE hs[] = {h.get()};
+ DWORD i;
+ CoWaitForMultipleHandles(COWAIT_DISPATCH_WINDOW_MESSAGES |
+ COWAIT_DISPATCH_CALLS |
+ COWAIT_INPUTAVAILABLE,
+ INFINITE, 1, hs, &i);
+ }
+ m_webview = op.GetResults();
+ m_webview.Settings().IsScriptNotifyAllowed(true);
+ m_webview.IsVisible(true);
+ m_webview.ScriptNotify([=](auto const &sender, auto const &args) {
+ std::string s = winrt::to_string(args.Value());
+ cb(s.c_str());
+ });
+ m_webview.NavigationStarting([=](auto const &sender, auto const &args) {
+ m_webview.AddInitializeScript(winrt::to_hstring(init_js));
+ });
+ init("window.external.invoke = s => window.external.notify(s)");
+ return true;
+ }
+
+ void navigate(const std::string url) override {
+ std::string html = html_from_uri(url);
+ if (html != "") {
+ m_webview.NavigateToString(winrt::to_hstring(html));
+ } else {
+ Uri uri(winrt::to_hstring(url));
+ m_webview.Navigate(uri);
+ }
+ }
+
+ void init(const std::string js) override {
+ init_js = init_js + "(function(){" + js + "})();";
+ }
+
+ void eval(const std::string js) override {
+ m_webview.InvokeScriptAsync(
+ L"eval", single_threaded_vector({winrt::to_hstring(js)}));
+ }
+
+ void resize(HWND wnd) override {
+ if (m_webview == nullptr) {
+ return;
+ }
+ RECT r;
+ GetClientRect(wnd, &r);
+ Rect bounds(r.left, r.top, r.right - r.left, r.bottom - r.top);
+ m_webview.Bounds(bounds);
+ }
+
+private:
+ WebViewControl m_webview = nullptr;
+ std::string init_js = "";
+};
+
+//
+// Edge/Chromium browser engine
+//
+class edge_chromium : public browser {
+public:
+ bool embed(HWND wnd, bool debug, msg_cb_t cb) override {
+ CoInitializeEx(nullptr, 0);
+ std::atomic_flag flag = ATOMIC_FLAG_INIT;
+ flag.test_and_set();
+ HRESULT res = CreateWebView2EnvironmentWithDetails(
+ nullptr, nullptr, nullptr,
+ new webview2_com_handler(wnd, [&](IWebView2WebView *webview) {
+ m_webview = webview;
+ flag.clear();
+ }));
+ if (res != S_OK) {
+ CoUninitialize();
+ return false;
+ }
+ MSG msg = {};
+ while (flag.test_and_set() && GetMessage(&msg, NULL, 0, 0)) {
+ TranslateMessage(&msg);
+ DispatchMessage(&msg);
+ }
+ init("window.external={invoke:s=>window.chrome.webview.postMessage(s)}");
+ return true;
+ }
+
+ void resize(HWND wnd) override {
+ if (m_webview == nullptr) {
+ return;
+ }
+ RECT bounds;
+ GetClientRect(wnd, &bounds);
+ m_webview->put_Bounds(bounds);
+ }
+
+ void navigate(const std::string url) override {
+ auto wurl = to_lpwstr(url);
+ m_webview->Navigate(wurl);
+ delete[] wurl;
+ }
+
+ void init(const std::string js) override {
+ LPCWSTR wjs = to_lpwstr(js);
+ m_webview->AddScriptToExecuteOnDocumentCreated(wjs, nullptr);
+ delete[] wjs;
+ }
+
+ void eval(const std::string js) override {
+ LPCWSTR wjs = to_lpwstr(js);
+ m_webview->ExecuteScript(wjs, nullptr);
+ delete[] wjs;
+ }
+
+private:
+ LPWSTR to_lpwstr(const std::string s) {
+ int n = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), -1, NULL, 0);
+ wchar_t *ws = new wchar_t[n];
+ MultiByteToWideChar(CP_UTF8, 0, s.c_str(), -1, ws, n);
+ return ws;
+ }
+
+ IWebView2WebView *m_webview = nullptr;
+
+ class webview2_com_handler
+ : public IWebView2CreateWebView2EnvironmentCompletedHandler,
+ public IWebView2CreateWebViewCompletedHandler {
+ using webview2_com_handler_cb_t = std::function;
+
+ public:
+ webview2_com_handler(HWND hwnd, webview2_com_handler_cb_t cb)
+ : m_window(hwnd), m_cb(cb) {}
+ ULONG STDMETHODCALLTYPE AddRef() { return 1; }
+ ULONG STDMETHODCALLTYPE Release() { return 1; }
+ HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, LPVOID *ppv) {
+ return S_OK;
+ }
+ HRESULT STDMETHODCALLTYPE Invoke(HRESULT res, IWebView2Environment *env) {
+ env->CreateWebView(m_window, this);
+ return S_OK;
+ }
+ HRESULT STDMETHODCALLTYPE Invoke(HRESULT res, IWebView2WebView *webview) {
+ webview->AddRef();
+ m_cb(webview);
+ return S_OK;
+ }
+
+ private:
+ HWND m_window;
+ webview2_com_handler_cb_t m_cb;
+ };
+};
+
+class win32_edge_engine {
+public:
+ win32_edge_engine(bool debug, void *window) {
+ if (window == nullptr) {
+ WNDCLASSEX wc;
+ ZeroMemory(&wc, sizeof(WNDCLASSEX));
+ wc.cbSize = sizeof(WNDCLASSEX);
+ wc.hInstance = GetModuleHandle(nullptr);
+ wc.lpszClassName = "webview";
+ wc.lpfnWndProc =
+ (WNDPROC)(+[](HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) -> int {
+ auto w = (win32_edge_engine *)GetWindowLongPtr(hwnd, GWLP_USERDATA);
+ switch (msg) {
+ case WM_SIZE:
+ w->m_browser->resize(hwnd);
+ break;
+ case WM_CLOSE:
+ DestroyWindow(hwnd);
+ break;
+ case WM_DESTROY:
+ w->terminate();
+ break;
+ case WM_GETMINMAXINFO: {
+ auto lpmmi = (LPMINMAXINFO)lp;
+ if (w == nullptr) {
+ return 0;
+ }
+ if (w->m_maxsz.x > 0 && w->m_maxsz.y > 0) {
+ lpmmi->ptMaxSize = w->m_maxsz;
+ lpmmi->ptMaxTrackSize = w->m_maxsz;
+ }
+ if (w->m_minsz.x > 0 && w->m_minsz.y > 0) {
+ lpmmi->ptMinTrackSize = w->m_minsz;
+ }
+ } break;
+ default:
+ return DefWindowProc(hwnd, msg, wp, lp);
+ }
+ return 0;
+ });
+ RegisterClassEx(&wc);
+ m_window = CreateWindow("webview", "", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT,
+ CW_USEDEFAULT, 640, 480, nullptr, nullptr,
+ GetModuleHandle(nullptr), nullptr);
+ SetWindowLongPtr(m_window, GWLP_USERDATA, (LONG_PTR)this);
+ } else {
+ m_window = *(static_cast(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);
+
+ if (!m_browser->embed(m_window, debug, cb)) {
+ m_browser = std::make_unique();
+ m_browser->embed(m_window, debug, cb);
+ }
+
+ m_browser->resize(m_window);
+ }
+
+ void run() {
+ MSG msg;
+ BOOL res;
+ while ((res = GetMessage(&msg, nullptr, 0, 0)) != -1) {
+ if (msg.hwnd) {
+ TranslateMessage(&msg);
+ DispatchMessage(&msg);
+ continue;
+ }
+ if (msg.message == WM_APP) {
+ auto f = (dispatch_fn_t *)(msg.lParam);
+ (*f)();
+ delete f;
+ } else if (msg.message == WM_QUIT) {
+ return;
+ }
+ }
+ }
+ void *window() { return (void *)m_window; }
+ void terminate() { PostQuitMessage(0); }
+ void dispatch(dispatch_fn_t f) {
+ PostThreadMessage(m_main_thread, WM_APP, 0, (LPARAM) new dispatch_fn_t(f));
+ }
+
+ void set_title(const std::string title) {
+ SetWindowText(m_window, title.c_str());
+ }
+
+ void set_size(int width, int height, int hints) {
+ 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 {
+ RECT r;
+ r.left = r.top = 0;
+ r.right = width;
+ r.bottom = height;
+ AdjustWindowRect(&r, WS_OVERLAPPEDWINDOW, 0);
+ SetWindowPos(
+ m_window, NULL, r.left, r.top, r.right - r.left, r.bottom - r.top,
+ SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOMOVE | SWP_FRAMECHANGED);
+ m_browser->resize(m_window);
+ }
+ }
+
+ void navigate(const std::string url) { m_browser->navigate(url); }
+ void eval(const std::string js) { m_browser->eval(js); }
+ void init(const std::string js) { m_browser->init(js); }
+
+private:
+ virtual void on_message(const std::string msg) = 0;
+
+ HWND m_window;
+ POINT m_minsz = POINT { 0, 0 };
+ POINT m_maxsz = POINT { 0, 0 };
+ DWORD m_main_thread = GetCurrentThreadId();
+ std::unique_ptr m_browser =
+ std::make_unique();
+};
+
+using browser_engine = win32_edge_engine;
+} // namespace webview
+
+#endif /* WEBVIEW_GTK, WEBVIEW_COCOA, WEBVIEW_EDGE */
+
+namespace webview {
+
+class webview : public browser_engine {
+public:
+ webview(bool debug = false, void *wnd = nullptr)
+ : browser_engine(debug, wnd) {}
+
+ void navigate(const std::string url) {
+ if (url == "") {
+ browser_engine::navigate("data:text/html," +
+ url_encode("Hello"));
+ return;
+ }
+ std::string html = html_from_uri(url);
+ if (html != "") {
+ browser_engine::navigate("data:text/html," + url_encode(html));
+ } else {
+ browser_engine::navigate(url);
+ }
+ }
+
+ using binding_t = std::function;
+ using binding_ctx_t = std::pair;
+
+ using sync_binding_t = std::function;
+ using sync_binding_ctx_t = std::pair;
+
+ void bind(const std::string name, sync_binding_t fn) {
+ bind(name,
+ [](std::string seq, std::string req, void *arg) {
+ auto pair = static_cast(arg);
+ pair->first->resolve(seq, 0, pair->second(req));
+ },
+ new sync_binding_ctx_t(this, fn));
+ }
+
+ void bind(const std::string name, binding_t f, void *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);
+ bindings[name] = new binding_ctx_t(new binding_t(f), arg);
+ }
+
+ void resolve(const std::string seq, int status, const std::string result) {
+ dispatch([=]() {
+ if (status == 0) {
+ eval("window._rpc[" + seq + "].resolve(" + result + "); window._rpc[" +
+ seq + "] = undefined");
+ } else {
+ eval("window._rpc[" + seq + "].reject(" + result + "); window._rpc[" +
+ seq + "] = undefined");
+ }
+ });
+ }
+
+private:
+ 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);
+ if (bindings.find(name) == bindings.end()) {
+ return;
+ }
+ auto fn = bindings[name];
+ (*fn->first)(seq, args, fn->second);
+ }
+ std::map bindings;
+};
+} // namespace webview
+
+WEBVIEW_API webview_t webview_create(int debug, void *wnd) {
+ return new webview::webview(debug, wnd);
+}
+
+WEBVIEW_API void webview_destroy(webview_t w) {
+ delete static_cast(w);
+}
+
+WEBVIEW_API void webview_run(webview_t w) {
+ static_cast(w)->run();
+}
+
+WEBVIEW_API void webview_terminate(webview_t w) {
+ static_cast(w)->terminate();
+}
+
+WEBVIEW_API void webview_dispatch(webview_t w, void (*fn)(webview_t, void *),
+ void *arg) {
+ static_cast(w)->dispatch([=]() { fn(w, arg); });
+}
+
+WEBVIEW_API void *webview_get_window(webview_t w) {
+ return static_cast(w)->window();
+}
+
+WEBVIEW_API void webview_set_title(webview_t w, const char *title) {
+ static_cast(w)->set_title(title);
+}
+
+WEBVIEW_API void webview_set_size(webview_t w, int width, int height,
+ int hints) {
+ static_cast(w)->set_size(width, height, hints);
+}
+
+WEBVIEW_API void webview_navigate(webview_t w, const char *url) {
+ static_cast(w)->navigate(url);
+}
+
+WEBVIEW_API void webview_init(webview_t w, const char *js) {
+ static_cast(w)->init(js);
+}
+
+WEBVIEW_API void webview_eval(webview_t w, const char *js) {
+ static_cast(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(w)->bind(
+ name,
+ [=](std::string seq, std::string req, void *arg) {
+ fn(seq.c_str(), req.c_str(), arg);
+ },
+ arg);
+}
+
+WEBVIEW_API void webview_return(webview_t w, const char *seq, int status,
+ const char *result) {
+ static_cast(w)->resolve(seq, status, result);
+}
+
+#endif /* WEBVIEW_HEADER */
+
+#endif /* WEBVIEW_H */
+
diff --git a/shard.yml b/shard.yml
new file mode 100644
index 0000000..8a7d65d
--- /dev/null
+++ b/shard.yml
@@ -0,0 +1,12 @@
+name: webview
+version: 0.1.0
+
+authors:
+ - Ali Naqvi
+
+description: |
+ Crystal bindings to the zserge's tiny cross-platform Webview library.
+
+crystal: 0.34.0
+
+license: MIT
diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr
new file mode 100644
index 0000000..3d8b3df
--- /dev/null
+++ b/spec/spec_helper.cr
@@ -0,0 +1,2 @@
+require "spec"
+require "../src/webview"
diff --git a/spec/webview_spec.cr b/spec/webview_spec.cr
new file mode 100644
index 0000000..a2b6360
--- /dev/null
+++ b/spec/webview_spec.cr
@@ -0,0 +1,9 @@
+require "./spec_helper"
+
+describe Webview do
+ # TODO: Write tests
+
+ it "works" do
+ false.should eq(true)
+ end
+end
diff --git a/src/lib.cr b/src/lib.cr
new file mode 100644
index 0000000..ee4aec8
--- /dev/null
+++ b/src/lib.cr
@@ -0,0 +1,69 @@
+module Webview
+ {% if flag?(:darwin) %}
+ @[Link(ldflags: "-L#{__DIR__}/../ext/darwin -lwebview.o -lc++")]
+ @[Link(framework: "WebKit")]
+ {% elsif flag?(:linux) %}
+ @[Link(ldflags: "-L#{__DIR__}/../ext/linux -lwebview.o -lc++")]
+ @[Link(ldflags: "`command -v pkg-config > /dev/null && pkg-config --cflags --libs gtk+-3.0 webkit2gtk-4.0`")]
+ {% elsif flag?(:windows) %}
+ @[Link(ldflags: "-L#{__DIR__}/../ext/windows -lwebview.o -lc++")]
+ # Windows requires special linker flags for GUI apps.
+ @[Link(ldflags: "-lole32 -lcomctl32 -loleaut32 -luuid -lgdi32 -H windowsgui")]
+ {% else %}
+ raise "Platform not supported"
+ {% end %}
+ lib LibWebView
+ alias T = Void*
+
+ # Creates a new webview instance. If debug is non-zero - developer tools will
+ # be enabled (if the platform supports them). Window parameter can be a
+ # pointer to the native window handle. If it's non-null - then child WebView
+ # is embedded into the given parent window. Otherwise a new window is created.
+ # Depending on the platform, a GtkWindow, NSWindow or HWND pointer can be
+ # passed here.
+ fun create = webview_create(debug : LibC::Int, window : Void*) : T
+ # Destroys a webview and closes the native window.
+ fun destroy = webview_destroy(w : T)
+ # Runs the main loop until it's terminated. After this function exits - you
+ # must destroy the webview.
+ fun run = webview_run(w : T)
+ # Stops the main loop. It is safe to call this function from another other
+ # background thread.
+ fun terminate = webview_terminate(w : T)
+ # Posts a function to be executed on the main thread. You normally do not need
+ # to call this function, unless you want to tweak the native window.
+ fun dispatch = webview_dispatch(w : T, fn : (T, Void* -> Void), arg : Void*)
+ # Returns a native window handle pointer. When using GTK backend the pointer
+ # is GtkWindow pointer, when using Cocoa backend the pointer is NSWindow
+ # pointer, when using Win32 backend the pointer is HWND pointer.
+ fun get_window = webview_get_window(w : T) : Void*
+ # Updates the title of the native window. Must be called from the UI thread.
+ fun set_title = webview_set_title(w : T, title : LibC::Char*)
+ # Updates native window size. See WEBVIEW_HINT constants.
+ fun set_size = webview_set_size(w : T, width : LibC::Int, height : LibC::Int, hints : LibC::Int)
+ # Navigates webview to the given URL. URL may be a data URI, i.e.
+ # "data:text/text,...". It is often ok not to url-encode it
+ # properly, webview will re-encode it for you.
+ fun navigate = webview_navigate(w : T, url : LibC::Char*)
+
+ # Injects JavaScript code at the initialization of the new page. Every time
+ # the webview will open a the new page - this initialization code will be
+ # executed. It is guaranteed that code is executed before window.onload.
+ fun init = webview_init(w : T, js : LibC::Char*)
+ # Evaluates arbitrary JavaScript code. Evaluation happens asynchronously, also
+ # the result of the expression is ignored. Use RPC bindings if you want to
+ # receive notifications about the results of the evaluation.
+ fun eval = webview_eval(w : T, js : LibC::Char*)
+ # Binds a native C callback so that it will appear under the given name as a
+ # global JavaScript function. Internally it uses webview_init(). Callback
+ # receives a request string and a user-provided argument pointer. Request
+ # string is a JSON array of all the arguments passed to the JavaScript
+ # function.
+ fun bind = webview_bind(w : T, name : LibC::Char*, fn : (LibC::Char*, LibC::Char*, Void* -> Void), arg : Void*)
+ # Allows to return a value from the native binding. Original request pointer
+ # must be provided to help internal RPC engine match requests with responses.
+ # If status is zero - result is expected to be a valid JSON result value.
+ # If status is not zero - result is an error JSON object.
+ fun webview_return(w : T, seq : LibC::Char*, status : LibC::Int, result : LibC::Char*)
+ end
+end
diff --git a/src/webview.cr b/src/webview.cr
new file mode 100644
index 0000000..ce52f77
--- /dev/null
+++ b/src/webview.cr
@@ -0,0 +1,119 @@
+require "json"
+
+# Crystal bindings for [zserge's Webview](https://github.com/zserge/webview) which is an excellent cross-platform single header webview library for C/C++ using Gtk, Cocoa or MSHTML repectively.
+module Webview
+ VERSION = "0.1.0"
+
+ # Window size hints
+ enum SizeHints
+ NONE = 0 # Width and height are default size
+ MIN = 1 # Width and height are minimum bounds
+ MAX = 2 # Width and height are maximum bounds
+ FIXED = 3 # Window size can not be changed by user
+ end
+ alias JSProc = Array(JSON::Any) -> JSON::Any
+
+ class Webview
+ private record BindContext, w : LibWebView::T, cb : JSProc
+
+ @@dispatchs = Hash(Proc(Nil), Pointer(Void)?).new
+ @@bindings = Hash(JSProc, Pointer(Void)?).new
+
+ def initialize(debug, title)
+ @w = LibWebView.create(debug ? 1 : 0, nil)
+ LibWebView.set_title(@w, title)
+ end
+
+ # destroys a WebView and closes the native window.
+ def destroy
+ LibWebView.destroy(@w)
+ end
+
+ # Terminate stops the main loop. It is safe to call this function from
+ # a background thread.
+ def terminate
+ LibWebView.terminate(@w)
+ end
+
+ # runs the main loop until it's terminated. After this function exists
+ # you must destroy the WebView
+ def run
+ LibWebView.run(@w)
+ end
+
+ # returns a native window handle pointer. When using GTK backend the
+ # pointer is GtkWindow pointer, when using Cocoa backend the pointer is
+ # NSWindow pointer, when using Win32 backend the pointer is HWND pointer.
+ def window
+ LibWebView.get_window(@w)
+ end
+
+ def title=(val)
+ LibWebView.set_title(@w, val)
+ end
+
+ def size(width, height, hint : SizeHints)
+ LibWebView.set_size(@w, width, height, hint.value)
+ end
+
+ # navigates WebView to the given URL. URL may be a data URI, i.e.
+ # "data:text/text,..". It is often ok not to url-encode it
+ # properlty, WebView will re-encode it for you.
+ def navigate(url)
+ LibWebView.navigate(@w, url)
+ end
+
+ # posts a function to be executed on the main thread. You normally do no need
+ # to call this function, unless you want to tweak the native window.
+ def dispatch(&f : ->)
+ boxed = Box.box(f)
+ @@dispatchs[f] = boxed
+
+ LibWebView.dispatch(@w, ->(_w, data) {
+ cb = Box(typeof(f)).unbox(data)
+ cb.call
+ @@dispatchs.delete(cb)
+ }, boxed)
+ end
+
+ # injects Javascript code at the initialization of the new page. Every
+ # time the WebView will open the new page - this initialization code will
+ # be executed. It is guaranteed that code is executed before window.onload.
+ def init(js : String)
+ LibWebView.init(@w, js)
+ end
+
+ # evaluates arbitrary Javascript code. Evaluation happens asynchronously,
+ # also the result of the expression is ignored. Use RPC bindings if you want
+ # to receive notifications about the result of the evaluation.
+ def eval(js : String)
+ LibWebView.init(@w, js)
+ end
+
+ # binds a callback function so that it will appear under the given name
+ # as a global Javascript function.
+ def bind(name : String, fn : JSProc)
+ ctx = BindContext.new(@w, fn)
+ boxed = Box.box(ctx)
+ @@bindings[fn] = boxed
+
+ LibWebView.bind(@w, name, ->(id, req, data) {
+ raw = JSON.parse(String.new(req))
+ cb_ctx = Box(BindContext).unbox(data)
+ res = cb_ctx.cb.call(raw.as_a)
+ @@bindings.delete(cb_ctx.cb)
+ LibWebView.webview_return(cb_ctx.w, id, 0, res.to_s)
+ }, boxed)
+ end
+ end
+
+ def self.window(width, height, hint, title, url, debug = false)
+ wv = Webview.new(debug, title)
+ wv.size(width, height, hint)
+ wv.title = title
+ wv.navigate(url)
+ wv
+ end
+end
+
+require "./*"