1 module guino;
2
3 import std.json;
4 import std.string;
5 import std.conv : to;
6
7 // Window size hints
8 enum WEBVIEW_HINT_NONE=0; /// Width and height are default size
9 enum WEBVIEW_HINT_MIN=1; /// Width and height are minimum bounds
10 enum WEBVIEW_HINT_MAX=2; /// Width and height are maximum bounds
11 enum WEBVIEW_HINT_FIXED=3; /// Window size can not be changed by a user
12
13
14 /// The main struct
15 struct WebView {
16
17 private webview_t handle = null;
18
19 /++ Create a new WebView.
20 + Params: enableDebug = allow code inspection from webview
21 +/
22 this(bool enableDebug, void * window = null) { create(enableDebug, window); }
23
24 /// Ditto
25 void create(bool enableDebug = false, void * window = null) in(handle is null, error_already_inited) { handle = webview_create(enableDebug, window); }
26
27 /// Start the WebView. Be sure to set size before.
28 void run() in(handle !is null, error_not_inited) { webview_run(handle); }
29
30 /++ Set webview HTML directly.
31 + ---
32 webview.html = "<html><body>hello, world!</body></html>";
33 + ---
34 +/
35 void html(string data) in(handle !is null, error_not_inited) { webview_set_html(handle, data.toStringz); }
36
37 /// Navigates webview to the given URL. URL may be a properly encoded data URI.
38 void navigate(string url) in(handle !is null, error_not_inited) { webview_navigate(handle, url.toStringz); }
39
40 /// Set the window title
41 void title(string title) in(handle !is null, error_not_inited) { webview_set_title(handle, title.toStringz);}
42
43 /// Set the window size
44 void size(int width, int height, int hints = WEBVIEW_HINT_NONE) in(handle !is null, error_not_inited) { webview_set_size(handle, width, height, hints);}
45
46 ///
47 void terminate() { if(handle !is null) webview_terminate(handle); handle = null; }
48
49 /// Returns a handle to the native window
50 void* window() in(handle !is null, error_not_inited) { return webview_get_window(handle); }
51
52
53 /++ Eval some js code. When it is done, `func()` is called (optional)
54 + ---
55 + void callback(JSValue v) { writeln("Result from js: ", v); }
56 + webview.eval!callback("1+1");
57 + ---
58 +/
59 void eval(alias func = null)(string js, void* extra = null)
60 in(handle !is null, error_not_inited)
61 {
62 static if (is (typeof(func) == typeof(null))) webview_eval(handle, js.toStringz);
63 else
64 {
65 import std.uuid;
66
67 struct EvalPayload
68 {
69 void *extra;
70 webview_t handle;
71 string uuid;
72 }
73
74 auto uuid = "__eval__" ~ randomUUID().toString.replace("-", "_");
75 EvalPayload *payload = new EvalPayload(extra, handle, uuid);
76
77 bindJs!(
78 (JSONValue[] v, void* extra)
79 {
80 import core.memory : GC;
81
82 EvalPayload *p = cast(EvalPayload*) extra;
83 webview_unbind(p.handle, p.uuid.toStringz);
84 static if (__traits(compiles, func(v[0], p.extra))) func(v[0], p.extra);
85 else func(v[0]);
86 p.destroy();
87 GC.free(p);
88 }
89 )
90 (uuid, cast(void*)payload);
91
92 eval(uuid ~ `(eval('` ~ js.escapeJs() ~ `'));`);
93 }
94 }
95
96 /++ Helper function to convert a file to data uri to embed inside html.
97 + It works at compile-time so you must set source import paths on your project.
98 + See_Also: [toDataUri], [fileAsDataUri], https://dlang.org/spec/expression.html#import_expressions
99 + ---
100 + webview.byId("myimg").src = importAsDataUri!"image.jpg";
101 +/
102 static string importAsDataUri(string file)(string mimeType = "application/octet-stream")
103 {
104 import std.algorithm;
105 import std.encoding;
106 import std.base64;
107
108 auto mime = mimeType;
109 auto extIdx = file.lastIndexOf('.');
110 if (extIdx >= 0 && file[extIdx..$] in mimeTypes)
111 mime = mimeTypes[file[extIdx..$]];
112
113 auto bytes = import(file).representation;
114 auto bom = getBOM(bytes);
115 bytes = bytes[bom.sequence.length .. $];
116
117 return WebView.toDataUri(bytes, mime);
118 }
119
120 /++ Helper function to convert bytes to data uri to embed inside html
121 + See_Also: [importAsDataUri], [fileAsDataUri]
122 +/
123 static auto toDataUri(const ubyte[] bytes, string mimeType = "application/octet-stream")
124 {
125 import std.base64;
126 return ("data:" ~ mimeType ~ ";base64," ~ Base64.encode(bytes));
127 }
128
129 /++ Helper function to convert bytes to data uri to embed inside html
130 + See_Also: [importAsDataUri], [toDataUri]
131 +/
132 static string fileAsDataUri(string file, string mimeType = "application/octet-stream")
133 {
134 import std.file;
135 auto mime = mimeType;
136 auto extIdx = file.lastIndexOf('.');
137 if (extIdx >= 0 && file[extIdx..$] in mimeTypes)
138 mime = mimeTypes[file[extIdx..$]];
139
140 ubyte[] data = cast(ubyte[])std.file.read(file);
141 return toDataUri(data, mime);
142 }
143
144 /++ Injects JavaScript code at the initialization of the new page. Every time
145 + the webview will open a new page - this initialization code will be
146 + executed. It is guaranteed that code is executed before window.onload.
147 +/
148 void onInit(string js) in(handle !is null, error_not_inited) { webview_init(handle, js.toStringz); }
149
150 /// Ditto
151 void onInit(alias func)()
152 {
153 import std.uuid;
154 string uuid = "_init_" ~ randomUUID.toString().replace("-", "_");
155 static void proxy(JSONValue[]) { func(); }
156 bindJs!(proxy)(uuid);
157 onInit(uuid ~ "();");
158 }
159
160
161 /++ Respond to a binding call from js.
162 + See_Also: [resolve], [reject]
163 +/
164 void respond(string seq, bool resolved, JSONValue v) in(handle !is null, error_not_inited) { webview_return(handle, seq.toStringz, resolved?0:1, v.toString.toStringz); }
165
166 /++ Resolve a js promise
167 + See_Also: [respond], [reject]
168 +/
169 void resolve(string seq, JSONValue v) in(handle !is null, error_not_inited) { respond(seq, 0, v); }
170
171 /++ Reject a js promise
172 + See_Also: [respond], [resolve]
173 +/
174 void reject(string seq, JSONValue v) in(handle !is null, error_not_inited) { respond(seq, 1, v); }
175
176 /++ Removes a D callback that was previously set by `bind()`
177 + See_Also: [bindJs]
178 ++/
179 void unbindJs(string name) in(handle !is null, error_not_inited) { webview_unbind(handle, name.toStringz); }
180
181 /++ Create a callback in D for a js function.
182 + See_Also: [response], [resolve], [reject], [unbindJs]
183 + ---
184 + // Simple callback without params
185 + void hello() { ... }
186 +
187 + // Js arguments will be passed as JSON array
188 + void world(JSONValue[] args) { ... }
189 +
190 + // You can use the sequence arg to return a response.
191 + void reply(JSONValue[] args, string sequence) { ... resolve(sequence, JSONValue("Hello Js!")); }
192 +
193 + webview.bind!hello; // If you call hello() from js, hello() from D will respond
194 + webview.bind!hello("test") // If tou call test() from js, hello() from D will respond
195 + ---
196 +/
197 void bindJs(alias func)(string jsFunc = __traits(identifier, func), void* extra = null)
198 in(handle !is null, error_not_inited)
199 {
200
201 static if (__traits(compiles, func(JSONValue[].init, string.init, (void *).init)))
202 extern(C) void callback(const char *seq, const char *request, void *extra)
203 {
204 func(parseJSON(request.to!string).array, seq.to!string, extra);
205 }
206
207 else static if (__traits(compiles, func(JSONValue[].init, (void*).init)))
208 extern(C) void callback(const char *seq, const char *request, void *extra)
209 {
210 func(parseJSON(request.to!string).array, extra);
211 }
212
213
214 else static if (__traits(compiles, func(JSONValue[].init, string.init)))
215 extern(C) void callback(const char *seq, const char *request, void *extra)
216 {
217 func(parseJSON(request.to!string).array, seq.to!string);
218 }
219
220 else static if (__traits(compiles, func(JSONValue[].init)))
221 extern(C) void callback(const char *seq, const char *request, void *extra)
222 {
223 func(parseJSON(request.to!string).array);
224 }
225
226 else static if (__traits(compiles, func()))
227 extern(C) void callback(const char *seq, const char *request, void *extra)
228 {
229 func();
230 }
231
232
233 static if (__traits(compiles, callback(null, null, null))) webview_bind(handle, jsFunc.toStringz, &callback, extra);
234 else static assert(0, "Can't bind `" ~ typeof(func).stringof ~ "` try with `void func(JSONValue[] args)`, for example.");
235 }
236
237 /// Ditto
238 void bindJs(alias func)(void* extra) in(handle !is null, error_not_inited) { bindJs!func( __traits(identifier, func), extra); }
239
240
241 /++ A helper function to parse args passed as JSONValue[]
242 + See_Also: [bindJs]
243 + ---
244 + void myFunction(JSONValue[] arg)
245 + {
246 + with(WebView.parseJsArgs!(int, "hello", string, "world")(args))
247 + {
248 + // Just like we have named args
249 + writeln(hello);
250 + writeln(world);
251 + }
252 + }
253 + ---
254 +/
255 static auto parseJsArgs(T...)(JSONValue[] v)
256 {
257 import std.typecons : Tuple;
258
259 alias TUPLE = Tuple!T;
260 TUPLE ret;
261 bool fail;
262
263 static foreach(idx; 0..TUPLE.length)
264 {
265 fail = false;
266 if (idx < v.length)
267 {
268 try { mixin("ret[" ~ idx.to!string ~ "]") = v[idx].get!(typeof(TUPLE[idx])); }
269 catch (Exception e) { fail = true; }
270 }
271 else fail = true;
272
273 if (fail)
274 mixin("ret[" ~ idx.to!string ~ "]") = typeof(TUPLE[idx]).init;
275 }
276
277 return ret;
278 }
279
280
281 /++ Search for an element in the dom, using a css selector. Returned element can forward calls to js.
282 + See_Also: [byId]
283 +/
284 Element bySelector(string query)
285 in(handle !is null, error_not_inited)
286 {
287 return new Element(this, "document.querySelector('" ~ query.escapeJs() ~ "')");
288 }
289
290 /++ Search for an element in the dom, using id. Returned element can forward calls to js.
291 + See_Also: [bySelector]
292 + ---
293 + webview.byId("myid").innerText = "Hi!";
294 + webview.byId("myid").setAttribute("href", "https://example.com");
295 + ---
296 +/
297 Element byId(string id)
298 in(handle !is null, error_not_inited)
299 {
300 return new Element(this, "document.getElementById('" ~ id.escapeJs() ~ "')");
301 }
302
303 class Element
304 {
305 private string js;
306 private WebView wv;
307
308 private this(WebView wv, string js) { this.wv = wv; this.js = js; }
309
310
311 // Try to use js dom from D
312 void opDispatch(string name, T...)(T val) {
313
314 static if (T.length == 0) zeroParamsOpDispatch!(name)(val);
315 else static if (T.length == 1) singleParamOpDispatch!(name)(val);
316 else static if (T.length > 1)
317 {
318
319 import std.conv;
320 import std.traits;
321 import std.string;
322
323 string jsCode = js ~ "." ~ name ~ "(";
324
325 static foreach(v; val)
326 {
327 static if (isIntegral!(typeof(v)) || isFloatingPoint!(typeof(v))) jsCode ~= v.to!string ~ ",";
328 else static if (isSomeString!(typeof(v))) jsCode ~= `'` ~ v.escapeJs() ~ `',`;
329 else throw new Exception("Can't assign " ~ T.stringof);
330 }
331
332 if (jsCode.endsWith(",")) jsCode = jsCode[0..$-1];
333
334 jsCode ~= ")";
335 wv.eval(jsCode);
336 }
337
338 }
339
340 private void zeroParamOpDispatch(string name, T)(T val) {
341 import std.conv;
342 import std.traits;
343
344 string jsCode;
345
346 string fullName = js ~ "." ~ name;
347
348 jsCode ~= "if (" ~fullName ~ " != null && (" ~ fullName ~ " instanceof Function)) {\n";
349 jsCode ~= fullName ~ "()";
350 jsCode ~= "\n}";
351
352 wv.eval(jsCode);
353 }
354
355 private void singleParamOpDispatch(string name, T)(T val) {
356 import std.conv;
357 import std.traits;
358
359 string jsCode;
360
361 string fullName = js ~ "." ~ name;
362
363 jsCode ~= "if (" ~fullName ~ " != null && (" ~ fullName ~ " instanceof Function)) {\n";
364 jsCode ~= js ~ "." ~ name;
365 static if (isIntegral!T || isFloatingPoint!T) jsCode ~= val.to!string;
366 else static if (isSomeString!T) jsCode ~= `('` ~ val.escapeJs() ~ "'";
367 else throw new Exception("Can't assign " ~ T.stringof);
368 jsCode ~= ");";
369
370 jsCode ~= "\n} else { \n";
371 static if (isIntegral!T || isFloatingPoint!T) jsCode ~= js ~ "." ~ name ~ `=` ~ val.to!string ~ ";";
372 else static if (isSomeString!T) jsCode ~= js ~ "." ~ name ~ `= '` ~ val.escapeJs() ~ "';";
373 else throw new Exception("Can't assign " ~ T.stringof);
374
375 jsCode ~= "\n}";
376
377 wv.eval(jsCode);
378 }
379
380 void setAttribute(T)(string name, T val) {
381 import std.conv;
382 import std.traits;
383
384 string jsCode;
385
386 static if (isIntegral!T || isFloatingPoint!T) jsCode = js ~ ".setAttribute('" ~ name.escapeJs() ~ `', ` ~ val.to!string ~ ");";
387 else static if (isSomeString!T) jsCode = js ~ ".setAttribute('" ~ name.escapeJs() ~ `', '` ~ val.escapeJs() ~ "');";
388 else throw new Exception("Can't assign " ~ T.stringof);
389
390 wv.eval(jsCode);
391 }
392
393 }
394
395 class Elements
396 {
397 private string js;
398 private WebView wv;
399
400 private this(WebView wv, string js) { this.wv = wv; this.js = js; }
401
402 Element opIndex(size_t idx)
403 {
404 return new Element(wv, js ~ "[" ~ idx.to!string ~ "]");
405 }
406
407 }
408
409 }
410
411 /// Helper function to escape js strings
412 string escapeJs(string s, char stringDelimeter = '\'')
413 {
414 return s.replace(`\`, `\\`).replace(stringDelimeter, `\` ~ stringDelimeter);
415 }
416
417
418 private:
419
420 private immutable error_not_inited = "WebView is not initialized. Please call .create() method first.";
421 private immutable error_already_inited = "Please call .terminate() first.";
422
423
424 enum string[string] mimeTypes =
425 [
426 ".aac" : "audio/aac", ".abw" : "application/x-abiword", ".arc" : "application/x-freearc", ".avif" : "image/avif",
427 ".bin" : "application/octet-stream", ".bmp" : "image/bmp", ".bz" : "application/x-bzip", ".bz2" : "application/x-bzip2",
428 ".cda" : "application/x-cdf", ".csh" : "application/x-csh", ".css" : "text/css", ".csv" : "text/csv",
429 ".doc" : "application/msword", ".docx" : "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
430 ".eot" : "application/vnd.ms-fontobject", ".epub" : "application/epub+zip", ".gz" : "application/gzip",
431 ".gif" : "image/gif", ".htm" : "text/html", ".html" : "text/html", ".ico" : "image/vnd.microsoft.icon",
432 ".ics" : "text/calendar", ".jar" : "application/java-archive", ".jpeg" : "image/jpeg", ".jpg" : "image/jpeg",
433 ".js" : "text/javascript", ".json" : "application/json", ".jsonld" : "application/ld+json", ".mid" : ".midi",
434 ".mjs" : "text/javascript", ".mp3" : "audio/mpeg",".mp4" : "video/mp4", ".mpeg" : "video/mpeg", ".mpkg" : "application/vnd.apple.installer+xml",
435 ".odp" : "application/vnd.oasis.opendocument.presentation", ".ods" : "application/vnd.oasis.opendocument.spreadsheet",
436 ".odt" : "application/vnd.oasis.opendocument.text", ".oga" : "audio/ogg", ".ogv" : "video/ogg", ".ogx" : "application/ogg",
437 ".opus" : "audio/opus", ".otf" : "font/otf", ".png" : "image/png", ".pdf" : "application/pdf", ".php" : "application/x-httpd-php",
438 ".ppt" : "application/vnd.ms-powerpoint", ".pptx" : "application/vnd.openxmlformats-officedocument.presentationml.presentation",
439 ".rar" : "application/vnd.rar", ".rtf" : "application/rtf", ".sh" : "application/x-sh", ".svg" : "image/svg+xml",
440 ".swf" : "application/x-shockwave-flash", ".tar" : "application/x-tar", ".tif" : "image/tiff", ".tiff" : "image/tiff",
441 ".ts" : "video/mp2t", ".ttf" : "font/ttf", ".txt" : "text/plain", ".vsd" : "application/vnd.visio", ".wasm" : "application/wasm",
442 ".wav" : "audio/wav", ".weba" : "audio/webm", ".webm" : "video/webm", ".webp" : "image/webp", ".woff" : "font/woff", ".woff2" : "font/woff2",
443 ".xhtml" : "application/xhtml+xml", ".xls" : "application/vnd.ms-excel", ".xlsx" : "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
444 ".xml" : "application/xml", ".xul" : "application/vnd.mozilla.xul+xml", ".zip" : "application/zip", ".3gp" : "video/3gpp",
445 ".3g2" : "video/3gpp2", ".7z" : "application/x-7z-compressed"
446 ];
447
448
449 alias webview_t = void*;
450
451 // Creates a new webview instance. If debug is non-zero - developer tools will
452 // be enabled (if the platform supports them). The window parameter can be a
453 // pointer to the native window wb.handle. If it's non-null - then child WebView
454 // is embedded into the given parent window. Otherwise a new window is created.
455 // Depending on the platform, a GtkWindow, NSWindow or HWND pointer can be
456 // passed here. Returns null on failure. Creation can fail for various reasons
457 // such as when required runtime dependencies are missing or when window creation
458 // fails.
459 extern(C) webview_t webview_create(int _debug, void *window);
460
461 // Destroys a webview and closes the native window.
462 extern(C) void webview_destroy(webview_t w);
463
464 // Runs the main loop until it's terminated. After this function exits - you
465 // must destroy the webview.
466 extern(C) void webview_run(webview_t w);
467
468 // Stops the main loop. It is safe to call this function from another other
469 // background thread.
470 extern(C) void webview_terminate(webview_t w);
471
472 // Posts a function to be executed on the main thread. You normally do not need
473 // to call this function, unless you want to tweak the native window.
474 extern(C) void
475 webview_dispatch(webview_t w, dispatchCallback, void *arg);
476
477 alias dispatchCallback = extern(C) void function(webview_t w, void* arg);
478
479 // Returns a native window wb.handle pointer. When using a GTK backend the pointer
480 // is a GtkWindow pointer, when using a Cocoa backend the pointer is a NSWindow
481 // pointer, when using a Win32 backend the pointer is a HWND pointer.
482 extern(C) void *webview_get_window(webview_t w);
483
484 // Updates the title of the native window. Must be called from the UI thread.
485 extern(C) void webview_set_title(webview_t w, const char *title);
486
487
488 // Updates the size of the native window. See WEBVIEW_HINT constants.
489 extern(C) void webview_set_size(webview_t w, int width, int height,
490 int hints);
491
492 // Navigates webview to the given URL. URL may be a properly encoded data URI.
493 // Examples:
494 // webview_navigate(w, "https://github.com/webview/webview");
495 // webview_navigate(w, "data:text/html,%3Ch1%3EHello%3C%2Fh1%3E");
496 // webview_navigate(w, "data:text/html;base64,PGgxPkhlbGxvPC9oMT4=");
497 extern(C) void webview_navigate(webview_t w, const char *url);
498
499 // Set webview HTML directly.
500 // Example: webview_set_html(w, "<h1>Hello</h1>");
501 extern(C) void webview_set_html(webview_t w, const char *html);
502
503 // Injects JavaScript code at the initialization of the new page. Every time
504 // the webview will open a new page this initialization code will be
505 // executed. It is guaranteed that code is executed before window.onload.
506 extern(C) void webview_init(webview_t w, const char *js);
507
508 // Evaluates arbitrary JavaScript code. Evaluation happens asynchronously, also
509 // the result of the expression is ignored. Use RPC bindings if you want to
510 // receive notifications about the results of the evaluation.
511 extern(C) void webview_eval(webview_t w, const char *js);
512
513 // Binds a native C callback so that it will appear under the given name as a
514 // global JavaScript function. Internally it uses webview_init(). The callback
515 // receives a sequential request id, a request string and a user-provided
516 // argument pointer. The request string is a JSON array of all the arguments
517 // passed to the JavaScript function.
518 extern(C) void webview_bind(webview_t w, const char *name,
519 bindCallback,
520 void *arg);
521
522 alias bindCallback = extern(C) void function (const char *seq, const char *req, void *arg);
523
524 // Removes a native C callback that was previously set by webview_bind.
525 extern(C) void webview_unbind(webview_t w, const char *name);
526
527 // Responds to a binding call from the JS side. The ID/sequence number must
528 // match the value passed to the binding wb.handler in order to respond to the
529 // call and complete the promise on the JS side. A status of zero resolves
530 // the promise, and any other value rejects it. The result must either be a
531 // valid JSON value or an empty string for the primitive JS value "undefined".
532 extern(C) void webview_return(webview_t w, const char *seq, int status, const char *result);