1 /++
2 	A webview (based on [arsd.webview]) for minigui.
3 
4 	For now at least, to use this, you MUST have a [WebViewApp] in scope in main for the duration of your gui application.
5 
6 	Warning: CEF spams the current directory with a bunch of files and directories. You might want to run your program in a dedicated location.
7 
8 	History:
9 		Added November 5, 2021. NOT YET STABLE.
10 
11 	Examples:
12 	---
13 	/+ dub.sdl:
14 		name "web"
15 		dependency "arsd-official:minigui-webview" version="*"
16 	+/
17 
18 	import arsd.minigui;
19 	import arsd.minigui_addons.webview;
20 
21 	void main() {
22 		auto app = WebViewApp(null);
23 		auto window = new Window;
24 		auto webview = new WebViewWidget("http://dlang.org/", window);
25 		window.loop;
26 	}
27 	---
28 +/
29 module minigui_addons.webview;
30 // FIXME: i think i can download the cef automatically if needed.
31 
32 version(linux)
33 	version=cef;
34 version(Windows)
35 	version=wv2;
36 
37 /+
38 	SPA mode: put favicon on top level window, no other user controls at top level, links to different domains always open in new window.
39 +/
40 
41 // FIXME: look in /opt/cef for the dll and the locales
42 
43 import arsd.minigui;
44 import arsd.webview;
45 
46 version(wv2) {
47 	alias WebViewWidget = WebViewWidget_WV2;
48 	alias WebViewApp = Wv2App;
49 } else version(cef) {
50 	alias WebViewWidget = WebViewWidget_CEF;
51 	alias WebViewApp = CefApp;
52 } else static assert(0, "no webview available");
53 
54 class WebViewWidgetBase : NestedChildWindowWidget {
55 	protected SimpleWindow containerWindow;
56 
57 	protected this(Widget parent) {
58 		containerWindow = new SimpleWindow(640, 480, null, OpenGlOptions.no, Resizability.allowResizing, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent));
59 
60 		super(containerWindow, parent);
61 	}
62 
63 	mixin Observable!(string, "title");
64 	mixin Observable!(string, "url");
65 	mixin Observable!(string, "status");
66 
67 	// not implemented on WV2
68 	mixin Observable!(int, "loadingProgress");
69 
70 	// not implemented on WV2
71 	mixin Observable!(string, "favicon_url");
72 	mixin Observable!(MemoryImage, "favicon"); // please note it can be changed to null!
73 
74 	abstract void refresh();
75 	abstract void back();
76 	abstract void forward();
77 	abstract void stop();
78 
79 	abstract void navigate(string url);
80 
81 	// the url and line are for error reporting purposes. They might be ignored.
82 	// FIXME: add a callback with the reply. this can send a message from the js thread in cef and just ExecuteScript inWV2
83 	// FIXME: add AddScriptToExecuteOnDocumentCreated for cef....
84 	abstract void executeJavascript(string code, string url = null, int line = 0);
85 	// for injecting stuff into the context
86 	// abstract void executeJavascriptBeforeEachLoad(string code);
87 
88 	abstract void showDevTools();
89 
90 	/++
91 		Your communication consists of running Javascript and sending string messages back and forth,
92 		kinda similar to your communication with a web server.
93 	+/
94 	// these form your communcation channel between the web view and the native world
95 	// abstract void sendMessageToHost(string json);
96 	// void delegate(string json) receiveMessageFromHost;
97 
98 	/+
99 		I also need a url filter
100 	+/
101 
102 	// this is implemented as a do-nothing in the NestedChildWindowWidget base
103 	// but you will almost certainly need to override it in implementations.
104 	// abstract void registerMovementAdditionalWork();
105 }
106 
107 // AddScriptToExecuteOnDocumentCreated
108 
109 
110 
111 version(wv2)
112 class WebViewWidget_WV2 : WebViewWidgetBase {
113 	private RC!ICoreWebView2 webview_window;
114 	private RC!ICoreWebView2Environment webview_env;
115 	private RC!ICoreWebView2Controller controller;
116 
117 	private bool initialized;
118 
119 	this(string url, void delegate(scope OpenNewWindowParams) openNewWindow, BrowserSettings settings, Widget parent) {
120 		// FIXME: openNewWindow
121 		super(parent);
122 		// that ctor sets containerWindow
123 
124 		Wv2App.useEnvironment((env) {
125 			env.CreateCoreWebView2Controller(containerWindow.impl.hwnd,
126 				callback!(ICoreWebView2CreateCoreWebView2ControllerCompletedHandler)(delegate(error, controller_raw) {
127 					if(error || controller_raw is null)
128 						return error;
129 
130 					// need to keep this beyond the callback or we're doomed.
131 					controller = RC!ICoreWebView2Controller(controller_raw);
132 
133 					webview_window = controller.CoreWebView2;
134 
135 					webview_window.add_DocumentTitleChanged((sender, args) {
136 						this.title = toGC(&sender.get_DocumentTitle);
137 						return S_OK;
138 					});
139 
140 					// add_HistoryChanged
141 					// that's where CanGoBack and CanGoForward can be rechecked.
142 
143 					RC!ICoreWebView2Settings Settings = webview_window.Settings;
144 					Settings.IsScriptEnabled = TRUE;
145 					Settings.AreDefaultScriptDialogsEnabled = TRUE;
146 					Settings.IsWebMessageEnabled = TRUE;
147 
148 
149 					auto ert = webview_window.add_NavigationStarting(
150 						delegate (sender, args) {
151 							this.url = toGC(&args.get_Uri);
152 							return S_OK;
153 						});
154 
155 					RECT bounds;
156 					GetClientRect(containerWindow.impl.hwnd, &bounds);
157 					controller.Bounds = bounds;
158 					//error = webview_window.Navigate("http://arsdnet.net/test.html"w.ptr);
159 					//error = webview_window.NavigateToString("<html><body>Hello</body></html>"w.ptr);
160 					//error = webview_window.Navigate("http://192.168.1.10/"w.ptr);
161 
162 					WCharzBuffer bfr = WCharzBuffer(url);
163 					webview_window.Navigate(bfr.ptr);
164 
165 					controller.IsVisible = true;
166 
167 					initialized = true;
168 
169 					return S_OK;
170 				}));
171 			});
172 	}
173 
174 	override void registerMovementAdditionalWork() {
175 		if(initialized) {
176 			RECT bounds;
177 			GetClientRect(containerWindow.impl.hwnd, &bounds);
178 			controller.Bounds = bounds;
179 
180 			controller.NotifyParentWindowPositionChanged();
181 		}
182 	}
183 
184 	override void refresh() {
185 		if(!initialized) return;
186 		webview_window.Reload();
187 	}
188 	override void back() {
189 		if(!initialized) return;
190 		webview_window.GoBack();
191 	}
192 	override void forward() {
193 		if(!initialized) return;
194 		webview_window.GoForward();
195 	}
196 	override void stop() {
197 		if(!initialized) return;
198 		webview_window.Stop();
199 	}
200 
201 	override void navigate(string url) {
202 		if(!initialized) return;
203 		import std.utf;
204 		auto error = webview_window.Navigate(url.toUTF16z);
205 	}
206 
207 	// the url and line are for error reporting purposes
208 	override void executeJavascript(string code, string url = null, int line = 0) {
209 		if(!initialized) return;
210 		import std.utf;
211 		webview_window.ExecuteScript(code.toUTF16z, null);
212 	}
213 
214 	override void showDevTools() {
215 		if(!initialized) return;
216 		webview_window.OpenDevToolsWindow();
217 	}
218 }
219 
220 /++
221 	The openInNewWindow delegate is given these params.
222 
223 	To accept the new window, call
224 
225 	params.accept(parent_widget);
226 
227 	Please note, you can force it to replace the current tab
228 	by just navigating your current thing to the given url instead
229 	of accepting it.
230 
231 	If you accept with a null widget, it will create a new window
232 	but then return null, since the new window is managed by the
233 	underlying webview instead of by minigui.
234 
235 	If you do not call accept, the pop up will be blocked.
236 
237 	accept returns an instance to the newly created widget, which will
238 	be a parent to the widget you passed.
239 
240 	accept will be called from the gui thread and it MUST not call into
241 	any other webview methods. It should only create windows/widgets
242 	and set event handlers etc.
243 
244 	You MUST not escape references to anything in this structure. It
245 	is entirely strictly temporary!
246 +/
247 struct OpenNewWindowParams {
248 	string url;
249 	WebViewWidget delegate(Widget parent, BrowserSettings settings = BrowserSettings.init) accept;
250 }
251 
252 /++
253 	Represents a browser setting that can be left default or specifically turned on or off.
254 +/
255 struct SettingValue {
256 	private byte value = -1;
257 
258 	/++
259 		Set it with `= true` or `= false`.
260 	+/
261 	void opAssign(bool enable) {
262 		value = enable ? 1 : 0;
263 	}
264 
265 	/++
266 		And this resets it to the default value.
267 	+/
268 	void setDefault() {
269 		value = -1;
270 	}
271 
272 	/// If isDefault, it will use the default setting from the browser. Else, the getValue return value will be used. getValue is invalid if !isDefault.
273 	bool isDefault() {
274 		return value == -1;
275 	}
276 
277 	/// ditto
278 	bool getValue() {
279 		return value == 1;
280 	}
281 }
282 
283 /++
284 	Defines settings for a browser widget. Not all backends will respect all settings.
285 
286 	The order of members of this struct may change at any time. Refer to its members by
287 	name.
288 +/
289 struct BrowserSettings {
290 	/// This is just disabling the automatic positional constructor, since that is not stable here.
291 	this(typeof(null)) {}
292 
293 	string standardFontFamily;
294 	string fixedFontFamily;
295 	string serifFontFamily;
296 	string sansSerifFontFamily;
297 	string cursiveFontFamily;
298 	string fantasyFontFamily;
299 
300 	int defaultFontSize;
301 	int defaultFixedFontSize;
302 	int minimumFontSize;
303 	//int minimumLogicalFontSize;
304 
305 	SettingValue remoteFontsEnabled;
306 	SettingValue javascriptEnabled;
307 	SettingValue imagesEnabled;
308 	SettingValue clipboardAccessEnabled;
309 	SettingValue localStorageEnabled;
310 
311 	version(cef)
312 	private void set(cef_browser_settings_t* browser_settings) {
313 		alias settings = this;
314 		if(settings.standardFontFamily)
315 			browser_settings.standard_font_family = cef_string_t(settings.standardFontFamily);
316 		if(settings.fixedFontFamily)
317 			browser_settings.fixed_font_family = cef_string_t(settings.fixedFontFamily);
318 		if(settings.serifFontFamily)
319 			browser_settings.serif_font_family = cef_string_t(settings.serifFontFamily);
320 		if(settings.sansSerifFontFamily)
321 			browser_settings.sans_serif_font_family = cef_string_t(settings.sansSerifFontFamily);
322 		if(settings.cursiveFontFamily)
323 			browser_settings.cursive_font_family = cef_string_t(settings.cursiveFontFamily);
324 		if(settings.fantasyFontFamily)
325 			browser_settings.fantasy_font_family = cef_string_t(settings.fantasyFontFamily);
326 		if(settings.defaultFontSize)
327 			browser_settings.default_font_size = settings.defaultFontSize;
328 		if(settings.defaultFixedFontSize)
329 			browser_settings.default_fixed_font_size = settings.defaultFixedFontSize;
330 		if(settings.minimumFontSize)
331 			browser_settings.minimum_font_size = settings.minimumFontSize;
332 
333 		if(!settings.remoteFontsEnabled.isDefault())
334 			browser_settings.remote_fonts = settings.remoteFontsEnabled.getValue() ? cef_state_t.STATE_ENABLED : cef_state_t.STATE_DISABLED;
335 		if(!settings.javascriptEnabled.isDefault())
336 			browser_settings.javascript = settings.javascriptEnabled.getValue() ? cef_state_t.STATE_ENABLED : cef_state_t.STATE_DISABLED;
337 		if(!settings.imagesEnabled.isDefault())
338 			browser_settings.image_loading = settings.imagesEnabled.getValue() ? cef_state_t.STATE_ENABLED : cef_state_t.STATE_DISABLED;
339 		if(!settings.clipboardAccessEnabled.isDefault())
340 			browser_settings.javascript_access_clipboard = settings.clipboardAccessEnabled.getValue() ? cef_state_t.STATE_ENABLED : cef_state_t.STATE_DISABLED;
341 		if(!settings.localStorageEnabled.isDefault())
342 			browser_settings.local_storage = settings.localStorageEnabled.getValue() ? cef_state_t.STATE_ENABLED : cef_state_t.STATE_DISABLED;
343 
344 	}
345 }
346 
347 version(cef)
348 class WebViewWidget_CEF : WebViewWidgetBase {
349 	/++
350 		Create a webview that does not support opening links in new windows and uses default settings to load the given url.
351 	+/
352 	this(string url, Widget parent) {
353 		this(url, null, BrowserSettings.init, parent);
354 	}
355 
356 	/++
357 		Full-featured constructor.
358 	+/
359 	this(string url, void delegate(scope OpenNewWindowParams) openNewWindow, BrowserSettings settings, Widget parent) {
360 		//semaphore = new Semaphore;
361 		assert(CefApp.active);
362 
363 		this(new MiniguiCefClient(openNewWindow), parent, false);
364 
365 		cef_window_info_t window_info;
366 		window_info.parent_window = containerWindow.nativeWindowHandle;
367 
368 		cef_string_t cef_url = cef_string_t(url);//"http://arsdnet.net/test.html");
369 
370 		cef_browser_settings_t browser_settings;
371 		browser_settings.size = cef_browser_settings_t.sizeof;
372 
373 		settings.set(&browser_settings);
374 
375 		auto got = libcef.browser_host_create_browser(&window_info, client.passable, &cef_url, &browser_settings, null, null);
376 	}
377 
378 	/+
379 	~this() {
380 		import core.stdc.stdio;
381 		import core.memory;
382 		printf("CLEANUP %s\n", GC.inFinalizer ? "GC".ptr : "destroy".ptr);
383 	}
384 	+/
385 
386 	override void dispose() {
387 		// sdpyPrintDebugString("closed");
388 		// the window is already gone so too late to do this really....
389 		// if(browserHandle) browserHandle.get_host.close_browser(true);
390 
391 		// sdpyPrintDebugString("DISPOSE");
392 
393 		if(win && win.nativeWindowHandle())
394 			mapping.remove(win.nativeWindowHandle());
395 		if(browserWindow)
396 			browserMapping.remove(browserWindow);
397 
398 		.destroy(this); // but this is ok to do some memory management cleanup
399 	}
400 
401 	private this(MiniguiCefClient client, Widget parent, bool isDevTools) {
402 		super(parent);
403 
404 		this.client = client;
405 
406 		flushGui();
407 
408 		mapping[containerWindow.nativeWindowHandle()] = this;
409 
410 		this.addEventListener(delegate(KeyDownEvent ke) {
411 			if(ke.key == Key.Tab)
412 				ke.preventDefault();
413 		});
414 
415 		this.addEventListener((FocusEvent fe) {
416 			if(!browserHandle) return;
417 
418 			XFocusChangeEvent ev;
419 			ev.type = arsd.simpledisplay.EventType.FocusIn;
420 			ev.display = XDisplayConnection.get;
421 			ev.window = ozone;
422 			ev.mode = NotifyModes.NotifyNormal;
423 			ev.detail = NotifyDetail.NotifyVirtual;
424 
425 			trapXErrors( {
426 				XSendEvent(XDisplayConnection.get, ozone, false, 0, cast(XEvent*) &ev);
427 			});
428 
429 			// this also works if the message is buggy and it avoids weirdness from raising window etc
430 			//executeJavascript("if(window.__arsdPreviouslyFocusedNode) window.__arsdPreviouslyFocusedNode.focus(); window.dispatchEvent(new FocusEvent(\"focus\"));");
431 		});
432 		this.addEventListener((BlurEvent be) {
433 			if(!browserHandle) return;
434 
435 			XFocusChangeEvent ev;
436 			ev.type = arsd.simpledisplay.EventType.FocusOut;
437 			ev.display = XDisplayConnection.get;
438 			ev.window = ozone;
439 			ev.mode = NotifyModes.NotifyNormal;
440 			ev.detail = NotifyDetail.NotifyNonlinearVirtual;
441 
442 			trapXErrors( {
443 				XSendEvent(XDisplayConnection.get, ozone, false, 0, cast(XEvent*) &ev);
444 			});
445 
446 			//executeJavascript("if(document.activeElement) { window.__arsdPreviouslyFocusedNode = document.activeElement; document.activeElement.blur(); } window.dispatchEvent(new FocusEvent(\"blur\"));");
447 		});
448 
449 		bool closeAttempted = false;
450 
451 		if(isDevTools)
452 		this.parentWindow.addEventListener((scope ClosingEvent ce) {
453 			this.parentWindow.hide();
454 			ce.preventDefault();
455 		});
456 		else
457 		this.parentWindow.addEventListener((scope ClosingEvent ce) {
458 			if(devTools)
459 				devTools.close();
460 			if(!closeAttempted && browserHandle) {
461 				browserHandle.get_host.close_browser(true);
462 				ce.preventDefault();
463 				// sdpyPrintDebugString("closing");
464 			}
465 			closeAttempted = true;
466 		});
467 	}
468 
469 	private MiniguiCefClient client;
470 
471 	override void registerMovementAdditionalWork() {
472 		if(browserWindow) {
473 			static if(UsingSimpledisplayX11)
474 				XResizeWindow(XDisplayConnection.get, browserWindow, width, height);
475 			// FIXME: do for Windows too
476 		}
477 	}
478 
479 	SimpleWindow browserWindowWrapped;
480 	override SimpleWindow focusableWindow() {
481 		if(browserWindowWrapped is null && browserWindow)
482 			browserWindowWrapped = new SimpleWindow(browserWindow);
483 		return browserWindowWrapped;
484 	}
485 
486 	private NativeWindowHandle browserWindow;
487 	private NativeWindowHandle ozone;
488 	private RC!cef_browser_t browserHandle;
489 
490 	private static WebViewWidget[NativeWindowHandle] mapping;
491 	private static WebViewWidget[NativeWindowHandle] browserMapping;
492 
493 	override void refresh() { if(browserHandle) browserHandle.reload(); }
494 	override void back() { if(browserHandle) browserHandle.go_back(); }
495 	override void forward() { if(browserHandle) browserHandle.go_forward(); }
496 	override void stop() { if(browserHandle) browserHandle.stop_load(); }
497 
498 	override void navigate(string url) {
499 		if(!browserHandle) return;
500 		auto s = cef_string_t(url);
501 		browserHandle.get_main_frame.load_url(&s);
502 	}
503 
504 	// the url and line are for error reporting purposes
505 	override void executeJavascript(string code, string url = null, int line = 0) {
506 		if(!browserHandle) return;
507 
508 		auto c = cef_string_t(code);
509 		auto u = cef_string_t(url);
510 		browserHandle.get_main_frame.execute_java_script(&c, &u, line);
511 	}
512 
513 	private Window devTools;
514 	override void showDevTools() {
515 		if(!browserHandle) return;
516 
517 		if(devTools is null) {
518 			auto host = browserHandle.get_host;
519 
520 			if(host.has_dev_tools()) {
521 				host.close_dev_tools();
522 				return;
523 			}
524 
525 			cef_window_info_t windowinfo;
526 			version(linux) {
527 				auto sw = new Window("DevTools");
528 				//sw.win.beingOpenKeepsAppOpen = false;
529 				devTools = sw;
530 
531 				auto wv = new WebViewWidget_CEF(client, sw, true);
532 
533 				sw.show();
534 
535 				windowinfo.parent_window = wv.containerWindow.nativeWindowHandle;
536 			}
537 			host.show_dev_tools(&windowinfo, client.passable, null /* settings */, null /* inspect element at coordinates */);
538 		} else {
539 			if(devTools.hidden)
540 				devTools.show();
541 			else
542 				devTools.hide();
543 		}
544 	}
545 
546 	// FYI the cef browser host also allows things like custom spelling dictionaries and getting navigation entries.
547 
548 	// JS on init?
549 	// JS bindings?
550 	// user styles?
551 	// navigate to string? (can just use a data uri maybe?)
552 	// custom scheme handlers?
553 
554 	// navigation callbacks to prohibit certain things or move links to new window etc?
555 }
556 
557 version(cef) {
558 
559 	//import core.sync.semaphore;
560 	//__gshared Semaphore semaphore;
561 
562 	/+
563 		Finds the WebViewWidget associated with the given browser, then runs the given code in the gui thread on it.
564 	+/
565 	void runOnWebView(RC!cef_browser_t browser, void delegate(WebViewWidget) dg) nothrow {
566 		auto wh = cast(NativeWindowHandle) browser.get_host.get_window_handle;
567 
568 		import core.thread;
569 		try { thread_attachThis(); } catch(Exception e) {}
570 
571 		runInGuiThreadAsync({
572 			if(auto wvp = wh in WebViewWidget.browserMapping) {
573 				dg(*wvp);
574 			} else {
575 				//writeln("not found ", wh, WebViewWidget.browserMapping);
576 			}
577 		});
578 	}
579 
580 	class MiniguiCefLifeSpanHandler : CEF!cef_life_span_handler_t {
581 		private MiniguiCefClient client;
582 		this(MiniguiCefClient client) {
583 			this.client = client;
584 		}
585 
586 		override int on_before_popup(
587 			RC!cef_browser_t browser,
588 			RC!cef_frame_t frame,
589 			const(cef_string_t)* target_url,
590 			const(cef_string_t)* target_frame_name,
591 			cef_window_open_disposition_t target_disposition,
592 			int user_gesture,
593 			const(cef_popup_features_t)* popupFeatures,
594 			cef_window_info_t* windowInfo,
595 			cef_client_t** client,
596 			cef_browser_settings_t* browser_settings,
597 			cef_dictionary_value_t** extra_info,
598 			int* no_javascript_access
599 		) {
600 		sdpyPrintDebugString("on_before_popup");
601 			if(this.client.openNewWindow is null)
602 				return 1; // new windows disabled
603 
604 			try {
605 				int ret;
606 
607 				import core.thread;
608 				try { thread_attachThis(); } catch(Exception e) {}
609 
610 				// FIXME: change settings here
611 
612 				runInGuiThread({
613 					ret = 1;
614 					scope WebViewWidget delegate(Widget, BrowserSettings) accept = (parent, passed_settings) {
615 						ret = 0;
616 						if(parent !is null) {
617 							auto widget = new WebViewWidget_CEF(this.client, parent, false);
618 							(*windowInfo).parent_window = widget.containerWindow.nativeWindowHandle;
619 
620 							passed_settings.set(browser_settings);
621 
622 							return widget;
623 						}
624 						return null;
625 					};
626 					this.client.openNewWindow(OpenNewWindowParams(target_url.toGC, accept));
627 					return;
628 				});
629 
630 				return ret;
631 			} catch(Exception e) {
632 				return 1;
633 			}
634 			/+
635 			if(!user_gesture)
636 				return 1; // if not created by the user, cancel it; basic popup blocking
637 			+/
638 		}
639 		override void on_after_created(RC!cef_browser_t browser) {
640 			auto handle = cast(NativeWindowHandle) browser.get_host().get_window_handle();
641 			auto ptr = browser.passable; // this adds to the refcount until it gets inside
642 
643 			import core.thread;
644 			try { thread_attachThis(); } catch(Exception e) {}
645 
646 			// the only reliable key (at least as far as i can tell) is the window handle
647 			// so gonna look that up and do the sync mapping that way.
648 			runInGuiThreadAsync({
649 				version(Windows) {
650 					auto parent = GetParent(handle);
651 				} else static if(UsingSimpledisplayX11) {
652 					import arsd.simpledisplay : Window;
653 					Window root;
654 					Window parent;
655 					Window ozone;
656 					uint c = 0;
657 					auto display = XDisplayConnection.get;
658 					Window* children;
659 					XQueryTree(display, handle, &root, &parent, &children, &c);
660 					if(c == 1)
661 						ozone = children[0];
662 					XFree(children);
663 				} else static assert(0);
664 
665 				if(auto wvp = parent in WebViewWidget.mapping) {
666 					auto wv = *wvp;
667 					wv.browserWindow = handle;
668 					wv.browserHandle = RC!cef_browser_t(ptr);
669 					wv.ozone = ozone ? ozone : handle;
670 
671 					wv.browserWindowWrapped = new SimpleWindow(wv.ozone);
672 					/+
673 					XSelectInput(XDisplayConnection.get, handle, EventMask.FocusChangeMask);
674 					
675 					wv.browserWindowWrapped.onFocusChange = (bool got) {
676 						import std.format;
677 						sdpyPrintDebugString(format("focus change %s %x", got, wv.browserWindowWrapped.impl.window));
678 					};
679 					+/
680 
681 					wv.registerMovementAdditionalWork();
682 
683 					WebViewWidget.browserMapping[handle] = wv;
684 				} else assert(0);
685 			});
686 		}
687 		override int do_close(RC!cef_browser_t browser) {
688 			browser.runOnWebView((wv) {
689 				auto bce = new BrowserClosedEvent(wv);
690 				bce.dispatch();
691 			});
692 			return 1;
693 		}
694 		override void on_before_close(RC!cef_browser_t browser) {
695 			/+
696 			import std.stdio; debug writeln("notify");
697 			try
698 			semaphore.notify;
699 			catch(Exception e) { assert(0); }
700 			+/
701 		}
702 	}
703 
704 	class MiniguiLoadHandler : CEF!cef_load_handler_t {
705 		override void on_loading_state_change(RC!(cef_browser_t) browser, int isLoading, int canGoBack, int canGoForward) {
706 			/+
707 			browser.runOnWebView((WebViewWidget wvw) {
708 				wvw.parentWindow.win.title = wvw.browserHandle.get_main_frame.get_url.toGCAndFree;
709 			});
710 			+/
711 		}
712 		override void on_load_start(RC!(cef_browser_t), RC!(cef_frame_t), cef_transition_type_t) {
713 		}
714 		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)*) {
715 		}
716 		override void on_load_end(RC!(cef_browser_t), RC!(cef_frame_t), int) {
717 		}
718 	}
719 
720 	class MiniguiDialogHandler : CEF!cef_dialog_handler_t {
721 
722 		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) {
723 			try {
724 				auto ptr = callback.passable();
725 				browser.runOnWebView((wv) {
726 					getOpenFileName((string name) {
727 						auto callback = RC!cef_file_dialog_callback_t(ptr);
728 						auto list = libcef.string_list_alloc();
729 						auto item = cef_string_t(name);
730 						libcef.string_list_append(list, &item);
731 						callback.cont(selected_accept_filter, list);
732 					}, null, null, () {
733 						auto callback = RC!cef_file_dialog_callback_t(ptr);
734 						callback.cancel();
735 					});
736 				});
737 			} catch(Exception e) {}
738 
739 			return 1;
740 		}
741 	}
742 
743 	class MiniguiDownloadHandler : CEF!cef_download_handler_t {
744 		override void on_before_download(
745 			RC!cef_browser_t browser,
746 			RC!cef_download_item_t download_item,
747 			const(cef_string_t)* suggested_name,
748 			RC!cef_before_download_callback_t callback
749 		) nothrow
750 		{
751 			// FIXME: different filename and check if exists for overwrite etc
752 			auto fn = cef_string_t(cast(wstring)("/home/me/Downloads/"w ~ suggested_name.str[0..suggested_name.length]));
753 			sdpyPrintDebugString(fn.toGC);
754 			callback.cont(&fn, false);
755 		}
756 
757 		override void on_download_updated(
758 			RC!cef_browser_t browser,
759 			RC!cef_download_item_t download_item,
760 			RC!cef_download_item_callback_t cancel
761 		) nothrow
762 		{
763 			sdpyPrintDebugString(download_item.get_percent_complete());
764 			// FIXME
765 		}
766 	}
767 
768 	class MiniguiKeyboardHandler : CEF!cef_keyboard_handler_t {
769 		override int on_pre_key_event(
770 			RC!(cef_browser_t) browser,
771 			const(cef_key_event_t)* event,
772 			XEvent* osEvent,
773 			int* isShortcut
774 		) nothrow {
775 		/+
776 			sdpyPrintDebugString("---pre---");
777 			sdpyPrintDebugString(event.focus_on_editable_field);
778 			sdpyPrintDebugString(event.windows_key_code);
779 			sdpyPrintDebugString(event.modifiers);
780 			sdpyPrintDebugString(event.unmodified_character);
781 		+/
782 			//*isShortcut = 1;
783 			return 0; // 1 if handled, which cancels sending it to browser
784 		}
785 
786 		override int on_key_event(
787 			RC!(cef_browser_t) browser,
788 			const(cef_key_event_t)* event,
789 			XEvent* osEvent
790 		) nothrow {
791 		/+
792 			sdpyPrintDebugString("---key---");
793 			sdpyPrintDebugString(event.focus_on_editable_field);
794 			sdpyPrintDebugString(event.windows_key_code);
795 			sdpyPrintDebugString(event.modifiers);
796 		+/
797 			return 0; // 1 if handled
798 		}
799 	}
800 
801 	class MiniguiDisplayHandler : CEF!cef_display_handler_t {
802 		override void on_address_change(RC!(cef_browser_t) browser, RC!(cef_frame_t), const(cef_string_utf16_t)* address) {
803 			auto url = address.toGC;
804 			browser.runOnWebView((wv) {
805 				wv.url = url;
806 			});
807 		}
808 		override void on_title_change(RC!(cef_browser_t) browser, const(cef_string_utf16_t)* title) {
809 			auto t = title.toGC;
810 			browser.runOnWebView((wv) {
811 				wv.title = t;
812 			});
813 		}
814 		override void on_favicon_urlchange(RC!(cef_browser_t) browser, cef_string_list_t urls) {
815 			string url;
816 			auto size = libcef.string_list_size(urls);
817 			if(size > 0) {
818 				cef_string_t str;
819 				libcef.string_list_value(urls, 0, &str);
820 				url = str.toGC;
821 
822 				static class Thing : CEF!cef_download_image_callback_t {
823 					RC!cef_browser_t browserHandle;
824 					this(RC!cef_browser_t browser) nothrow {
825 						this.browserHandle = browser;
826 					}
827 					override void on_download_image_finished(const(cef_string_t)* image_url, int http_status_code, RC!cef_image_t image) nothrow {
828 						int width;
829 						int height;
830 						if(image.getRawPointer is null) {
831 							browserHandle.runOnWebView((wv) {
832 								wv.favicon = null;
833 							});
834 							return;
835 						}
836 
837 						auto data = image.get_as_bitmap(1.0, cef_color_type_t.CEF_COLOR_TYPE_RGBA_8888, cef_alpha_type_t.CEF_ALPHA_TYPE_POSTMULTIPLIED, &width, &height);
838 
839 						if(data.getRawPointer is null || width == 0 || height == 0) {
840 							browserHandle.runOnWebView((wv) {
841 								wv.favicon = null;
842 							});
843 						} else {
844 							auto s = data.get_size();
845 							auto buffer = new ubyte[](s);
846 							auto got = data.get_data(buffer.ptr, buffer.length, 0);
847 							auto slice = buffer[0 .. got];
848 
849 							auto img = new TrueColorImage (width, height, slice);
850 
851 							browserHandle.runOnWebView((wv) {
852 								wv.favicon = img;
853 							});
854 						}
855 					}
856 				}
857 
858 				if(url.length) {
859 					auto callback = new Thing(browser);
860 
861 					browser.get_host().download_image(&str, true, 16, 0, callback.passable);
862 				} else {
863 					browser.runOnWebView((wv) {
864 						wv.favicon = null;
865 					});
866 				}
867 			}
868 
869 			browser.runOnWebView((wv) {
870 				wv.favicon_url = url;
871 			});
872 		}
873 		override void on_fullscreen_mode_change(RC!(cef_browser_t) browser, int) {
874 		}
875 		override int on_tooltip(RC!(cef_browser_t) browser, cef_string_utf16_t*) {
876 			return 0;
877 		}
878 		override void on_status_message(RC!(cef_browser_t) browser, const(cef_string_utf16_t)* msg) {
879 			auto status = msg.toGC;
880 			browser.runOnWebView((wv) {
881 				wv.status = status;
882 			});
883 		}
884 		override void on_loading_progress_change(RC!(cef_browser_t) browser, double progress) {
885 			// progress is from 0.0 to 1.0
886 			browser.runOnWebView((wv) {
887 				wv.loadingProgress = cast(int) (progress * 100);
888 			});
889 		}
890 		override int on_console_message(RC!(cef_browser_t), cef_log_severity_t, const(cef_string_utf16_t)*, const(cef_string_utf16_t)*, int) {
891 			return 0; // 1 means to suppress it being automatically output
892 		}
893 		override int on_auto_resize(RC!(cef_browser_t), const(cef_size_t)*) {
894 			return 0;
895 		}
896 		override int on_cursor_change(RC!(cef_browser_t), cef_cursor_handle_t, cef_cursor_type_t, const(cef_cursor_info_t)*) {
897 			return 0;
898 		}
899 	}
900 
901 	class MiniguiRequestHandler : CEF!cef_request_handler_t {
902 		override int on_before_browse(RC!(cef_browser_t), RC!(cef_frame_t), RC!(cef_request_t), int, int) nothrow {
903 			return 0;
904 		}
905 		override int on_open_urlfrom_tab(RC!(cef_browser_t), RC!(cef_frame_t), const(cef_string_utf16_t)*, cef_window_open_disposition_t, int) nothrow {
906 			return 0;
907 		}
908 		override cef_resource_request_handler_t* get_resource_request_handler(RC!(cef_browser_t), RC!(cef_frame_t), RC!(cef_request_t), int, int, const(cef_string_utf16_t)*, int*) nothrow {
909 			return null;
910 		}
911 		override int get_auth_credentials(RC!(cef_browser_t), const(cef_string_utf16_t)*, int, const(cef_string_utf16_t)*, int, const(cef_string_utf16_t)*, const(cef_string_utf16_t)*, RC!(cef_auth_callback_t)) nothrow {
912 			// this is for http basic auth popup.....
913 			return 0;
914 		}
915 		override int on_quota_request(RC!(cef_browser_t), const(cef_string_utf16_t)*, long, RC!(cef_callback_t)) nothrow {
916 			return 0;
917 		}
918 		override int on_certificate_error(RC!(cef_browser_t), cef_errorcode_t, const(cef_string_utf16_t)*, RC!(cef_sslinfo_t), RC!(cef_callback_t)) nothrow {
919 			return 0;
920 		}
921 		override int on_select_client_certificate(RC!(cef_browser_t), int, const(cef_string_utf16_t)*, int, ulong, cef_x509certificate_t**, RC!(cef_select_client_certificate_callback_t)) nothrow {
922 			return 0;
923 		}
924 		override void on_plugin_crashed(RC!(cef_browser_t), const(cef_string_utf16_t)*) nothrow {
925 
926 		}
927 		override void on_render_view_ready(RC!(cef_browser_t) p) nothrow {
928 
929 		}
930 		override void on_render_process_terminated(RC!(cef_browser_t), cef_termination_status_t) nothrow {
931 
932 		}
933 		override void on_document_available_in_main_frame(RC!(cef_browser_t) browser) nothrow {
934 			browser.runOnWebView(delegate(wv) {
935 				wv.executeJavascript("console.log('here');");
936 			});
937 
938 		}
939 	}
940 
941 	class MiniguiFocusHandler : CEF!cef_focus_handler_t {
942 		override void on_take_focus(RC!(cef_browser_t) browser, int next) nothrow {
943 			browser.runOnWebView(delegate(wv) {
944 				Widget f;
945 				if(next) {
946 					f = Window.getFirstFocusable(wv.parentWindow);
947 				} else {
948 					foreach(w; &wv.parentWindow.focusableWidgets) {
949 						if(w is wv)
950 							break;
951 						f = w;
952 					}
953 				}
954 				if(f)
955 					f.focus();
956 			});
957 		}
958 		override int on_set_focus(RC!(cef_browser_t) browser, cef_focus_source_t source) nothrow {
959 			/+
960 			browser.runOnWebView((ev) {
961 				ev.focus(); // even this can steal focus from other parts of my application!
962 			});
963 			+/
964 			//sdpyPrintDebugString("setting");
965 
966 			return 1; // otherwise, cancel because this bullshit tends to steal focus from other applications and i never, ever, ever want that to happen.
967 			// seems to happen because of race condition in it getting a focus event and then stealing the focus from the parent
968 			// even though things work fine if i always cancel except
969 			// it still keeps the decoration assuming focus though even though it doesn't have it which is kinda fucked up but meh
970 			// it also breaks its own pop up menus and drop down boxes to allow this! wtf
971 		}
972 		override void on_got_focus(RC!(cef_browser_t) browser) nothrow {
973 			browser.runOnWebView((ev) {
974 				// this sometimes steals from the app too but it is relatively acceptable
975 				// steals when i mouse in from the side of the window quickly, but still
976 				// i want the minigui state to match so i'll allow it
977 				ev.focus();
978 			});
979 		}
980 	}
981 
982 	class MiniguiCefClient : CEF!cef_client_t {
983 
984 		void delegate(scope OpenNewWindowParams) openNewWindow;
985 
986 		MiniguiCefLifeSpanHandler lsh;
987 		MiniguiLoadHandler loadHandler;
988 		MiniguiDialogHandler dialogHandler;
989 		MiniguiDisplayHandler displayHandler;
990 		MiniguiDownloadHandler downloadHandler;
991 		MiniguiKeyboardHandler keyboardHandler;
992 		MiniguiFocusHandler focusHandler;
993 		MiniguiRequestHandler requestHandler;
994 		this(void delegate(scope OpenNewWindowParams) openNewWindow) {
995 			this.openNewWindow = openNewWindow;
996 			lsh = new MiniguiCefLifeSpanHandler(this);
997 			loadHandler = new MiniguiLoadHandler();
998 			dialogHandler = new MiniguiDialogHandler();
999 			displayHandler = new MiniguiDisplayHandler();
1000 			downloadHandler = new MiniguiDownloadHandler();
1001 			keyboardHandler = new MiniguiKeyboardHandler();
1002 			focusHandler = new MiniguiFocusHandler();
1003 			requestHandler = new MiniguiRequestHandler();
1004 		}
1005 
1006 		override cef_audio_handler_t* get_audio_handler() {
1007 			return null;
1008 		}
1009 		override cef_context_menu_handler_t* get_context_menu_handler() {
1010 			return null;
1011 		}
1012 		override cef_dialog_handler_t* get_dialog_handler() {
1013 			return dialogHandler.returnable;
1014 		}
1015 		override cef_display_handler_t* get_display_handler() {
1016 			return displayHandler.returnable;
1017 		}
1018 		override cef_download_handler_t* get_download_handler() {
1019 			return downloadHandler.returnable;
1020 		}
1021 		override cef_drag_handler_t* get_drag_handler() {
1022 			return null;
1023 		}
1024 		override cef_find_handler_t* get_find_handler() {
1025 			return null;
1026 		}
1027 		override cef_focus_handler_t* get_focus_handler() {
1028 			return focusHandler.returnable;
1029 		}
1030 		override cef_jsdialog_handler_t* get_jsdialog_handler() {
1031 			// needed for alert etc.
1032 			return null;
1033 		}
1034 		override cef_keyboard_handler_t* get_keyboard_handler() {
1035 			// this can handle keyboard shortcuts etc
1036 			return keyboardHandler.returnable;
1037 		}
1038 		override cef_life_span_handler_t* get_life_span_handler() {
1039 			return lsh.returnable;
1040 		}
1041 		override cef_load_handler_t* get_load_handler() {
1042 			return loadHandler.returnable;
1043 		}
1044 		override cef_render_handler_t* get_render_handler() {
1045 			// this thing might work for an off-screen thing
1046 			// like to an image or to a video stream maybe
1047 			//
1048 			// might be useful to have it render here then send it over too for remote X sharing a process
1049 			return null;
1050 		}
1051 		override cef_request_handler_t* get_request_handler() {
1052 			return requestHandler.returnable;
1053 		}
1054 		override int on_process_message_received(RC!cef_browser_t, RC!cef_frame_t, cef_process_id_t, RC!cef_process_message_t) {
1055 			return 0; // return 1 if you can actually handle the message
1056 		}
1057 		override cef_frame_handler_t* get_frame_handler() nothrow {
1058 			return null;
1059 		}
1060 		override cef_print_handler_t* get_print_handler() nothrow {
1061 			return null;
1062 		}
1063 
1064 	}
1065 }
1066 
1067 class BrowserClosedEvent : Event {
1068 	enum EventString = "browserclosed";
1069 
1070 	this(Widget target) { super(EventString, target); }
1071 	override bool cancelable() const { return false; }
1072 }
1073 
1074 /+
1075 pragma(mangle, "_ZN12CefWindowX115FocusEv")
1076 //pragma(mangle, "_ZN3x116XProto13SetInputFocusERKNS_20SetInputFocusRequestE")
1077 extern(C++)
1078 export void _ZN12CefWindowX115FocusEv() {
1079 	sdpyPrintDebugString("OVERRIDDEN");
1080 }
1081 +/