/++ A webview (based on [arsd.webview]) for minigui. For now at least, to use this, you MUST have a [WebViewApp] in scope in main for the duration of your gui application. Warning: CEF spams the current directory with a bunch of files and directories. You might want to run your program in a dedicated location. History: Added November 5, 2021. NOT YET STABLE. Examples: --- /+ dub.sdl: name "web" dependency "arsd-official:minigui-webview" version="*" +/ import arsd.minigui; import arsd.minigui_addons.webview; void main() { auto app = WebViewApp(null); auto window = new Window; auto webview = new WebViewWidget("http://dlang.org/", window); window.loop; } --- +/ module minigui_addons.webview; // FIXME: i think i can download the cef automatically if needed. version(linux) version=cef; version(Windows) version=wv2; /+ SPA mode: put favicon on top level window, no other user controls at top level, links to different domains always open in new window. +/ // FIXME: look in /opt/cef for the dll and the locales import arsd.minigui; import arsd.webview; version(wv2) { alias WebViewWidget = WebViewWidget_WV2; alias WebViewApp = Wv2App; } else version(cef) { alias WebViewWidget = WebViewWidget_CEF; alias WebViewApp = CefApp; } else static assert(0, "no webview available"); class WebViewWidgetBase : NestedChildWindowWidget { protected SimpleWindow containerWindow; protected this(Widget parent) { containerWindow = new SimpleWindow(640, 480, null, OpenGlOptions.no, Resizability.allowResizing, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent)); super(containerWindow, parent); } mixin Observable!(string, "title"); mixin Observable!(string, "url"); mixin Observable!(string, "status"); mixin Observable!(int, "loadingProgress"); abstract void refresh(); abstract void back(); abstract void forward(); abstract void stop(); abstract void navigate(string url); // the url and line are for error reporting purposes. They might be ignored. abstract void executeJavascript(string code, string url = null, int line = 0); // for injecting stuff into the context // abstract void executeJavascriptBeforeEachLoad(string code); abstract void showDevTools(); /++ Your communication consists of running Javascript and sending string messages back and forth, kinda similar to your communication with a web server. +/ // these form your communcation channel between the web view and the native world // abstract void sendMessageToHost(string json); // void delegate(string json) receiveMessageFromHost; /+ I also need a url filter +/ // this is implemented as a do-nothing in the NestedChildWindowWidget base // but you will almost certainly need to override it in implementations. // abstract void registerMovementAdditionalWork(); } // AddScriptToExecuteOnDocumentCreated version(wv2) class WebViewWidget_WV2 : WebViewWidgetBase { private RC!ICoreWebView2 webview_window; private RC!ICoreWebView2Environment webview_env; private RC!ICoreWebView2Controller controller; private bool initialized; this(string url, Widget parent) { super(parent); // that ctor sets containerWindow Wv2App.useEnvironment((env) { env.CreateCoreWebView2Controller(containerWindow.impl.hwnd, callback!(ICoreWebView2CreateCoreWebView2ControllerCompletedHandler)(delegate(error, controller_raw) { if(error || controller_raw is null) return error; // need to keep this beyond the callback or we're doomed. controller = RC!ICoreWebView2Controller(controller_raw); webview_window = controller.CoreWebView2; webview_window.add_DocumentTitleChanged((sender, args) { this.title = toGC(&sender.get_DocumentTitle); return S_OK; }); // add_HistoryChanged // that's where CanGoBack and CanGoForward can be rechecked. RC!ICoreWebView2Settings Settings = webview_window.Settings; Settings.IsScriptEnabled = TRUE; Settings.AreDefaultScriptDialogsEnabled = TRUE; Settings.IsWebMessageEnabled = TRUE; auto ert = webview_window.add_NavigationStarting( delegate (sender, args) { this.url = toGC(&args.get_Uri); return S_OK; }); RECT bounds; GetClientRect(containerWindow.impl.hwnd, &bounds); controller.Bounds = bounds; //error = webview_window.Navigate("http://arsdnet.net/test.html"w.ptr); //error = webview_window.NavigateToString("Hello"w.ptr); //error = webview_window.Navigate("http://192.168.1.10/"w.ptr); WCharzBuffer bfr = WCharzBuffer(url); webview_window.Navigate(bfr.ptr); controller.IsVisible = true; initialized = true; return S_OK; })); }); } override void registerMovementAdditionalWork() { if(initialized) { RECT bounds; GetClientRect(containerWindow.impl.hwnd, &bounds); controller.Bounds = bounds; controller.NotifyParentWindowPositionChanged(); } } override void refresh() { if(!initialized) return; webview_window.Reload(); } override void back() { if(!initialized) return; webview_window.GoBack(); } override void forward() { if(!initialized) return; webview_window.GoForward(); } override void stop() { if(!initialized) return; webview_window.Stop(); } override void navigate(string url) { if(!initialized) return; import std.utf; auto error = webview_window.Navigate(url.toUTF16z); } // the url and line are for error reporting purposes override void executeJavascript(string code, string url = null, int line = 0) { if(!initialized) return; import std.utf; webview_window.ExecuteScript(code.toUTF16z, null); } override void showDevTools() { if(!initialized) return; webview_window.OpenDevToolsWindow(); } } version(cef) class WebViewWidget_CEF : WebViewWidgetBase { this(string url, Widget parent) { //semaphore = new Semaphore; assert(CefApp.active); super(parent); flushGui(); mapping[containerWindow.nativeWindowHandle()] = this; cef_window_info_t window_info; window_info.parent_window = containerWindow.nativeWindowHandle; cef_string_t cef_url = cef_string_t(url);//"http://arsdnet.net/test.html"); cef_browser_settings_t browser_settings; browser_settings.size = cef_browser_settings_t.sizeof; client = new MiniguiCefClient(); auto got = libcef.browser_host_create_browser(&window_info, client.passable, &cef_url, &browser_settings, null, null); /+ containerWindow.closeQuery = delegate() { browserHandle.get_host.close_browser(true); //containerWindow.close(); }; +/ } private MiniguiCefClient client; /+ override void close() { // FIXME: this should prolly be on the onclose event instead mapping.remove[win.nativeWindowHandle()]; super.close(); } +/ override void registerMovementAdditionalWork() { if(browserWindow) { static if(UsingSimpledisplayX11) XResizeWindow(XDisplayConnection.get, browserWindow, width, height); // FIXME: do for Windows too } } private NativeWindowHandle browserWindow; private RC!cef_browser_t browserHandle; private static WebViewWidget[NativeWindowHandle] mapping; private static WebViewWidget[NativeWindowHandle] browserMapping; override void refresh() { if(browserHandle) browserHandle.reload(); } override void back() { if(browserHandle) browserHandle.go_back(); } override void forward() { if(browserHandle) browserHandle.go_forward(); } override void stop() { if(browserHandle) browserHandle.stop_load(); } override void navigate(string url) { if(!browserHandle) return; auto s = cef_string_t(url); browserHandle.get_main_frame.load_url(&s); } // the url and line are for error reporting purposes override void executeJavascript(string code, string url = null, int line = 0) { if(!browserHandle) return; auto c = cef_string_t(code); auto u = cef_string_t(url); browserHandle.get_main_frame.execute_java_script(&c, &u, line); } override void showDevTools() { if(!browserHandle) return; browserHandle.get_host.show_dev_tools(null /* window info */, client.passable, null /* settings */, null /* inspect element at coordinates */); } // FYI the cef browser host also allows things like custom spelling dictionaries and getting navigation entries. // JS on init? // JS bindings? // user styles? // navigate to string? (can just use a data uri maybe?) // custom scheme handlers? // navigation callbacks to prohibit certain things or move links to new window etc? } version(cef) { //import core.sync.semaphore; //__gshared Semaphore semaphore; /+ Finds the WebViewWidget associated with the given browser, then runs the given code in the gui thread on it. +/ void runOnWebView(RC!cef_browser_t browser, void delegate(WebViewWidget) dg) nothrow { auto wh = cast(NativeWindowHandle) browser.get_host.get_window_handle; runInGuiThreadAsync({ if(auto wvp = wh in WebViewWidget.browserMapping) { dg(*wvp); } else { //writeln("not found ", wh, WebViewWidget.browserMapping); } }); } class MiniguiCefLifeSpanHandler : CEF!cef_life_span_handler_t { 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*) { return 0; } override void on_after_created(RC!cef_browser_t browser) { auto handle = cast(NativeWindowHandle) browser.get_host().get_window_handle(); auto ptr = browser.passable; // this adds to the refcount until it gets inside // the only reliable key (at least as far as i can tell) is the window handle // so gonna look that up and do the sync mapping that way. runInGuiThreadAsync({ version(Windows) { auto parent = GetParent(handle); } else static if(UsingSimpledisplayX11) { import arsd.simpledisplay : Window; Window root; Window parent; uint c = 0; auto display = XDisplayConnection.get; Window* children; XQueryTree(display, handle, &root, &parent, &children, &c); XFree(children); } else static assert(0); if(auto wvp = parent in WebViewWidget.mapping) { auto wv = *wvp; wv.browserWindow = handle; wv.browserHandle = RC!cef_browser_t(ptr); wv.registerMovementAdditionalWork(); WebViewWidget.browserMapping[handle] = wv; } else assert(0); }); } override int do_close(RC!cef_browser_t browser) { return 0; } override void on_before_close(RC!cef_browser_t browser) { /+ import std.stdio; debug writeln("notify"); try semaphore.notify; catch(Exception e) { assert(0); } +/ } } class MiniguiLoadHandler : CEF!cef_load_handler_t { override void on_loading_state_change(RC!(cef_browser_t) browser, int isLoading, int canGoBack, int canGoForward) { /+ browser.runOnWebView((WebViewWidget wvw) { wvw.parentWindow.win.title = wvw.browserHandle.get_main_frame.get_url.toGCAndFree; }); +/ } override void on_load_start(RC!(cef_browser_t), RC!(cef_frame_t), cef_transition_type_t) { } 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)*) { } override void on_load_end(RC!(cef_browser_t), RC!(cef_frame_t), int) { } } class MiniguiDialogHandler : CEF!cef_dialog_handler_t { 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) { try { auto ptr = callback.passable(); runInGuiThreadAsync({ getOpenFileName((string name) { auto callback = RC!cef_file_dialog_callback_t(ptr); auto list = libcef.string_list_alloc(); auto item = cef_string_t(name); libcef.string_list_append(list, &item); callback.cont(selected_accept_filter, list); }, null, null, () { auto callback = RC!cef_file_dialog_callback_t(ptr); callback.cancel(); }); }); } catch(Exception e) {} return 1; } } class MiniguiDisplayHandler : CEF!cef_display_handler_t { override void on_address_change(RC!(cef_browser_t) browser, RC!(cef_frame_t), const(cef_string_utf16_t)* address) { auto url = address.toGC; browser.runOnWebView((wv) { wv.url = url; }); } override void on_title_change(RC!(cef_browser_t) browser, const(cef_string_utf16_t)* title) { auto t = title.toGC; browser.runOnWebView((wv) { wv.title = t; }); } override void on_favicon_urlchange(RC!(cef_browser_t) browser, cef_string_list_t) { } override void on_fullscreen_mode_change(RC!(cef_browser_t) browser, int) { } override int on_tooltip(RC!(cef_browser_t) browser, cef_string_utf16_t*) { return 0; } override void on_status_message(RC!(cef_browser_t) browser, const(cef_string_utf16_t)* msg) { auto status = msg.toGC; browser.runOnWebView((wv) { wv.status = status; }); } override void on_loading_progress_change(RC!(cef_browser_t) browser, double progress) { // progress is from 0.0 to 1.0 browser.runOnWebView((wv) { wv.loadingProgress = cast(int) (progress * 100); }); } override int on_console_message(RC!(cef_browser_t), cef_log_severity_t, const(cef_string_utf16_t)*, const(cef_string_utf16_t)*, int) { return 0; // 1 means to suppress it being automatically output } override int on_auto_resize(RC!(cef_browser_t), const(cef_size_t)*) { return 0; } override int on_cursor_change(RC!(cef_browser_t), cef_cursor_handle_t, cef_cursor_type_t, const(cef_cursor_info_t)*) { return 0; } } class MiniguiCefClient : CEF!cef_client_t { MiniguiCefLifeSpanHandler lsh; MiniguiLoadHandler loadHandler; MiniguiDialogHandler dialogHandler; MiniguiDisplayHandler displayHandler; this() { lsh = new MiniguiCefLifeSpanHandler(); loadHandler = new MiniguiLoadHandler(); dialogHandler = new MiniguiDialogHandler(); displayHandler = new MiniguiDisplayHandler(); } override cef_audio_handler_t* get_audio_handler() { return null; } override cef_context_menu_handler_t* get_context_menu_handler() { return null; } override cef_dialog_handler_t* get_dialog_handler() { return dialogHandler.returnable; } override cef_display_handler_t* get_display_handler() { return displayHandler.returnable; } override cef_download_handler_t* get_download_handler() { return null; } override cef_drag_handler_t* get_drag_handler() { return null; } override cef_find_handler_t* get_find_handler() { return null; } override cef_focus_handler_t* get_focus_handler() { return null; } override cef_jsdialog_handler_t* get_jsdialog_handler() { // needed for alert etc. return null; } override cef_keyboard_handler_t* get_keyboard_handler() { // this can handle keyboard shortcuts etc return null; } override cef_life_span_handler_t* get_life_span_handler() { return lsh.returnable; } override cef_load_handler_t* get_load_handler() { return loadHandler.returnable; } override cef_render_handler_t* get_render_handler() { // this thing might work for an off-screen thing // like to an image or to a video stream maybe return null; } override cef_request_handler_t* get_request_handler() { return null; } override int on_process_message_received(RC!cef_browser_t, RC!cef_frame_t, cef_process_id_t, RC!cef_process_message_t) { return 0; // return 1 if you can actually handle the message } override cef_frame_handler_t* get_frame_handler() nothrow { return null; } override cef_print_handler_t* get_print_handler() nothrow { return null; } } }