1 /++ 2 A webview (based on [arsd.webview]) for minigui. 3 4 For now at least, to use this, you MUST have a CefApp in scope in main for the duration of your gui application. 5 6 History: 7 Added November 5, 2021. NOT YET STABLE. 8 +/ 9 module minigui_addons.webview; 10 11 version(linux) 12 version=cef; 13 version(Windows) 14 version=wv2; 15 16 17 /+ 18 SPA mode: put favicon on top level window, no other user controls at top level, links to different domains always open in new window. 19 +/ 20 21 // FIXME: look in /opt/cef for the dll and the locales 22 23 import arsd.minigui; 24 import arsd.webview; 25 26 version(wv2) 27 alias WebViewWidget = WebViewWidget_WV2; 28 else version(cef) 29 alias WebViewWidget = WebViewWidget_CEF; 30 else static assert(0, "no webview available"); 31 32 class WebViewWidgetBase : NestedChildWindowWidget { 33 protected SimpleWindow containerWindow; 34 35 protected this(Widget parent) { 36 containerWindow = new SimpleWindow(640, 480, null, OpenGlOptions.no, Resizability.allowResizing, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent)); 37 38 super(containerWindow, parent); 39 } 40 41 mixin Observable!(string, "title"); 42 mixin Observable!(string, "url"); 43 mixin Observable!(string, "status"); 44 mixin Observable!(int, "loadingProgress"); 45 46 abstract void refresh(); 47 abstract void back(); 48 abstract void forward(); 49 abstract void stop(); 50 51 abstract void navigate(string url); 52 53 // the url and line are for error reporting purposes. They might be ignored. 54 abstract void executeJavascript(string code, string url = null, int line = 0); 55 // for injecting stuff into the context 56 // abstract void executeJavascriptBeforeEachLoad(string code); 57 58 abstract void showDevTools(); 59 60 /++ 61 Your communication consists of running Javascript and sending string messages back and forth, 62 kinda similar to your communication with a web server. 63 +/ 64 // these form your communcation channel between the web view and the native world 65 // abstract void sendMessageToHost(string json); 66 // void delegate(string json) receiveMessageFromHost; 67 68 /+ 69 I also need a url filter 70 +/ 71 72 // this is implemented as a do-nothing in the NestedChildWindowWidget base 73 // but you will almost certainly need to override it in implementations. 74 // abstract void registerMovementAdditionalWork(); 75 } 76 77 // AddScriptToExecuteOnDocumentCreated 78 79 version(wv2) 80 class WebViewWidget_WV2 : WebViewWidgetBase { 81 private RC!ICoreWebView2 webview_window; 82 private RC!ICoreWebView2Environment webview_env; 83 private RC!ICoreWebView2Controller controller; 84 85 private bool initialized; 86 87 this(Widget parent) { 88 super(parent); 89 // that ctor sets containerWindow 90 91 Wv2App.useEnvironment((env) { 92 env.CreateCoreWebView2Controller(containerWindow.impl.hwnd, 93 callback!(ICoreWebView2CreateCoreWebView2ControllerCompletedHandler)(delegate(error, controller_raw) { 94 if(error || controller_raw is null) 95 return error; 96 97 // need to keep this beyond the callback or we're doomed. 98 controller = RC!ICoreWebView2Controller(controller_raw); 99 100 webview_window = controller.CoreWebView2; 101 102 webview_window.add_DocumentTitleChanged((sender, args) { 103 this.title = toGC(&sender.get_DocumentTitle); 104 return S_OK; 105 }); 106 107 // add_HistoryChanged 108 // that's where CanGoBack and CanGoForward can be rechecked. 109 110 RC!ICoreWebView2Settings Settings = webview_window.Settings; 111 Settings.IsScriptEnabled = TRUE; 112 Settings.AreDefaultScriptDialogsEnabled = TRUE; 113 Settings.IsWebMessageEnabled = TRUE; 114 115 116 auto ert = webview_window.add_NavigationStarting( 117 delegate (sender, args) { 118 this.url = toGC(&args.get_Uri); 119 return S_OK; 120 }); 121 122 RECT bounds; 123 GetClientRect(containerWindow.impl.hwnd, &bounds); 124 controller.Bounds = bounds; 125 error = webview_window.Navigate("http://arsdnet.net/test.html"w.ptr); 126 //error = webview_window.NavigateToString("<html><body>Hello</body></html>"w.ptr); 127 //error = webview_window.Navigate("http://192.168.1.10/"w.ptr); 128 129 controller.IsVisible = true; 130 131 initialized = true; 132 133 return S_OK; 134 })); 135 }); 136 } 137 138 override void registerMovementAdditionalWork() { 139 if(initialized) { 140 RECT bounds; 141 GetClientRect(containerWindow.impl.hwnd, &bounds); 142 controller.Bounds = bounds; 143 144 controller.NotifyParentWindowPositionChanged(); 145 } 146 } 147 148 override void refresh() { 149 if(!initialized) return; 150 webview_window.Reload(); 151 } 152 override void back() { 153 if(!initialized) return; 154 webview_window.GoBack(); 155 } 156 override void forward() { 157 if(!initialized) return; 158 webview_window.GoForward(); 159 } 160 override void stop() { 161 if(!initialized) return; 162 webview_window.Stop(); 163 } 164 165 override void navigate(string url) { 166 if(!initialized) return; 167 import std.utf; 168 auto error = webview_window.Navigate(url.toUTF16z); 169 } 170 171 // the url and line are for error reporting purposes 172 override void executeJavascript(string code, string url = null, int line = 0) { 173 if(!initialized) return; 174 import std.utf; 175 webview_window.ExecuteScript(code.toUTF16z, null); 176 } 177 178 override void showDevTools() { 179 if(!initialized) return; 180 webview_window.OpenDevToolsWindow(); 181 } 182 } 183 184 version(cef) 185 class WebViewWidget_CEF : WebViewWidgetBase { 186 this(Widget parent) { 187 //semaphore = new Semaphore; 188 assert(CefApp.active); 189 190 super(parent); 191 192 flushGui(); 193 194 mapping[containerWindow.nativeWindowHandle()] = this; 195 196 cef_window_info_t window_info; 197 window_info.parent_window = containerWindow.nativeWindowHandle; 198 199 cef_string_t cef_url = cef_string_t("http://arsdnet.net/test.html"); 200 201 cef_browser_settings_t browser_settings; 202 browser_settings.size = cef_browser_settings_t.sizeof; 203 204 client = new MiniguiCefClient(); 205 206 auto got = libcef.browser_host_create_browser(&window_info, client.passable, &cef_url, &browser_settings, null, null); 207 208 /+ 209 containerWindow.closeQuery = delegate() { 210 browserHandle.get_host.close_browser(true); 211 //containerWindow.close(); 212 }; 213 +/ 214 215 } 216 217 private MiniguiCefClient client; 218 219 /+ 220 override void close() { 221 // FIXME: this should prolly be on the onclose event instead 222 mapping.remove[win.nativeWindowHandle()]; 223 super.close(); 224 } 225 +/ 226 227 override void registerMovementAdditionalWork() { 228 if(browserWindow) { 229 static if(UsingSimpledisplayX11) 230 XResizeWindow(XDisplayConnection.get, browserWindow, width, height); 231 // FIXME: do for Windows too 232 } 233 } 234 235 236 private NativeWindowHandle browserWindow; 237 private RC!cef_browser_t browserHandle; 238 239 private static WebViewWidget[NativeWindowHandle] mapping; 240 private static WebViewWidget[NativeWindowHandle] browserMapping; 241 242 override void refresh() { if(browserHandle) browserHandle.reload(); } 243 override void back() { if(browserHandle) browserHandle.go_back(); } 244 override void forward() { if(browserHandle) browserHandle.go_forward(); } 245 override void stop() { if(browserHandle) browserHandle.stop_load(); } 246 247 override void navigate(string url) { 248 if(!browserHandle) return; 249 auto s = cef_string_t(url); 250 browserHandle.get_main_frame.load_url(&s); 251 } 252 253 // the url and line are for error reporting purposes 254 override void executeJavascript(string code, string url = null, int line = 0) { 255 if(!browserHandle) return; 256 257 auto c = cef_string_t(code); 258 auto u = cef_string_t(url); 259 browserHandle.get_main_frame.execute_java_script(&c, &u, line); 260 } 261 262 override void showDevTools() { 263 if(!browserHandle) return; 264 browserHandle.get_host.show_dev_tools(null /* window info */, client.passable, null /* settings */, null /* inspect element at coordinates */); 265 } 266 267 // FYI the cef browser host also allows things like custom spelling dictionaries and getting navigation entries. 268 269 // JS on init? 270 // JS bindings? 271 // user styles? 272 // navigate to string? (can just use a data uri maybe?) 273 // custom scheme handlers? 274 275 // navigation callbacks to prohibit certain things or move links to new window etc? 276 } 277 278 version(cef) { 279 280 //import core.sync.semaphore; 281 //__gshared Semaphore semaphore; 282 283 /+ 284 Finds the WebViewWidget associated with the given browser, then runs the given code in the gui thread on it. 285 +/ 286 void runOnWebView(RC!cef_browser_t browser, void delegate(WebViewWidget) dg) nothrow { 287 auto wh = cast(NativeWindowHandle) browser.get_host.get_window_handle; 288 runInGuiThreadAsync({ 289 if(auto wvp = wh in WebViewWidget.browserMapping) { 290 dg(*wvp); 291 } else { 292 //writeln("not found ", wh, WebViewWidget.browserMapping); 293 } 294 }); 295 } 296 297 class MiniguiCefLifeSpanHandler : CEF!cef_life_span_handler_t { 298 override int on_before_popup(RC!cef_browser_t, RC!cef_frame_t, const(cef_string_utf16_t)*, const(cef_string_utf16_t)*, cef_window_open_disposition_t, int, const(cef_popup_features_t)*, cef_window_info_t*, cef_client_t**, cef_browser_settings_t*, cef_dictionary_value_t**, int*) { 299 return 0; 300 } 301 override void on_after_created(RC!cef_browser_t browser) { 302 auto handle = cast(NativeWindowHandle) browser.get_host().get_window_handle(); 303 auto ptr = browser.passable; // this adds to the refcount until it gets inside 304 305 // the only reliable key (at least as far as i can tell) is the window handle 306 // so gonna look that up and do the sync mapping that way. 307 runInGuiThreadAsync({ 308 version(Windows) { 309 auto parent = GetParent(handle); 310 } else static if(UsingSimpledisplayX11) { 311 import arsd.simpledisplay : Window; 312 Window root; 313 Window parent; 314 uint c = 0; 315 auto display = XDisplayConnection.get; 316 Window* children; 317 XQueryTree(display, handle, &root, &parent, &children, &c); 318 XFree(children); 319 } else static assert(0); 320 321 if(auto wvp = parent in WebViewWidget.mapping) { 322 auto wv = *wvp; 323 wv.browserWindow = handle; 324 wv.browserHandle = RC!cef_browser_t(ptr); 325 326 wv.registerMovementAdditionalWork(); 327 328 WebViewWidget.browserMapping[handle] = wv; 329 } else assert(0); 330 }); 331 } 332 override int do_close(RC!cef_browser_t browser) { 333 return 0; 334 } 335 override void on_before_close(RC!cef_browser_t browser) { 336 /+ 337 import std.stdio; debug writeln("notify"); 338 try 339 semaphore.notify; 340 catch(Exception e) { assert(0); } 341 +/ 342 } 343 } 344 345 class MiniguiLoadHandler : CEF!cef_load_handler_t { 346 override void on_loading_state_change(RC!(cef_browser_t) browser, int isLoading, int canGoBack, int canGoForward) { 347 /+ 348 browser.runOnWebView((WebViewWidget wvw) { 349 wvw.parentWindow.win.title = wvw.browserHandle.get_main_frame.get_url.toGCAndFree; 350 }); 351 +/ 352 } 353 override void on_load_start(RC!(cef_browser_t), RC!(cef_frame_t), cef_transition_type_t) { 354 } 355 override void on_load_error(RC!(cef_browser_t), RC!(cef_frame_t), cef_errorcode_t, const(cef_string_utf16_t)*, const(cef_string_utf16_t)*) { 356 } 357 override void on_load_end(RC!(cef_browser_t), RC!(cef_frame_t), int) { 358 } 359 } 360 361 class MiniguiDialogHandler : CEF!cef_dialog_handler_t { 362 363 override int on_file_dialog(RC!(cef_browser_t) browser, cef_file_dialog_mode_t mode, const(cef_string_utf16_t)* title, const(cef_string_utf16_t)* default_file_path, cef_string_list_t accept_filters, int selected_accept_filter, RC!(cef_file_dialog_callback_t) callback) { 364 try { 365 auto ptr = callback.passable(); 366 runInGuiThreadAsync({ 367 getOpenFileName((string name) { 368 auto callback = RC!cef_file_dialog_callback_t(ptr); 369 auto list = libcef.string_list_alloc(); 370 auto item = cef_string_t(name); 371 libcef.string_list_append(list, &item); 372 callback.cont(selected_accept_filter, list); 373 }, null, null, () { 374 auto callback = RC!cef_file_dialog_callback_t(ptr); 375 callback.cancel(); 376 }); 377 }); 378 } catch(Exception e) {} 379 380 return 1; 381 } 382 } 383 384 class MiniguiDisplayHandler : CEF!cef_display_handler_t { 385 override void on_address_change(RC!(cef_browser_t) browser, RC!(cef_frame_t), const(cef_string_utf16_t)* address) { 386 auto url = address.toGC; 387 browser.runOnWebView((wv) { 388 wv.url = url; 389 }); 390 } 391 override void on_title_change(RC!(cef_browser_t) browser, const(cef_string_utf16_t)* title) { 392 auto t = title.toGC; 393 browser.runOnWebView((wv) { 394 wv.title = t; 395 }); 396 } 397 override void on_favicon_urlchange(RC!(cef_browser_t) browser, cef_string_list_t) { 398 } 399 override void on_fullscreen_mode_change(RC!(cef_browser_t) browser, int) { 400 } 401 override int on_tooltip(RC!(cef_browser_t) browser, cef_string_utf16_t*) { 402 return 0; 403 } 404 override void on_status_message(RC!(cef_browser_t) browser, const(cef_string_utf16_t)* msg) { 405 auto status = msg.toGC; 406 browser.runOnWebView((wv) { 407 wv.status = status; 408 }); 409 } 410 override void on_loading_progress_change(RC!(cef_browser_t) browser, double progress) { 411 // progress is from 0.0 to 1.0 412 browser.runOnWebView((wv) { 413 wv.loadingProgress = cast(int) (progress * 100); 414 }); 415 } 416 override int on_console_message(RC!(cef_browser_t), cef_log_severity_t, const(cef_string_utf16_t)*, const(cef_string_utf16_t)*, int) { 417 return 0; // 1 means to suppress it being automatically output 418 } 419 override int on_auto_resize(RC!(cef_browser_t), const(cef_size_t)*) { 420 return 0; 421 } 422 override int on_cursor_change(RC!(cef_browser_t), cef_cursor_handle_t, cef_cursor_type_t, const(cef_cursor_info_t)*) { 423 return 0; 424 } 425 } 426 427 class MiniguiCefClient : CEF!cef_client_t { 428 MiniguiCefLifeSpanHandler lsh; 429 MiniguiLoadHandler loadHandler; 430 MiniguiDialogHandler dialogHandler; 431 MiniguiDisplayHandler displayHandler; 432 this() { 433 lsh = new MiniguiCefLifeSpanHandler(); 434 loadHandler = new MiniguiLoadHandler(); 435 dialogHandler = new MiniguiDialogHandler(); 436 displayHandler = new MiniguiDisplayHandler(); 437 } 438 439 override cef_audio_handler_t* get_audio_handler() { 440 return null; 441 } 442 override cef_context_menu_handler_t* get_context_menu_handler() { 443 return null; 444 } 445 override cef_dialog_handler_t* get_dialog_handler() { 446 return dialogHandler.returnable; 447 } 448 override cef_display_handler_t* get_display_handler() { 449 return displayHandler.returnable; 450 } 451 override cef_download_handler_t* get_download_handler() { 452 return null; 453 } 454 override cef_drag_handler_t* get_drag_handler() { 455 return null; 456 } 457 override cef_find_handler_t* get_find_handler() { 458 return null; 459 } 460 override cef_focus_handler_t* get_focus_handler() { 461 return null; 462 } 463 override cef_jsdialog_handler_t* get_jsdialog_handler() { 464 // needed for alert etc. 465 return null; 466 } 467 override cef_keyboard_handler_t* get_keyboard_handler() { 468 // this can handle keyboard shortcuts etc 469 return null; 470 } 471 override cef_life_span_handler_t* get_life_span_handler() { 472 return lsh.returnable; 473 } 474 override cef_load_handler_t* get_load_handler() { 475 return loadHandler.returnable; 476 } 477 override cef_render_handler_t* get_render_handler() { 478 // this thing might work for an off-screen thing 479 // like to an image or to a video stream maybe 480 return null; 481 } 482 override cef_request_handler_t* get_request_handler() { 483 return null; 484 } 485 override int on_process_message_received(RC!cef_browser_t, RC!cef_frame_t, cef_process_id_t, RC!cef_process_message_t) { 486 return 0; // return 1 if you can actually handle the message 487 } 488 override cef_frame_handler_t* get_frame_handler() nothrow { 489 return null; 490 } 491 override cef_print_handler_t* get_print_handler() nothrow { 492 return null; 493 } 494 495 } 496 }