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 		status text and favicon change notifications implemented on Windows WebView2 on December 16, 2023 (so long as the necessary api version is available, otherwise it will silently drop it).
12 
13 	Dependencies:
14 		Requires arsd.png on Windows for favicons, may require more in the future.
15 
16 	Examples:
17 	---
18 	/+ dub.sdl:
19 		name "web"
20 		dependency "arsd-official:minigui-webview" version="*"
21 	+/
22 
23 	import arsd.minigui;
24 	import arsd.minigui_addons.webview;
25 
26 	void main() {
27 		auto app = WebViewApp(null);
28 		auto window = new Window;
29 		auto webview = new WebViewWidget("http://dlang.org/", window);
30 		window.loop;
31 	}
32 	---
33 +/
34 module arsd.minigui_addons.webview;
35 // FIXME: i think i can download the cef automatically if needed.
36 
37 // want to add mute support
38 // https://learn.microsoft.com/en-us/microsoft-edge/webview2/reference/win32/icorewebview2_8?view=webview2-1.0.2210.55
39 
40 // javascript : AddScriptToExecuteOnDocumentCreated / cef_register_extension or https://bitbucket.org/chromiumembedded/cef/wiki/JavaScriptIntegration.md idk
41 // need a: post web message / on web message posted
42 
43 // and some magic reply to certain url schemes.
44 
45 // also want to make sure it can prefix http:// and such when typing in a url bar cross platform
46 
47 import arsd.core;
48 
49 version(linux)
50 	version=cef;
51 version(Windows)
52 	version=wv2;
53 
54 /+
55 	SPA mode: put favicon on top level window, no other user controls at top level, links to different domains always open in new window.
56 +/
57 
58 // FIXME: look in /opt/cef for the dll and the locales
59 
60 import arsd.minigui;
61 import arsd.webview;
62 
63 version(wv2) {
64 	alias WebViewWidget = WebViewWidget_WV2;
65 	alias WebViewApp = Wv2App;
66 } else version(cef) {
67 	alias WebViewWidget = WebViewWidget_CEF;
68 	alias WebViewApp = CefApp;
69 } else static assert(0, "no webview available");
70 
71 class WebViewWidgetBase : NestedChildWindowWidget {
72 	protected SimpleWindow containerWindow;
73 
74 	protected this(Widget parent) {
75 		containerWindow = new SimpleWindow(640, 480, null, OpenGlOptions.no, Resizability.allowResizing, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent));
76 		// import std.stdio; writefln("container window %d created", containerWindow.window);
77 
78 		super(containerWindow, parent);
79 	}
80 
81 	mixin Observable!(string, "title");
82 	mixin Observable!(string, "url");
83 	mixin Observable!(string, "status");
84 
85 	// not implemented on WV2
86 	mixin Observable!(int, "loadingProgress");
87 
88 	// not implemented on WV2
89 	mixin Observable!(string, "favicon_url");
90 	mixin Observable!(MemoryImage, "favicon"); // please note it can be changed to null!
91 
92 	abstract void refresh();
93 	abstract void back();
94 	abstract void forward();
95 	abstract void stop();
96 
97 	abstract void navigate(string url);
98 
99 	// the url and line are for error reporting purposes. They might be ignored.
100 	// FIXME: add a callback with the reply. this can send a message from the js thread in cef and just ExecuteScript inWV2
101 	// FIXME: add AddScriptToExecuteOnDocumentCreated for cef....
102 	abstract void executeJavascript(string code, string url = null, int line = 0);
103 	// for injecting stuff into the context
104 	// abstract void executeJavascriptBeforeEachLoad(string code);
105 
106 	abstract void showDevTools();
107 
108 	/++
109 		Your communication consists of running Javascript and sending string messages back and forth,
110 		kinda similar to your communication with a web server.
111 	+/
112 	// these form your communcation channel between the web view and the native world
113 	// abstract void sendMessageToHost(string json);
114 	// void delegate(string json) receiveMessageFromHost;
115 
116 	/+
117 		I also need a url filter
118 	+/
119 
120 	// this is implemented as a do-nothing in the NestedChildWindowWidget base
121 	// but you will almost certainly need to override it in implementations.
122 	// abstract void registerMovementAdditionalWork();
123 }
124 
125 // AddScriptToExecuteOnDocumentCreated
126 
127 
128 
129 version(wv2)
130 class WebViewWidget_WV2 : WebViewWidgetBase {
131 	private RC!ICoreWebView2 webview_window;
132 	// 12 introduces status bar
133 	// 15 introduces favicon notifications
134 	// 16 introduces printing
135 	private RC!ICoreWebView2_16 webview_window_ext_1;
136 	private RC!ICoreWebView2Environment webview_env;
137 	private RC!ICoreWebView2Controller controller;
138 
139 	private bool initialized;
140 
141 	private HRESULT initializeWithController(ICoreWebView2Controller controller_raw) {
142 
143 		// need to keep this beyond the callback or we're doomed.
144 		this.controller = RC!ICoreWebView2Controller(controller_raw);
145 
146 		this.webview_window = controller.CoreWebView2;
147 
148 		this.webview_window_ext_1 = this.webview_window.queryInterface!(ICoreWebView2_16);
149 
150 		bool enableStatusBar = true;
151 
152 		if(this.webview_window_ext_1) {
153 			enableStatusBar = false;
154 			this.webview_window_ext_1.add_StatusBarTextChanged!(typeof(this))((sender, args, this_) {
155 				this_.status = toGC(&this_.webview_window_ext_1.raw.get_StatusBarText);
156 				return S_OK;
157 			}, this);
158 
159 			webview_window_ext_1.add_FaviconChanged!(typeof(this))((sender, args, this_) {
160 				this_.webview_window_ext_1.GetFavicon(
161 					COREWEBVIEW2_FAVICON_IMAGE_FORMAT_PNG,
162 					callback!(ICoreWebView2GetFaviconCompletedHandler, typeof(this_))(function(error, streamPtrConst, ctx2) {
163 
164 						auto streamPtr = cast(IStream) streamPtrConst;
165 
166 						ubyte[] buffer = new ubyte[](640); // most favicons are pretty small
167 						enum growth_size = 1024; // and we'll grow linearly by the kilobyte
168 						size_t at;
169 
170 						more:
171 						ULONG actuallyRead;
172 						auto ret = streamPtr.Read(buffer.ptr + at, cast(UINT) (buffer.length - at), &actuallyRead);
173 						if(ret == S_OK) {
174 							// read completed, possibly more data pending
175 							auto moreData = actuallyRead >= (buffer.length - at);
176 
177 							at += actuallyRead;
178 							if(moreData && (buffer.length - at < growth_size))
179 								buffer.length += growth_size;
180 							goto more;
181 						} else if(ret == S_FALSE) {
182 							// end of file reached
183 							at += actuallyRead;
184 							buffer = buffer[0 .. at];
185 
186 							import arsd.png;
187 							ctx2.favicon = readPngFromBytes(buffer);
188 						} else {
189 							// other error
190 							throw new ComException(ret);
191 						}
192 
193 						return S_OK;
194 					}, this_)
195 				);
196 
197 				return S_OK;
198 			}, this);
199 		}
200 
201 		webview_window.add_DocumentTitleChanged!(typeof(this))((sender, args, this_) {
202 			this_.title = toGC(&sender.get_DocumentTitle);
203 			return S_OK;
204 		}, this);
205 
206 		webview_window.add_NewWindowRequested!(typeof(this))((sender, args, this_) {
207 			// args.get_Uri
208 			// args.get_IsUserInitiated
209 			// args.put_NewWindow();
210 
211 			string url = toGC(&args.get_Uri);
212 			int ret;
213 
214 			WebViewWidget_WV2 widget;
215 
216 			runInGuiThread({
217 				ret = 0;
218 
219 				scope WebViewWidget delegate(Widget, BrowserSettings) accept = (parent, passed_settings) {
220 					ret = 1;
221 					if(parent !is null) {
222 						auto widget = new WebViewWidget_WV2(url, this_.openNewWindow, passed_settings, parent);
223 
224 						return widget;
225 					}
226 					return null;
227 				};
228 				this_.openNewWindow(OpenNewWindowParams(url, accept));
229 				return;
230 			});
231 
232 			if(ret) {
233 				args.put_Handled(true);
234 				// args.put_NewWindow(widget.webview_window.returnable);
235 			}
236 
237 			return S_OK;
238 
239 		}, this);
240 
241 		// add_HistoryChanged
242 		// that's where CanGoBack and CanGoForward can be rechecked.
243 
244 		RC!ICoreWebView2Settings Settings = this.webview_window.Settings;
245 		Settings.IsScriptEnabled = TRUE;
246 		Settings.AreDefaultScriptDialogsEnabled = TRUE;
247 		Settings.IsWebMessageEnabled = TRUE;
248 		Settings.IsStatusBarEnabled = enableStatusBar;
249 
250 		auto ert = this.webview_window.add_NavigationStarting!(typeof(this))(
251 			function (sender, args, this_) {
252 				this_.url = toGC(&args.get_Uri);
253 				return S_OK;
254 			}, this);
255 
256 		RECT bounds;
257 		GetClientRect(this.containerWindow.impl.hwnd, &bounds);
258 		controller.Bounds = bounds;
259 		//error = webview_window.Navigate("http://arsdnet.net/test.html"w.ptr);
260 		//error = webview_window.NavigateToString("<html><body>Hello</body></html>"w.ptr);
261 		//error = webview_window.Navigate("http://192.168.1.10/"w.ptr);
262 
263 		if(url !is null) {
264 			WCharzBuffer bfr = WCharzBuffer(url);
265 			this.webview_window.Navigate(bfr.ptr);
266 		}
267 
268 		controller.IsVisible = true;
269 
270 		this.initialized = true;
271 
272 		return S_OK;
273 	}
274 
275 	private void delegate(scope OpenNewWindowParams) openNewWindow;
276 
277 	this(string url, void delegate(scope OpenNewWindowParams) openNewWindow, BrowserSettings settings, Widget parent) {
278 		this.openNewWindow = openNewWindow;
279 		super(parent);
280 		// that ctor sets containerWindow
281 
282 		this.url = url;
283 
284 		Wv2App.useEnvironment((env) {
285 			env.CreateCoreWebView2Controller(containerWindow.impl.hwnd,
286 				callback!(ICoreWebView2CreateCoreWebView2ControllerCompletedHandler, typeof(this))(function(error, controller_raw, ctx) {
287 					if(error || controller_raw is null)
288 						return error;
289 
290 					return ctx.initializeWithController(controller_raw);
291 				}, this));
292 		});
293 	}
294 
295 	override void registerMovementAdditionalWork() {
296 		if(initialized) {
297 			RECT bounds;
298 			GetClientRect(containerWindow.impl.hwnd, &bounds);
299 			controller.Bounds = bounds;
300 
301 			controller.NotifyParentWindowPositionChanged();
302 		}
303 	}
304 
305 	override void refresh() {
306 		if(!initialized) return;
307 		webview_window.Reload();
308 	}
309 	override void back() {
310 		if(!initialized) return;
311 		webview_window.GoBack();
312 	}
313 	override void forward() {
314 		if(!initialized) return;
315 		webview_window.GoForward();
316 	}
317 	override void stop() {
318 		if(!initialized) return;
319 		webview_window.Stop();
320 	}
321 
322 	override void navigate(string url) {
323 		if(!initialized) return;
324 		import std.utf;
325 		auto error = webview_window.Navigate(url.toUTF16z);
326 	}
327 
328 	// the url and line are for error reporting purposes
329 	override void executeJavascript(string code, string url = null, int line = 0) {
330 		if(!initialized) return;
331 		import std.utf;
332 		webview_window.ExecuteScript(code.toUTF16z, null);
333 	}
334 
335 	override void showDevTools() {
336 		if(!initialized) return;
337 		webview_window.OpenDevToolsWindow();
338 	}
339 }
340 
341 /++
342 	The openInNewWindow delegate is given these params.
343 
344 	To accept the new window, call
345 
346 	params.accept(parent_widget);
347 
348 	Please note, you can force it to replace the current tab
349 	by just navigating your current thing to the given url instead
350 	of accepting it.
351 
352 	If you accept with a null widget, it will create a new window
353 	but then return null, since the new window is managed by the
354 	underlying webview instead of by minigui.
355 
356 	If you do not call accept, the pop up will be blocked.
357 
358 	accept returns an instance to the newly created widget, which will
359 	be a parent to the widget you passed.
360 
361 	accept will be called from the gui thread and it MUST not call into
362 	any other webview methods. It should only create windows/widgets
363 	and set event handlers etc.
364 
365 	You MUST not escape references to anything in this structure. It
366 	is entirely strictly temporary!
367 +/
368 struct OpenNewWindowParams {
369 	string url;
370 	WebViewWidget delegate(Widget parent, BrowserSettings settings = BrowserSettings.init) accept;
371 }
372 
373 /++
374 	Represents a browser setting that can be left default or specifically turned on or off.
375 +/
376 struct SettingValue {
377 	private byte value = -1;
378 
379 	/++
380 		Set it with `= true` or `= false`.
381 	+/
382 	void opAssign(bool enable) {
383 		value = enable ? 1 : 0;
384 	}
385 
386 	/++
387 		And this resets it to the default value.
388 	+/
389 	void setDefault() {
390 		value = -1;
391 	}
392 
393 	/// If isDefault, it will use the default setting from the browser. Else, the getValue return value will be used. getValue is invalid if !isDefault.
394 	bool isDefault() {
395 		return value == -1;
396 	}
397 
398 	/// ditto
399 	bool getValue() {
400 		return value == 1;
401 	}
402 }
403 
404 /++
405 	Defines settings for a browser widget. Not all backends will respect all settings.
406 
407 	The order of members of this struct may change at any time. Refer to its members by
408 	name.
409 +/
410 struct BrowserSettings {
411 	/// This is just disabling the automatic positional constructor, since that is not stable here.
412 	this(typeof(null)) {}
413 
414 	string standardFontFamily;
415 	string fixedFontFamily;
416 	string serifFontFamily;
417 	string sansSerifFontFamily;
418 	string cursiveFontFamily;
419 	string fantasyFontFamily;
420 
421 	int defaultFontSize;
422 	int defaultFixedFontSize;
423 	int minimumFontSize;
424 	//int minimumLogicalFontSize;
425 
426 	SettingValue remoteFontsEnabled;
427 	SettingValue javascriptEnabled;
428 	SettingValue imagesEnabled;
429 	SettingValue clipboardAccessEnabled;
430 	SettingValue localStorageEnabled;
431 
432 	version(cef)
433 	private void set(cef_browser_settings_t* browser_settings) {
434 		alias settings = this;
435 		if(settings.standardFontFamily)
436 			browser_settings.standard_font_family = cef_string_t(settings.standardFontFamily);
437 		if(settings.fixedFontFamily)
438 			browser_settings.fixed_font_family = cef_string_t(settings.fixedFontFamily);
439 		if(settings.serifFontFamily)
440 			browser_settings.serif_font_family = cef_string_t(settings.serifFontFamily);
441 		if(settings.sansSerifFontFamily)
442 			browser_settings.sans_serif_font_family = cef_string_t(settings.sansSerifFontFamily);
443 		if(settings.cursiveFontFamily)
444 			browser_settings.cursive_font_family = cef_string_t(settings.cursiveFontFamily);
445 		if(settings.fantasyFontFamily)
446 			browser_settings.fantasy_font_family = cef_string_t(settings.fantasyFontFamily);
447 		if(settings.defaultFontSize)
448 			browser_settings.default_font_size = settings.defaultFontSize;
449 		if(settings.defaultFixedFontSize)
450 			browser_settings.default_fixed_font_size = settings.defaultFixedFontSize;
451 		if(settings.minimumFontSize)
452 			browser_settings.minimum_font_size = settings.minimumFontSize;
453 
454 		if(!settings.remoteFontsEnabled.isDefault())
455 			browser_settings.remote_fonts = settings.remoteFontsEnabled.getValue() ? cef_state_t.STATE_ENABLED : cef_state_t.STATE_DISABLED;
456 		if(!settings.javascriptEnabled.isDefault())
457 			browser_settings.javascript = settings.javascriptEnabled.getValue() ? cef_state_t.STATE_ENABLED : cef_state_t.STATE_DISABLED;
458 		if(!settings.imagesEnabled.isDefault())
459 			browser_settings.image_loading = settings.imagesEnabled.getValue() ? cef_state_t.STATE_ENABLED : cef_state_t.STATE_DISABLED;
460 		if(!settings.clipboardAccessEnabled.isDefault())
461 			browser_settings.javascript_access_clipboard = settings.clipboardAccessEnabled.getValue() ? cef_state_t.STATE_ENABLED : cef_state_t.STATE_DISABLED;
462 		if(!settings.localStorageEnabled.isDefault())
463 			browser_settings.local_storage = settings.localStorageEnabled.getValue() ? cef_state_t.STATE_ENABLED : cef_state_t.STATE_DISABLED;
464 
465 	}
466 }
467 
468 version(cef)
469 class WebViewWidget_CEF : WebViewWidgetBase {
470 	/++
471 		Create a webview that does not support opening links in new windows and uses default settings to load the given url.
472 	+/
473 	this(string url, Widget parent) {
474 		this(url, null, BrowserSettings.init, parent);
475 	}
476 
477 	/++
478 		Full-featured constructor.
479 	+/
480 	this(string url, void delegate(scope OpenNewWindowParams) openNewWindow, BrowserSettings settings, Widget parent) {
481 		//semaphore = new Semaphore;
482 		assert(CefApp.active);
483 
484 		this(new MiniguiCefClient(openNewWindow), parent, false);
485 
486 		cef_window_info_t window_info;
487 		window_info.parent_window = containerWindow.nativeWindowHandle;
488 
489 		cef_string_t cef_url = cef_string_t(url);//"http://arsdnet.net/test.html");
490 
491 		cef_browser_settings_t browser_settings;
492 		browser_settings.size = cef_browser_settings_t.sizeof;
493 
494 		settings.set(&browser_settings);
495 
496 		auto got = libcef.browser_host_create_browser(&window_info, client.passable, &cef_url, &browser_settings, null, null);
497 	}
498 
499 	/+
500 	~this() {
501 		import core.stdc.stdio;
502 		import core.memory;
503 		printf("CLEANUP %s\n", GC.inFinalizer ? "GC".ptr : "destroy".ptr);
504 	}
505 	+/
506 
507 	override void dispose() {
508 		// sdpyPrintDebugString("closed");
509 		// the window is already gone so too late to do this really....
510 		// if(browserHandle) browserHandle.get_host.close_browser(true);
511 
512 		// sdpyPrintDebugString("DISPOSE");
513 
514 		if(win && win.nativeWindowHandle())
515 			mapping.remove(win.nativeWindowHandle());
516 		if(browserWindow)
517 			browserMapping.remove(browserWindow);
518 
519 		.destroy(this); // but this is ok to do some memory management cleanup
520 	}
521 
522 	private this(MiniguiCefClient client, Widget parent, bool isDevTools) {
523 		super(parent);
524 
525 		this.client = client;
526 
527 		flushGui();
528 
529 		mapping[containerWindow.nativeWindowHandle()] = this;
530 
531 		this.addEventListener(delegate(KeyDownEvent ke) {
532 			if(ke.key == Key.Tab)
533 				ke.preventDefault();
534 		});
535 
536 		this.addEventListener((FocusEvent fe) {
537 			if(!browserHandle) return;
538 
539 			XFocusChangeEvent ev;
540 			ev.type = arsd.simpledisplay.EventType.FocusIn;
541 			ev.display = XDisplayConnection.get;
542 			ev.window = ozone;
543 			ev.mode = NotifyModes.NotifyNormal;
544 			ev.detail = NotifyDetail.NotifyVirtual;
545 
546 			// sdpyPrintDebugString("Sending FocusIn");
547 
548 			trapXErrors( {
549 				XSendEvent(XDisplayConnection.get, ozone, false, 0, cast(XEvent*) &ev);
550 			});
551 
552 			// this also works if the message is buggy and it avoids weirdness from raising window etc
553 			//executeJavascript("if(window.__arsdPreviouslyFocusedNode) window.__arsdPreviouslyFocusedNode.focus(); window.dispatchEvent(new FocusEvent(\"focus\"));");
554 		});
555 		this.addEventListener((BlurEvent be) {
556 			if(!browserHandle) return;
557 
558 			XFocusChangeEvent ev;
559 			ev.type = arsd.simpledisplay.EventType.FocusOut;
560 			ev.display = XDisplayConnection.get;
561 			ev.window = ozone;
562 			ev.mode = NotifyModes.NotifyNormal;
563 			ev.detail = NotifyDetail.NotifyNonlinearVirtual;
564 
565 			// sdpyPrintDebugString("Sending FocusOut");
566 
567 			trapXErrors( {
568 				XSendEvent(XDisplayConnection.get, ozone, false, 0, cast(XEvent*) &ev);
569 			});
570 
571 			//executeJavascript("if(document.activeElement) { window.__arsdPreviouslyFocusedNode = document.activeElement; document.activeElement.blur(); } window.dispatchEvent(new FocusEvent(\"blur\"));");
572 		});
573 
574 		bool closeAttempted = false;
575 
576 		if(isDevTools)
577 		this.parentWindow.addEventListener((scope ClosingEvent ce) {
578 			this.parentWindow.hide();
579 			ce.preventDefault();
580 		});
581 		else
582 		this.parentWindow.addEventListener((scope ClosingEvent ce) {
583 			if(devTools)
584 				devTools.close();
585 			if(browserHandle) {
586 				if(!closeAttempted) {
587 					closeAttempted = true;
588 					browserHandle.get_host.close_browser(false);
589 					ce.preventDefault();
590 				 	sdpyPrintDebugString("closing 1");
591 				} else {
592 					browserHandle.get_host.close_browser(true);
593 				 	sdpyPrintDebugString("closing 2");
594 				}
595 			}
596 		});
597 	}
598 
599 	~this() {
600 		import core.stdc.stdio;
601 		printf("GC'd %p\n", cast(void*) this);
602 	}
603 
604 	private MiniguiCefClient client;
605 
606 	override void registerMovementAdditionalWork() {
607 		if(browserWindow) {
608 			// import std.stdio; writeln("new size ", width, "x", height);
609 			static if(UsingSimpledisplayX11) {
610 				XResizeWindow(XDisplayConnection.get, browserWindow, width, height);
611 				if(ozone) XResizeWindow(XDisplayConnection.get, ozone, width, height);
612 			}
613 			// FIXME: do for Windows too
614 		}
615 	}
616 
617 	SimpleWindow browserHostWrapped;
618 	SimpleWindow browserWindowWrapped;
619 	override SimpleWindow focusableWindow() {
620 		if(browserWindowWrapped is null && browserWindow) {
621 			browserWindowWrapped = new SimpleWindow(browserWindow);
622 			// FIXME: this should never actually happen should it
623 		}
624 		return browserWindowWrapped;
625 	}
626 
627 	private NativeWindowHandle browserWindow;
628 	private NativeWindowHandle ozone;
629 	private RC!cef_browser_t browserHandle;
630 
631 	private static WebViewWidget[NativeWindowHandle] mapping;
632 	private static WebViewWidget[NativeWindowHandle] browserMapping;
633 
634 	private {
635 		string findingText;
636 		bool findingCase;
637 	}
638 
639 	// might not be stable, webview does this fully integrated
640 	void findText(string text, bool forward = true, bool matchCase = false, bool findNext = false) {
641 		if(browserHandle) {
642 			auto host = browserHandle.get_host();
643 
644 			auto txt = cef_string_t(text);
645 			host.find(&txt, forward, matchCase, findNext);
646 
647 			findingText = text;
648 			findingCase = matchCase;
649 		}
650 	}
651 
652 	// ditto
653 	void findPrevious() {
654 		if(!browserHandle)
655 			return;
656 		auto host = browserHandle.get_host();
657 		auto txt = cef_string_t(findingText);
658 		host.find(&txt, 0, findingCase, 1);
659 	}
660 
661 	// ditto
662 	void findNext() {
663 		if(!browserHandle)
664 			return;
665 		auto host = browserHandle.get_host();
666 		auto txt = cef_string_t(findingText);
667 		host.find(&txt, 1, findingCase, 1);
668 	}
669 
670 	// ditto
671 	void stopFind() {
672 		if(!browserHandle)
673 			return;
674 		auto host = browserHandle.get_host();
675 		host.stop_finding(1);
676 	}
677 
678 	override void refresh() { if(browserHandle) browserHandle.reload(); }
679 	override void back() { if(browserHandle) browserHandle.go_back(); }
680 	override void forward() { if(browserHandle) browserHandle.go_forward(); }
681 	override void stop() { if(browserHandle) browserHandle.stop_load(); }
682 
683 	override void navigate(string url) {
684 		if(!browserHandle) return;
685 		auto s = cef_string_t(url);
686 		browserHandle.get_main_frame.load_url(&s);
687 	}
688 
689 	// the url and line are for error reporting purposes
690 	override void executeJavascript(string code, string url = null, int line = 0) {
691 		if(!browserHandle) return;
692 
693 		auto c = cef_string_t(code);
694 		auto u = cef_string_t(url);
695 		browserHandle.get_main_frame.execute_java_script(&c, &u, line);
696 	}
697 
698 	private Window devTools;
699 	override void showDevTools() {
700 		if(!browserHandle) return;
701 
702 		if(devTools is null) {
703 			auto host = browserHandle.get_host;
704 
705 			if(host.has_dev_tools()) {
706 				host.close_dev_tools();
707 				return;
708 			}
709 
710 			cef_window_info_t windowinfo;
711 			version(linux) {
712 				auto sw = new Window("DevTools");
713 				//sw.win.beingOpenKeepsAppOpen = false;
714 				devTools = sw;
715 
716 				auto wv = new WebViewWidget_CEF(client, sw, true);
717 
718 				sw.show();
719 
720 				windowinfo.parent_window = wv.containerWindow.nativeWindowHandle;
721 			}
722 			host.show_dev_tools(&windowinfo, client.passable, null /* settings */, null /* inspect element at coordinates */);
723 		} else {
724 			if(devTools.hidden)
725 				devTools.show();
726 			else
727 				devTools.hide();
728 		}
729 	}
730 
731 	// FYI the cef browser host also allows things like custom spelling dictionaries and getting navigation entries.
732 
733 	// JS on init?
734 	// JS bindings?
735 	// user styles?
736 	// navigate to string? (can just use a data uri maybe?)
737 	// custom scheme handlers?
738 
739 	// navigation callbacks to prohibit certain things or move links to new window etc?
740 }
741 
742 version(cef) {
743 
744 	//import core.sync.semaphore;
745 	//__gshared Semaphore semaphore;
746 
747 	/+
748 		Finds the WebViewWidget associated with the given browser, then runs the given code in the gui thread on it.
749 	+/
750 	void runOnWebView(RC!cef_browser_t browser, void delegate(WebViewWidget) dg) nothrow {
751 		auto wh = cast(NativeWindowHandle) browser.get_host.get_window_handle;
752 
753 		import core.thread;
754 		try { thread_attachThis(); } catch(Exception e) {}
755 
756 		runInGuiThreadAsync({
757 			if(auto wvp = wh in WebViewWidget.browserMapping) {
758 				dg(*wvp);
759 			} else {
760 				writeln("not found ", wh, WebViewWidget.browserMapping);
761 			}
762 		});
763 	}
764 
765 	class MiniguiCefLifeSpanHandler : CEF!cef_life_span_handler_t {
766 		private MiniguiCefClient client;
767 		this(MiniguiCefClient client) {
768 			this.client = client;
769 		}
770 
771 		override void on_before_dev_tools_popup(RC!(cef_browser_t), cef_window_info_t*, cef_client_t**, cef_browser_settings_t*, cef_dictionary_value_t**, int*) nothrow {
772 
773 		}
774 
775 		override int on_before_popup(
776 			RC!cef_browser_t browser,
777 			RC!cef_frame_t frame,
778 			const(cef_string_t)* target_url,
779 			const(cef_string_t)* target_frame_name,
780 			cef_window_open_disposition_t target_disposition,
781 			int user_gesture,
782 			const(cef_popup_features_t)* popupFeatures,
783 			cef_window_info_t* windowInfo,
784 			cef_client_t** client,
785 			cef_browser_settings_t* browser_settings,
786 			cef_dictionary_value_t** extra_info,
787 			int* no_javascript_access
788 		) {
789 		sdpyPrintDebugString("on_before_popup");
790 			if(this.client.openNewWindow is null)
791 				return 1; // new windows disabled
792 
793 			try {
794 				int ret;
795 
796 				import core.thread;
797 				try { thread_attachThis(); } catch(Exception e) {}
798 
799 				// FIXME: change settings here
800 
801 				runInGuiThread({
802 					ret = 1;
803 					scope WebViewWidget delegate(Widget, BrowserSettings) accept = (parent, passed_settings) {
804 						ret = 0;
805 						if(parent !is null) {
806 							auto widget = new WebViewWidget_CEF(this.client, parent, false);
807 							(*windowInfo).parent_window = widget.containerWindow.nativeWindowHandle;
808 
809 							passed_settings.set(browser_settings);
810 
811 							return widget;
812 						}
813 						return null;
814 					};
815 					this.client.openNewWindow(OpenNewWindowParams(target_url.toGC, accept));
816 					return;
817 				});
818 
819 				return ret;
820 			} catch(Exception e) {
821 				return 1;
822 			}
823 			/+
824 			if(!user_gesture)
825 				return 1; // if not created by the user, cancel it; basic popup blocking
826 			+/
827 		}
828 		override void on_after_created(RC!cef_browser_t browser) {
829 			auto handle = cast(NativeWindowHandle) browser.get_host().get_window_handle();
830 			auto ptr = browser.passable; // this adds to the refcount until it gets inside
831 
832 			import core.thread;
833 			try { thread_attachThis(); } catch(Exception e) {}
834 
835 			// the only reliable key (at least as far as i can tell) is the window handle
836 			// so gonna look that up and do the sync mapping that way.
837 			runInGuiThreadAsync({
838 				version(Windows) {
839 					auto parent = GetParent(handle);
840 				} else static if(UsingSimpledisplayX11) {
841 					import arsd.simpledisplay : Window;
842 					Window root;
843 					Window parent;
844 					Window ozone;
845 					uint c = 0;
846 					auto display = XDisplayConnection.get;
847 					Window* children;
848 					XQueryTree(display, handle, &root, &parent, &children, &c);
849 					if(c == 1)
850 						ozone = children[0];
851 					XFree(children);
852 				} else static assert(0);
853 
854 				if(auto wvp = parent in WebViewWidget.mapping) {
855 					auto wv = *wvp;
856 					wv.browserWindow = handle;
857 					wv.browserHandle = RC!cef_browser_t(ptr);
858 					wv.ozone = ozone ? ozone : handle;
859 
860 					wv.browserHostWrapped = new SimpleWindow(handle);
861 					// XSelectInput(XDisplayConnection.get, handle, EventMask.StructureNotifyMask);
862 
863 					wv.browserHostWrapped.onDestroyed = delegate{
864 						import std.stdio; writefln("browser host %d destroyed (handle %d)", wv.browserWindowWrapped.window, wv.browserWindow);
865 
866 						auto bce = new BrowserClosedEvent(wv);
867 						bce.dispatch();
868 					};
869 
870 					// need this to forward key events to
871 					wv.browserWindowWrapped = new SimpleWindow(wv.ozone);
872 
873 					/+
874 					XSelectInput(XDisplayConnection.get, wv.ozone, EventMask.StructureNotifyMask);
875 					wv.browserWindowWrapped.onDestroyed = delegate{
876 						import std.stdio; writefln("browser core %d destroyed (handle %d)", wv.browserWindowWrapped.window, wv.browserWindow);
877 
878 						//auto bce = new BrowserClosedEvent(wv);
879 						//bce.dispatch();
880 					};
881 					+/
882 
883 					/+
884 					XSelectInput(XDisplayConnection.get, ozone, EventMask.FocusChangeMask);
885 					wv.browserWindowWrapped.onFocusChange = (bool got) {
886 						import std.format;
887 						sdpyPrintDebugString(format("focus change %s %x", got, wv.browserWindowWrapped.impl.window));
888 					};
889 					+/
890 
891 					wv.registerMovementAdditionalWork();
892 
893 					WebViewWidget.browserMapping[handle] = wv;
894 				} else assert(0);
895 			});
896 		}
897 		override int do_close(RC!cef_browser_t browser) {
898 						import std.stdio;
899 						debug writeln("do_close");
900 			/+
901 			browser.runOnWebView((wv) {
902 				wv.browserWindowWrapped.close();
903 				.destroy(wv.browserHandle);
904 			});
905 
906 			return 1;
907 			+/
908 
909 			return 0;
910 		}
911 		override void on_before_close(RC!cef_browser_t browser) {
912 			import std.stdio; debug writeln("notify");
913 			browser.runOnWebView((wv) {
914 				.destroy(wv.browserHandle);
915 			});
916 			/+
917 			try
918 			semaphore.notify;
919 			catch(Exception e) { assert(0); }
920 			+/
921 		}
922 	}
923 
924 	class MiniguiLoadHandler : CEF!cef_load_handler_t {
925 		override void on_loading_state_change(RC!(cef_browser_t) browser, int isLoading, int canGoBack, int canGoForward) {
926 			/+
927 			browser.runOnWebView((WebViewWidget wvw) {
928 				wvw.parentWindow.win.title = wvw.browserHandle.get_main_frame.get_url.toGCAndFree;
929 			});
930 			+/
931 		}
932 		override void on_load_start(RC!(cef_browser_t), RC!(cef_frame_t), cef_transition_type_t) {
933 		}
934 		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)*) {
935 		}
936 		override void on_load_end(RC!(cef_browser_t), RC!(cef_frame_t), int) {
937 		}
938 	}
939 
940 	class MiniguiDialogHandler : CEF!cef_dialog_handler_t {
941 		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,
942 			cef_string_list_t accept_filters,
943 			cef_string_list_t accept_extensions,
944 			cef_string_list_t accept_descriptions,
945 			RC!(cef_file_dialog_callback_t) callback)
946 		{
947 			try {
948 				auto ptr = callback.passable();
949 				browser.runOnWebView((wv) {
950 					getOpenFileName(wv.parentWindow, (string name) {
951 						auto callback = RC!cef_file_dialog_callback_t(ptr);
952 						auto list = libcef.string_list_alloc();
953 						auto item = cef_string_t(name);
954 						libcef.string_list_append(list, &item);
955 						callback.cont(list);
956 					}, null, null, () {
957 						auto callback = RC!cef_file_dialog_callback_t(ptr);
958 						callback.cancel();
959 					}, "/home/me/");
960 				});
961 			} catch(Exception e) {}
962 
963 			return 1;
964 		}
965 	}
966 
967 	class MiniguiDownloadHandler : CEF!cef_download_handler_t {
968 		override int on_before_download(
969 			RC!cef_browser_t browser,
970 			RC!cef_download_item_t download_item,
971 			const(cef_string_t)* suggested_name,
972 			RC!cef_before_download_callback_t callback
973 		) nothrow
974 		{
975 			// FIXME: different filename and check if exists for overwrite etc
976 			auto fn = cef_string_t(cast(wstring)("/home/me/Downloads/"w ~ suggested_name.str[0..suggested_name.length]));
977 			sdpyPrintDebugString(fn.toGC);
978 			callback.cont(&fn, false);
979 
980 			return 1;
981 		}
982 
983 		override void on_download_updated(
984 			RC!cef_browser_t browser,
985 			RC!cef_download_item_t download_item,
986 			RC!cef_download_item_callback_t cancel
987 		) nothrow
988 		{
989 			sdpyPrintDebugString(download_item.get_percent_complete());
990 			// FIXME
991 		}
992 
993 		override int can_download(RC!(cef_browser_t), const(cef_string_utf16_t)*, const(cef_string_utf16_t)*) {
994 			return 1;
995 		}
996 	}
997 
998 	class MiniguiKeyboardHandler : CEF!cef_keyboard_handler_t {
999 		override int on_pre_key_event(
1000 			RC!(cef_browser_t) browser,
1001 			const(cef_key_event_t)* event,
1002 			XEvent* osEvent,
1003 			int* isShortcut
1004 		) nothrow {
1005 		/+
1006 			sdpyPrintDebugString("---pre---");
1007 			sdpyPrintDebugString(event.focus_on_editable_field);
1008 			sdpyPrintDebugString(event.windows_key_code);
1009 			sdpyPrintDebugString(event.modifiers);
1010 			sdpyPrintDebugString(event.unmodified_character);
1011 		+/
1012 			//*isShortcut = 1;
1013 			return 0; // 1 if handled, which cancels sending it to browser
1014 		}
1015 
1016 		override int on_key_event(
1017 			RC!(cef_browser_t) browser,
1018 			const(cef_key_event_t)* event,
1019 			XEvent* osEvent
1020 		) nothrow {
1021 		/+
1022 			sdpyPrintDebugString("---key---");
1023 			sdpyPrintDebugString(event.focus_on_editable_field);
1024 			sdpyPrintDebugString(event.windows_key_code);
1025 			sdpyPrintDebugString(event.modifiers);
1026 		+/
1027 			return 0; // 1 if handled
1028 		}
1029 	}
1030 
1031 	class MiniguiDisplayHandler : CEF!cef_display_handler_t {
1032 		override void on_address_change(RC!(cef_browser_t) browser, RC!(cef_frame_t), const(cef_string_utf16_t)* address) {
1033 			auto url = address.toGC;
1034 			browser.runOnWebView((wv) {
1035 				wv.url = url;
1036 			});
1037 		}
1038 		override void on_title_change(RC!(cef_browser_t) browser, const(cef_string_utf16_t)* title) {
1039 			auto t = title.toGC;
1040 			browser.runOnWebView((wv) {
1041 				wv.title = t;
1042 			});
1043 		}
1044 		override void on_favicon_urlchange(RC!(cef_browser_t) browser, cef_string_list_t urls) {
1045 			string url;
1046 			auto size = libcef.string_list_size(urls);
1047 			if(size > 0) {
1048 				cef_string_t str;
1049 				libcef.string_list_value(urls, 0, &str);
1050 				url = str.toGC;
1051 
1052 				static class Thing : CEF!cef_download_image_callback_t {
1053 					RC!cef_browser_t browserHandle;
1054 					this(RC!cef_browser_t browser) nothrow {
1055 						this.browserHandle = browser;
1056 					}
1057 					override void on_download_image_finished(const(cef_string_t)* image_url, int http_status_code, RC!cef_image_t image) nothrow {
1058 						int width;
1059 						int height;
1060 						if(image.getRawPointer is null) {
1061 							browserHandle.runOnWebView((wv) {
1062 								wv.favicon = null;
1063 							});
1064 							return;
1065 						}
1066 
1067 						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);
1068 
1069 						if(data.getRawPointer is null || width == 0 || height == 0) {
1070 							browserHandle.runOnWebView((wv) {
1071 								wv.favicon = null;
1072 							});
1073 						} else {
1074 							auto s = data.get_size();
1075 							auto buffer = new ubyte[](s);
1076 							auto got = data.get_data(buffer.ptr, buffer.length, 0);
1077 							auto slice = buffer[0 .. got];
1078 
1079 							auto img = new TrueColorImage (width, height, slice);
1080 
1081 							browserHandle.runOnWebView((wv) {
1082 								wv.favicon = img;
1083 							});
1084 						}
1085 					}
1086 				}
1087 
1088 				if(url.length) {
1089 					auto callback = new Thing(browser);
1090 
1091 					browser.get_host().download_image(&str, true, 16, 0, callback.passable);
1092 				} else {
1093 					browser.runOnWebView((wv) {
1094 						wv.favicon = null;
1095 					});
1096 				}
1097 			}
1098 
1099 			browser.runOnWebView((wv) {
1100 				wv.favicon_url = url;
1101 			});
1102 		}
1103 		override void on_fullscreen_mode_change(RC!(cef_browser_t) browser, int) {
1104 		}
1105 		override int on_tooltip(RC!(cef_browser_t) browser, cef_string_utf16_t*) {
1106 			return 0;
1107 		}
1108 		override void on_status_message(RC!(cef_browser_t) browser, const(cef_string_utf16_t)* msg) {
1109 			auto status = msg.toGC;
1110 			browser.runOnWebView((wv) {
1111 				wv.status = status;
1112 			});
1113 		}
1114 		override void on_loading_progress_change(RC!(cef_browser_t) browser, double progress) {
1115 			// progress is from 0.0 to 1.0
1116 			browser.runOnWebView((wv) {
1117 				wv.loadingProgress = cast(int) (progress * 100);
1118 			});
1119 		}
1120 		override int on_console_message(RC!(cef_browser_t), cef_log_severity_t, const(cef_string_utf16_t)*, const(cef_string_utf16_t)*, int) {
1121 			return 1; // 1 means to suppress it being automatically output
1122 		}
1123 		override int on_auto_resize(RC!(cef_browser_t), const(cef_size_t)*) {
1124 			return 0;
1125 		}
1126 		override int on_cursor_change(RC!(cef_browser_t), cef_cursor_handle_t, cef_cursor_type_t, const(cef_cursor_info_t)*) {
1127 			return 0;
1128 		}
1129 		override void on_media_access_change(RC!(cef_browser_t), int, int) {
1130 
1131 		}
1132 	}
1133 
1134 	class MiniguiRequestHandler : CEF!cef_request_handler_t {
1135 
1136 		override int on_render_process_unresponsive(RC!(cef_browser_t), RC!(cef_unresponsive_process_callback_t)) nothrow {
1137 			return 0;
1138 		}
1139 		override void on_render_process_responsive(RC!(cef_browser_t) p) nothrow {
1140 
1141 		}
1142 
1143 		override int on_before_browse(RC!(cef_browser_t), RC!(cef_frame_t), RC!(cef_request_t), int, int) nothrow {
1144 			return 0;
1145 		}
1146 		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 {
1147 			return 0;
1148 		}
1149 		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 {
1150 			return null;
1151 		}
1152 		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 {
1153 			// this is for http basic auth popup.....
1154 			return 0;
1155 		}
1156 		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 {
1157 			return 0;
1158 		}
1159 		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 {
1160 			return 0;
1161 		}
1162 		override void on_render_view_ready(RC!(cef_browser_t) p) nothrow {
1163 
1164 		}
1165 		override void on_render_process_terminated(RC!(cef_browser_t), cef_termination_status_t, int error_code, const(cef_string_utf16_t)*) nothrow {
1166 
1167 		}
1168 		override void on_document_available_in_main_frame(RC!(cef_browser_t) browser) nothrow {
1169 			browser.runOnWebView(delegate(wv) {
1170 				wv.executeJavascript("console.log('here');");
1171 			});
1172 
1173 		}
1174 	}
1175 
1176 	class MiniguiContextMenuHandler : CEF!cef_context_menu_handler_t {
1177 		private MiniguiCefClient client;
1178 		this(MiniguiCefClient client) {
1179 			this.client = client;
1180 		}
1181 
1182 		override void on_before_context_menu(RC!(cef_browser_t) browser, RC!(cef_frame_t) frame, RC!(cef_context_menu_params_t) params, RC!(cef_menu_model_t) model) nothrow {
1183 			// FIXME: should i customize these? it is kinda specific to my browser
1184 			int itemNo;
1185 
1186 			void addItem(string label, int commandNo) {
1187 				auto lbl = cef_string_t(label);
1188 				model.insert_item_at(/* index */ itemNo, /* command id */ cef_menu_id_t.MENU_ID_USER_FIRST + commandNo, &lbl);
1189 				itemNo++;
1190 			}
1191 
1192 			void addSeparator() {
1193 				model.insert_separator_at(itemNo);
1194 				itemNo++;
1195 			}
1196 
1197 			auto flags = params.get_type_flags();
1198 
1199 			if(flags & cef_context_menu_type_flags_t.CM_TYPEFLAG_LINK) {
1200 				// cef_string_userfree_t linkUrl = params.get_unfiltered_link_url();
1201 				// toGCAndFree
1202 				addItem("Open link in new window", 1);
1203 				addItem("Copy link URL", 2);
1204 
1205 				// FIXME: open in other browsers
1206 				// FIXME: open in ytv
1207 				addSeparator();
1208 			}
1209 
1210 			if(flags & cef_context_menu_type_flags_t.CM_TYPEFLAG_MEDIA) {
1211 				// cef_string_userfree_t linkUrl = params.get_source_url();
1212 				// toGCAndFree
1213 				addItem("Open media in new window", 3);
1214 				addItem("Copy source URL", 4);
1215 				addItem("Download media", 5);
1216 				addSeparator();
1217 			}
1218 
1219 
1220 			// get_page_url
1221 			// get_title_text
1222 			// has_image_contents ???
1223 			// get_source_url
1224 			// get_xcoord and get_ycoord
1225 			// get_selection_text
1226 
1227 		}
1228 		override int run_context_menu(RC!(cef_browser_t), RC!(cef_frame_t), RC!(cef_context_menu_params_t), RC!(cef_menu_model_t), RC!(cef_run_context_menu_callback_t)) nothrow {
1229 			// could do a custom display here if i want but i think it is good enough as it is
1230 			return 0;
1231 		}
1232 		override int on_context_menu_command(RC!(cef_browser_t) browser, RC!(cef_frame_t) frame, RC!(cef_context_menu_params_t) params, int commandId, cef_event_flags_t flags) nothrow {
1233 			switch(commandId) {
1234 				case cef_menu_id_t.MENU_ID_USER_FIRST + 1: // open link in new window
1235 					auto what = params.get_unfiltered_link_url().toGCAndFree();
1236 
1237 					browser.runOnWebView((widget) {
1238 						auto event = new NewWindowRequestedEvent(what, widget);
1239 						event.dispatch();
1240 					});
1241 					return 1;
1242 				case cef_menu_id_t.MENU_ID_USER_FIRST + 2: // copy link url
1243 					auto what = params.get_link_url().toGCAndFree();
1244 
1245 					browser.runOnWebView((widget) {
1246 						auto event = new CopyRequestedEvent(what, widget);
1247 						event.dispatch();
1248 					});
1249 					return 1;
1250 				case cef_menu_id_t.MENU_ID_USER_FIRST + 3: // open media in new window
1251 					auto what = params.get_source_url().toGCAndFree();
1252 
1253 					browser.runOnWebView((widget) {
1254 						auto event = new NewWindowRequestedEvent(what, widget);
1255 						event.dispatch();
1256 					});
1257 					return 1;
1258 				case cef_menu_id_t.MENU_ID_USER_FIRST + 4: // copy source url
1259 					auto what = params.get_source_url().toGCAndFree();
1260 
1261 					browser.runOnWebView((widget) {
1262 						auto event = new CopyRequestedEvent(what, widget);
1263 						event.dispatch();
1264 					});
1265 					return 1;
1266 				case cef_menu_id_t.MENU_ID_USER_FIRST + 5: // download media
1267 					auto str = cef_string_t(params.get_source_url().toGCAndFree());
1268 					browser.get_host().start_download(&str);
1269 					return 1;
1270 				default:
1271 					return 0;
1272 			}
1273 		}
1274 		override void on_context_menu_dismissed(RC!(cef_browser_t), RC!(cef_frame_t)) nothrow {
1275 			// to close the custom display
1276 		}
1277 
1278 		override int run_quick_menu(RC!(cef_browser_t), RC!(cef_frame_t), const(cef_point_t)*, const(cef_size_t)*, cef_quick_menu_edit_state_flags_t, RC!(cef_run_quick_menu_callback_t)) nothrow {
1279 			return 0;
1280 		}
1281 		override int on_quick_menu_command(RC!(cef_browser_t), RC!(cef_frame_t), int, cef_event_flags_t) nothrow {
1282 			return 0;
1283 		}
1284 		override void on_quick_menu_dismissed(RC!(cef_browser_t), RC!(cef_frame_t)) nothrow {
1285 
1286 		}
1287 	}
1288 
1289 	class MiniguiFocusHandler : CEF!cef_focus_handler_t {
1290 		override void on_take_focus(RC!(cef_browser_t) browser, int next) nothrow {
1291 			// sdpyPrintDebugString("taking");
1292 			browser.runOnWebView(delegate(wv) {
1293 				Widget f;
1294 				if(next) {
1295 					f = Window.getFirstFocusable(wv.parentWindow);
1296 				} else {
1297 					foreach(w; &wv.parentWindow.focusableWidgets) {
1298 						if(w is wv)
1299 							break;
1300 						f = w;
1301 					}
1302 				}
1303 				if(f)
1304 					f.focus();
1305 			});
1306 		}
1307 		override int on_set_focus(RC!(cef_browser_t) browser, cef_focus_source_t source) nothrow {
1308 			/+
1309 			browser.runOnWebView((ev) {
1310 				ev.focus(); // even this can steal focus from other parts of my application!
1311 			});
1312 			+/
1313 			// sdpyPrintDebugString("setting");
1314 
1315 			// if either the parent window or the ozone window has the focus, we
1316 			// can redirect it to the input focus. CEF calls this method sometimes
1317 			// before setting the focus (where return 1 can override) and sometimes
1318 			// after... which is totally inappropriate for it to do but it does anyway
1319 			// and we want to undo the damage of this.
1320 			browser.runOnWebView((ev) {
1321 				arsd.simpledisplay.Window focus_window;
1322 				int revert_to_return;
1323 				XGetInputFocus(XDisplayConnection.get, &focus_window, &revert_to_return);
1324 				if(focus_window is ev.parentWindow.win.impl.window || focus_window is ev.ozone) {
1325 					// refocus our correct input focus
1326 					ev.parentWindow.win.focus();
1327 					XSync(XDisplayConnection.get, 0);
1328 
1329 					// and then tell the chromium thing it still has it
1330 					// so it will think it got it, lost it, then got it again
1331 					// and hopefully not try to get it again
1332 					XFocusChangeEvent eve;
1333 					eve.type = arsd.simpledisplay.EventType.FocusIn;
1334 					eve.display = XDisplayConnection.get;
1335 					eve.window = ev.ozone;
1336 					eve.mode = NotifyModes.NotifyNormal;
1337 					eve.detail = NotifyDetail.NotifyVirtual;
1338 
1339 					// sdpyPrintDebugString("Sending FocusIn hack here");
1340 
1341 					trapXErrors( {
1342 						XSendEvent(XDisplayConnection.get, ev.ozone, false, 0, cast(XEvent*) &eve);
1343 					});
1344 
1345 				}
1346 			});
1347 
1348 			return 1; // otherwise, cancel because this bullshit tends to steal focus from other applications and i never, ever, ever want that to happen.
1349 			// seems to happen because of race condition in it getting a focus event and then stealing the focus from the parent
1350 			// even though things work fine if i always cancel except
1351 			// it still keeps the decoration assuming focus though even though it doesn't have it which is kinda fucked up but meh
1352 			// it also breaks its own pop up menus and drop down boxes to allow this! wtf
1353 		}
1354 		override void on_got_focus(RC!(cef_browser_t) browser) nothrow {
1355 			// sdpyPrintDebugString("got");
1356 			browser.runOnWebView((ev) {
1357 				// this sometimes steals from the app too but it is relatively acceptable
1358 				// steals when i mouse in from the side of the window quickly, but still
1359 				// i want the minigui state to match so i'll allow it
1360 
1361 				//if(ev.parentWindow) ev.parentWindow.focus();
1362 				ev.focus();
1363 			});
1364 		}
1365 	}
1366 
1367 	class MiniguiCefClient : CEF!cef_client_t {
1368 
1369 		void delegate(scope OpenNewWindowParams) openNewWindow;
1370 
1371 		MiniguiCefLifeSpanHandler lsh;
1372 		MiniguiLoadHandler loadHandler;
1373 		MiniguiDialogHandler dialogHandler;
1374 		MiniguiDisplayHandler displayHandler;
1375 		MiniguiDownloadHandler downloadHandler;
1376 		MiniguiKeyboardHandler keyboardHandler;
1377 		MiniguiFocusHandler focusHandler;
1378 		MiniguiRequestHandler requestHandler;
1379 		MiniguiContextMenuHandler contextMenuHandler;
1380 		this(void delegate(scope OpenNewWindowParams) openNewWindow) {
1381 			this.openNewWindow = openNewWindow;
1382 			lsh = new MiniguiCefLifeSpanHandler(this);
1383 			loadHandler = new MiniguiLoadHandler();
1384 			dialogHandler = new MiniguiDialogHandler();
1385 			displayHandler = new MiniguiDisplayHandler();
1386 			downloadHandler = new MiniguiDownloadHandler();
1387 			keyboardHandler = new MiniguiKeyboardHandler();
1388 			focusHandler = new MiniguiFocusHandler();
1389 			requestHandler = new MiniguiRequestHandler();
1390 			contextMenuHandler = new MiniguiContextMenuHandler(this);
1391 		}
1392 
1393 		override cef_audio_handler_t* get_audio_handler() {
1394 			return null;
1395 		}
1396 		override cef_context_menu_handler_t* get_context_menu_handler() {
1397 			return contextMenuHandler.returnable;
1398 		}
1399 		override cef_dialog_handler_t* get_dialog_handler() {
1400 			return dialogHandler.returnable;
1401 		}
1402 		override cef_display_handler_t* get_display_handler() {
1403 			return displayHandler.returnable;
1404 		}
1405 		override cef_download_handler_t* get_download_handler() {
1406 			return downloadHandler.returnable;
1407 		}
1408 		override cef_drag_handler_t* get_drag_handler() {
1409 			return null;
1410 		}
1411 		override cef_find_handler_t* get_find_handler() {
1412 			return null;
1413 		}
1414 		override cef_focus_handler_t* get_focus_handler() {
1415 			return focusHandler.returnable;
1416 		}
1417 		override cef_jsdialog_handler_t* get_jsdialog_handler() {
1418 			// needed for alert etc.
1419 			return null;
1420 		}
1421 		override cef_keyboard_handler_t* get_keyboard_handler() {
1422 			// this can handle keyboard shortcuts etc
1423 			return keyboardHandler.returnable;
1424 		}
1425 		override cef_life_span_handler_t* get_life_span_handler() {
1426 			return lsh.returnable;
1427 		}
1428 		override cef_load_handler_t* get_load_handler() {
1429 			return loadHandler.returnable;
1430 		}
1431 		override cef_render_handler_t* get_render_handler() {
1432 			// this thing might work for an off-screen thing
1433 			// like to an image or to a video stream maybe
1434 			//
1435 			// might be useful to have it render here then send it over too for remote X sharing a process
1436 			return null;
1437 		}
1438 		override cef_request_handler_t* get_request_handler() {
1439 			return requestHandler.returnable;
1440 		}
1441 		override int on_process_message_received(RC!cef_browser_t, RC!cef_frame_t, cef_process_id_t, RC!cef_process_message_t) {
1442 			return 0; // return 1 if you can actually handle the message
1443 		}
1444 		override cef_frame_handler_t* get_frame_handler() nothrow {
1445 			return null;
1446 		}
1447 		override cef_print_handler_t* get_print_handler() nothrow {
1448 			return null;
1449 		}
1450 
1451 		override cef_command_handler_t* get_command_handler() {
1452 			return null;
1453 		}
1454 
1455 		override cef_permission_handler_t* get_permission_handler() {
1456 			return null;
1457 		}
1458 
1459 	}
1460 }
1461 
1462 class BrowserClosedEvent : Event {
1463 	enum EventString = "browserclosed";
1464 
1465 	this(Widget target) { super(EventString, target); }
1466 	override bool cancelable() const { return false; }
1467 }
1468 
1469 class CopyRequestedEvent : Event {
1470 	enum EventString = "browsercopyrequested";
1471 
1472 	string what;
1473 
1474 	this(string what, Widget target) { this.what = what; super(EventString, target); }
1475 	override bool cancelable() const { return false; }
1476 }
1477 
1478 class NewWindowRequestedEvent : Event {
1479 	enum EventString = "browserwindowrequested";
1480 
1481 	string url;
1482 
1483 	this(string url, Widget target) { this.url = url; super(EventString, target); }
1484 	override bool cancelable() const { return false; }
1485 }
1486 
1487 
1488 
1489 /+
1490 pragma(mangle, "_ZN12CefWindowX115FocusEv")
1491 //pragma(mangle, "_ZN3x116XProto13SetInputFocusERKNS_20SetInputFocusRequestE")
1492 extern(C++)
1493 export void _ZN12CefWindowX115FocusEv() {
1494 	sdpyPrintDebugString("OVERRIDDEN");
1495 }
1496 +/