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