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);