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 }