mirror of
https://github.com/taigrr/wails.git
synced 2026-04-07 07:32:48 -07:00
523 lines
16 KiB
C
523 lines
16 KiB
C
/*
|
|
* 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
|
|
|
|
#ifdef __cplusplus
|
|
extern "C"
|
|
{
|
|
#endif
|
|
|
|
#ifdef WEBVIEW_STATIC
|
|
#define WEBVIEW_API static
|
|
#else
|
|
#define WEBVIEW_API extern
|
|
#endif
|
|
|
|
#include <stdint.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include <JavaScriptCore/JavaScript.h>
|
|
#include <gtk/gtk.h>
|
|
#include <webkit2/webkit2.h>
|
|
|
|
struct webview_priv
|
|
{
|
|
GtkWidget *window;
|
|
GtkWidget *scroller;
|
|
GtkWidget *webview;
|
|
GtkWidget *inspector_window;
|
|
GAsyncQueue *queue;
|
|
int ready;
|
|
int js_busy;
|
|
int should_exit;
|
|
};
|
|
|
|
struct webview;
|
|
|
|
typedef void (*webview_external_invoke_cb_t)(struct webview *w,
|
|
const char *arg);
|
|
|
|
struct webview
|
|
{
|
|
const char *url;
|
|
const char *title;
|
|
int width;
|
|
int height;
|
|
int resizable;
|
|
int transparentTitlebar;
|
|
int debug;
|
|
webview_external_invoke_cb_t external_invoke_cb;
|
|
struct webview_priv priv;
|
|
void *userdata;
|
|
};
|
|
|
|
enum webview_dialog_type
|
|
{
|
|
WEBVIEW_DIALOG_TYPE_OPEN = 0,
|
|
WEBVIEW_DIALOG_TYPE_SAVE = 1,
|
|
WEBVIEW_DIALOG_TYPE_ALERT = 2
|
|
};
|
|
|
|
#define WEBVIEW_DIALOG_FLAG_FILE (0 << 0)
|
|
#define WEBVIEW_DIALOG_FLAG_DIRECTORY (1 << 0)
|
|
|
|
#define WEBVIEW_DIALOG_FLAG_INFO (1 << 1)
|
|
#define WEBVIEW_DIALOG_FLAG_WARNING (2 << 1)
|
|
#define WEBVIEW_DIALOG_FLAG_ERROR (3 << 1)
|
|
#define WEBVIEW_DIALOG_FLAG_ALERT_MASK (3 << 1)
|
|
|
|
typedef void (*webview_dispatch_fn)(struct webview *w, void *arg);
|
|
|
|
struct webview_dispatch_arg
|
|
{
|
|
webview_dispatch_fn fn;
|
|
struct webview *w;
|
|
void *arg;
|
|
};
|
|
|
|
#define DEFAULT_URL \
|
|
"data:text/" \
|
|
"html,%3C%21DOCTYPE%20html%3E%0A%3Chtml%20lang=%22en%22%3E%0A%3Chead%3E%" \
|
|
"3Cmeta%20charset=%22utf-8%22%3E%3Cmeta%20http-equiv=%22IE=edge%22%" \
|
|
"20content=%22IE=edge%22%3E%3C%2Fhead%3E%0A%3Cbody%3E%3Cdiv%20id=%22app%22%" \
|
|
"3E%3C%2Fdiv%3E%3Cscript%20type=%22text%2Fjavascript%22%3E%3C%2Fscript%3E%" \
|
|
"3C%2Fbody%3E%0A%3C%2Fhtml%3E"
|
|
|
|
#define CSS_INJECT_FUNCTION \
|
|
"(function(e){var " \
|
|
"t=document.createElement('style'),d=document.head||document." \
|
|
"getElementsByTagName('head')[0];t.setAttribute('type','text/" \
|
|
"css'),t.styleSheet?t.styleSheet.cssText=e:t.appendChild(document." \
|
|
"createTextNode(e)),d.appendChild(t)})"
|
|
|
|
static const char *webview_check_url(const char *url)
|
|
{
|
|
if (url == NULL || strlen(url) == 0)
|
|
{
|
|
return DEFAULT_URL;
|
|
}
|
|
return url;
|
|
}
|
|
|
|
WEBVIEW_API int webview(const char *title, const char *url, int width,
|
|
int height, int resizable);
|
|
|
|
WEBVIEW_API int webview_init(struct webview *w);
|
|
WEBVIEW_API int webview_loop(struct webview *w, int blocking);
|
|
WEBVIEW_API int webview_eval(struct webview *w, const char *js);
|
|
WEBVIEW_API int webview_inject_css(struct webview *w, const char *css);
|
|
WEBVIEW_API void webview_set_title(struct webview *w, const char *title);
|
|
WEBVIEW_API void webview_set_fullscreen(struct webview *w, int fullscreen);
|
|
WEBVIEW_API void webview_set_color(struct webview *w, uint8_t r, uint8_t g,
|
|
uint8_t b, uint8_t a);
|
|
WEBVIEW_API void webview_dialog(struct webview *w,
|
|
enum webview_dialog_type dlgtype, int flags,
|
|
const char *title, const char *arg,
|
|
char *result, size_t resultsz, char *filter);
|
|
WEBVIEW_API void webview_dispatch(struct webview *w, webview_dispatch_fn fn,
|
|
void *arg);
|
|
WEBVIEW_API void webview_terminate(struct webview *w);
|
|
WEBVIEW_API void webview_exit(struct webview *w);
|
|
WEBVIEW_API void webview_debug(const char *format, ...);
|
|
WEBVIEW_API void webview_print_log(const char *s);
|
|
|
|
WEBVIEW_API int webview(const char *title, const char *url, int width,
|
|
int height, int resizable)
|
|
{
|
|
struct webview webview;
|
|
memset(&webview, 0, sizeof(webview));
|
|
webview.title = title;
|
|
webview.url = url;
|
|
webview.width = width;
|
|
webview.height = height;
|
|
webview.resizable = resizable;
|
|
int r = webview_init(&webview);
|
|
if (r != 0)
|
|
{
|
|
return r;
|
|
}
|
|
while (webview_loop(&webview, 1) == 0)
|
|
{
|
|
}
|
|
webview_exit(&webview);
|
|
return 0;
|
|
}
|
|
|
|
WEBVIEW_API void webview_debug(const char *format, ...)
|
|
{
|
|
char buf[4096];
|
|
va_list ap;
|
|
va_start(ap, format);
|
|
vsnprintf(buf, sizeof(buf), format, ap);
|
|
webview_print_log(buf);
|
|
va_end(ap);
|
|
}
|
|
|
|
static int webview_js_encode(const char *s, char *esc, size_t n)
|
|
{
|
|
int r = 1; /* At least one byte for trailing zero */
|
|
for (; *s; s++)
|
|
{
|
|
const unsigned char c = *s;
|
|
if (c >= 0x20 && c < 0x80 && strchr("<>\\'\"", c) == NULL)
|
|
{
|
|
if (n > 0)
|
|
{
|
|
*esc++ = c;
|
|
n--;
|
|
}
|
|
r++;
|
|
}
|
|
else
|
|
{
|
|
if (n > 0)
|
|
{
|
|
snprintf(esc, n, "\\x%02x", (int)c);
|
|
esc += 4;
|
|
n -= 4;
|
|
}
|
|
r += 4;
|
|
}
|
|
}
|
|
return r;
|
|
}
|
|
|
|
WEBVIEW_API int webview_inject_css(struct webview *w, const char *css)
|
|
{
|
|
int n = webview_js_encode(css, NULL, 0);
|
|
char *esc = (char *)calloc(1, sizeof(CSS_INJECT_FUNCTION) + n + 4);
|
|
if (esc == NULL)
|
|
{
|
|
return -1;
|
|
}
|
|
char *js = (char *)calloc(1, n);
|
|
webview_js_encode(css, js, n);
|
|
snprintf(esc, sizeof(CSS_INJECT_FUNCTION) + n + 4, "%s(\"%s\")",
|
|
CSS_INJECT_FUNCTION, js);
|
|
int r = webview_eval(w, esc);
|
|
free(js);
|
|
free(esc);
|
|
return r;
|
|
}
|
|
|
|
static void external_message_received_cb(WebKitUserContentManager *m,
|
|
WebKitJavascriptResult *r,
|
|
gpointer arg)
|
|
{
|
|
(void)m;
|
|
struct webview *w = (struct webview *)arg;
|
|
if (w->external_invoke_cb == NULL)
|
|
{
|
|
return;
|
|
}
|
|
JSGlobalContextRef context = webkit_javascript_result_get_global_context(r);
|
|
JSValueRef value = webkit_javascript_result_get_value(r);
|
|
JSStringRef js = JSValueToStringCopy(context, value, NULL);
|
|
size_t n = JSStringGetMaximumUTF8CStringSize(js);
|
|
char *s = g_new(char, n);
|
|
JSStringGetUTF8CString(js, s, n);
|
|
w->external_invoke_cb(w, s);
|
|
JSStringRelease(js);
|
|
g_free(s);
|
|
}
|
|
|
|
static void webview_load_changed_cb(WebKitWebView *webview,
|
|
WebKitLoadEvent event, gpointer arg)
|
|
{
|
|
(void)webview;
|
|
struct webview *w = (struct webview *)arg;
|
|
if (event == WEBKIT_LOAD_FINISHED)
|
|
{
|
|
w->priv.ready = 1;
|
|
}
|
|
}
|
|
|
|
static void webview_destroy_cb(GtkWidget *widget, gpointer arg)
|
|
{
|
|
(void)widget;
|
|
struct webview *w = (struct webview *)arg;
|
|
webview_terminate(w);
|
|
}
|
|
|
|
static gboolean webview_context_menu_cb(WebKitWebView *webview,
|
|
GtkWidget *default_menu,
|
|
WebKitHitTestResult *hit_test_result,
|
|
gboolean triggered_with_keyboard,
|
|
gpointer userdata)
|
|
{
|
|
(void)webview;
|
|
(void)default_menu;
|
|
(void)hit_test_result;
|
|
(void)triggered_with_keyboard;
|
|
(void)userdata;
|
|
return TRUE;
|
|
}
|
|
|
|
WEBVIEW_API int webview_init(struct webview *w)
|
|
{
|
|
if (gtk_init_check(0, NULL) == FALSE)
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
w->priv.ready = 0;
|
|
w->priv.should_exit = 0;
|
|
w->priv.queue = g_async_queue_new();
|
|
w->priv.window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
|
|
gtk_window_set_title(GTK_WINDOW(w->priv.window), w->title);
|
|
|
|
if (w->resizable)
|
|
{
|
|
gtk_window_set_default_size(GTK_WINDOW(w->priv.window), w->width,
|
|
w->height);
|
|
}
|
|
else
|
|
{
|
|
gtk_widget_set_size_request(w->priv.window, w->width, w->height);
|
|
}
|
|
gtk_window_set_resizable(GTK_WINDOW(w->priv.window), !!w->resizable);
|
|
gtk_window_set_position(GTK_WINDOW(w->priv.window), GTK_WIN_POS_CENTER);
|
|
|
|
w->priv.scroller = gtk_scrolled_window_new(NULL, NULL);
|
|
gtk_container_add(GTK_CONTAINER(w->priv.window), w->priv.scroller);
|
|
|
|
WebKitUserContentManager *m = webkit_user_content_manager_new();
|
|
webkit_user_content_manager_register_script_message_handler(m, "external");
|
|
g_signal_connect(m, "script-message-received::external",
|
|
G_CALLBACK(external_message_received_cb), w);
|
|
|
|
w->priv.webview = webkit_web_view_new_with_user_content_manager(m);
|
|
webkit_web_view_load_uri(WEBKIT_WEB_VIEW(w->priv.webview),
|
|
webview_check_url(w->url));
|
|
g_signal_connect(G_OBJECT(w->priv.webview), "load-changed",
|
|
G_CALLBACK(webview_load_changed_cb), w);
|
|
gtk_container_add(GTK_CONTAINER(w->priv.scroller), w->priv.webview);
|
|
|
|
if (w->debug)
|
|
{
|
|
WebKitSettings *settings =
|
|
webkit_web_view_get_settings(WEBKIT_WEB_VIEW(w->priv.webview));
|
|
webkit_settings_set_enable_write_console_messages_to_stdout(settings, true);
|
|
webkit_settings_set_enable_developer_extras(settings, true);
|
|
}
|
|
else
|
|
{
|
|
g_signal_connect(G_OBJECT(w->priv.webview), "context-menu",
|
|
G_CALLBACK(webview_context_menu_cb), w);
|
|
}
|
|
|
|
gtk_widget_show_all(w->priv.window);
|
|
|
|
webkit_web_view_run_javascript(
|
|
WEBKIT_WEB_VIEW(w->priv.webview),
|
|
"window.external={invoke:function(x){"
|
|
"window.webkit.messageHandlers.external.postMessage(x);}}",
|
|
NULL, NULL, NULL);
|
|
|
|
g_signal_connect(G_OBJECT(w->priv.window), "destroy",
|
|
G_CALLBACK(webview_destroy_cb), w);
|
|
return 0;
|
|
}
|
|
|
|
WEBVIEW_API int webview_loop(struct webview *w, int blocking)
|
|
{
|
|
gtk_main_iteration_do(blocking);
|
|
return w->priv.should_exit;
|
|
}
|
|
|
|
WEBVIEW_API void webview_set_title(struct webview *w, const char *title)
|
|
{
|
|
gtk_window_set_title(GTK_WINDOW(w->priv.window), title);
|
|
}
|
|
|
|
WEBVIEW_API void webview_set_fullscreen(struct webview *w, int fullscreen)
|
|
{
|
|
if (fullscreen)
|
|
{
|
|
gtk_window_fullscreen(GTK_WINDOW(w->priv.window));
|
|
}
|
|
else
|
|
{
|
|
gtk_window_unfullscreen(GTK_WINDOW(w->priv.window));
|
|
}
|
|
}
|
|
|
|
WEBVIEW_API void webview_set_color(struct webview *w, uint8_t r, uint8_t g,
|
|
uint8_t b, uint8_t a)
|
|
{
|
|
GdkRGBA color = {r / 255.0, g / 255.0, b / 255.0, a / 255.0};
|
|
webkit_web_view_set_background_color(WEBKIT_WEB_VIEW(w->priv.webview),
|
|
&color);
|
|
}
|
|
|
|
WEBVIEW_API void webview_dialog(struct webview *w,
|
|
enum webview_dialog_type dlgtype, int flags,
|
|
const char *title, const char *arg,
|
|
char *result, size_t resultsz, char *filter)
|
|
{
|
|
GtkWidget *dlg;
|
|
if (result != NULL)
|
|
{
|
|
result[0] = '\0';
|
|
}
|
|
if (dlgtype == WEBVIEW_DIALOG_TYPE_OPEN ||
|
|
dlgtype == WEBVIEW_DIALOG_TYPE_SAVE)
|
|
{
|
|
dlg = gtk_file_chooser_dialog_new(
|
|
title, GTK_WINDOW(w->priv.window),
|
|
(dlgtype == WEBVIEW_DIALOG_TYPE_OPEN
|
|
? (flags & WEBVIEW_DIALOG_FLAG_DIRECTORY
|
|
? GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER
|
|
: GTK_FILE_CHOOSER_ACTION_OPEN)
|
|
: GTK_FILE_CHOOSER_ACTION_SAVE),
|
|
"_Cancel", GTK_RESPONSE_CANCEL,
|
|
(dlgtype == WEBVIEW_DIALOG_TYPE_OPEN ? "_Open" : "_Save"),
|
|
GTK_RESPONSE_ACCEPT, NULL);
|
|
if (filter[0] != '\0') {
|
|
GtkFileFilter *file_filter = gtk_file_filter_new();
|
|
gchar **filters = g_strsplit(filter, ",", -1);
|
|
gint i;
|
|
for(i = 0; filters && filters[i]; i++) {
|
|
gtk_file_filter_add_pattern(file_filter, filters[i]);
|
|
}
|
|
gtk_file_filter_set_name(file_filter, filter);
|
|
gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dlg), file_filter);
|
|
g_strfreev(filters);
|
|
}
|
|
gtk_file_chooser_set_local_only(GTK_FILE_CHOOSER(dlg), FALSE);
|
|
gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(dlg), FALSE);
|
|
gtk_file_chooser_set_show_hidden(GTK_FILE_CHOOSER(dlg), TRUE);
|
|
gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(dlg), TRUE);
|
|
gtk_file_chooser_set_create_folders(GTK_FILE_CHOOSER(dlg), TRUE);
|
|
gint response = gtk_dialog_run(GTK_DIALOG(dlg));
|
|
if (response == GTK_RESPONSE_ACCEPT)
|
|
{
|
|
gchar *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dlg));
|
|
g_strlcpy(result, filename, resultsz);
|
|
g_free(filename);
|
|
}
|
|
gtk_widget_destroy(dlg);
|
|
}
|
|
else if (dlgtype == WEBVIEW_DIALOG_TYPE_ALERT)
|
|
{
|
|
GtkMessageType type = GTK_MESSAGE_OTHER;
|
|
switch (flags & WEBVIEW_DIALOG_FLAG_ALERT_MASK)
|
|
{
|
|
case WEBVIEW_DIALOG_FLAG_INFO:
|
|
type = GTK_MESSAGE_INFO;
|
|
break;
|
|
case WEBVIEW_DIALOG_FLAG_WARNING:
|
|
type = GTK_MESSAGE_WARNING;
|
|
break;
|
|
case WEBVIEW_DIALOG_FLAG_ERROR:
|
|
type = GTK_MESSAGE_ERROR;
|
|
break;
|
|
}
|
|
dlg = gtk_message_dialog_new(GTK_WINDOW(w->priv.window), GTK_DIALOG_MODAL,
|
|
type, GTK_BUTTONS_OK, "%s", title);
|
|
gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(dlg), "%s",
|
|
arg);
|
|
gtk_dialog_run(GTK_DIALOG(dlg));
|
|
gtk_widget_destroy(dlg);
|
|
}
|
|
}
|
|
|
|
static void webview_eval_finished(GObject *object, GAsyncResult *result,
|
|
gpointer userdata)
|
|
{
|
|
(void)object;
|
|
(void)result;
|
|
struct webview *w = (struct webview *)userdata;
|
|
w->priv.js_busy = 0;
|
|
}
|
|
|
|
WEBVIEW_API int webview_eval(struct webview *w, const char *js)
|
|
{
|
|
while (w->priv.ready == 0)
|
|
{
|
|
g_main_context_iteration(NULL, TRUE);
|
|
}
|
|
w->priv.js_busy = 1;
|
|
webkit_web_view_run_javascript(WEBKIT_WEB_VIEW(w->priv.webview), js, NULL,
|
|
webview_eval_finished, w);
|
|
while (w->priv.js_busy)
|
|
{
|
|
g_main_context_iteration(NULL, TRUE);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static gboolean webview_dispatch_wrapper(gpointer userdata)
|
|
{
|
|
struct webview *w = (struct webview *)userdata;
|
|
for (;;)
|
|
{
|
|
struct webview_dispatch_arg *arg =
|
|
(struct webview_dispatch_arg *)g_async_queue_try_pop(w->priv.queue);
|
|
if (arg == NULL)
|
|
{
|
|
break;
|
|
}
|
|
(arg->fn)(w, arg->arg);
|
|
g_free(arg);
|
|
}
|
|
return FALSE;
|
|
}
|
|
|
|
WEBVIEW_API void webview_dispatch(struct webview *w, webview_dispatch_fn fn,
|
|
void *arg)
|
|
{
|
|
struct webview_dispatch_arg *context =
|
|
(struct webview_dispatch_arg *)g_new(struct webview_dispatch_arg *, 1);
|
|
context->w = w;
|
|
context->arg = arg;
|
|
context->fn = fn;
|
|
g_async_queue_lock(w->priv.queue);
|
|
g_async_queue_push_unlocked(w->priv.queue, context);
|
|
if (g_async_queue_length_unlocked(w->priv.queue) == 1)
|
|
{
|
|
gdk_threads_add_idle(webview_dispatch_wrapper, w);
|
|
}
|
|
g_async_queue_unlock(w->priv.queue);
|
|
}
|
|
|
|
WEBVIEW_API void webview_terminate(struct webview *w)
|
|
{
|
|
w->priv.should_exit = 1;
|
|
}
|
|
|
|
WEBVIEW_API void webview_exit(struct webview *w) { (void)w; }
|
|
WEBVIEW_API void webview_print_log(const char *s)
|
|
{
|
|
fprintf(stderr, "%s\n", s);
|
|
}
|
|
|
|
#ifdef __cplusplus
|
|
}
|
|
#endif
|
|
|
|
#endif /* WEBVIEW_H */
|