Introduction
Recently I was asked to implement a webview from unity editor. My client requested the following:
- an editor window- with all its util
- basic chromium functionality (load url, load files, execute js and etc...)
- receiving callbacks from the js
- and make it simple :0)
This was a very hard assignment due to all the reflection necessary and deep understanding of the unity callback mechanism. It took me more than a month to figure how to do it correctly. So let me help you make it less than 5 minutes .
Background
Unity has its own wrapper for chromium, so you don't really need to use the OS API or implement it by yourself, though it may be useful in some other projects. Furthermore, implement chromium by yourself is REALLY expensive when it comes to size. If you are going to distribute you code, you should better think about it.
Another issue, Unity 5.4 and newer versions are really different from earlier version in all the web implementation area. I truly recommend to decompile the unity editor and examine the files.
Using the code
As I was saying Unity 5.4 and newer versions are very much different regarding to the way they implemented their entire web-view design.
Up until 5.4 I was capable to use the web-view DLL (by reflection) invoke its initialization and receive a functioning web-view window that can be docked or float and call web-view functions like "onLoadError" to receive callbacks with no problem ... but this is not the current case.
Currently using just the "web-view" DLL will give me partly functioning window that can't be docked of re-float after docking simply because of the way unity save the object rendered over the window,
This paragraph is for those of you who don't really know what is chromium web-view, let's take it in baby steps (it took me few days to get it myself, so don't worry).
In general, just think about the Asset-store. The Asset-store window magically uses the web from unity. Well ... once you decompile unity you see that there is no magic at all, just a very disorganize code with lots of inner hacking and there is a very good reason why it is not served as API.
Taking it from the asset store decompile:
private void InitWebView(Rect webViewRect)
{
this.m_CurrentSkin = EditorGUIUtility.skinIndex;
this.m_IsDocked = this.docked;
this.m_IsOffline = false;
if (!(bool) this.webView)
{
int x = (int) webViewRect.x;
int y = (int) webViewRect.y;
int width = (int) webViewRect.width;
int height = (int) webViewRect.height;
this.webView = ScriptableObject.CreateInstance<WebView>();
this.webView.InitWebView((GUIView) this.m_Parent, x, y, width, height, false);
this.webView.hideFlags = HideFlags.HideAndDontSave;
this.webView.AllowRightClickMenu(true);
if (this.hasFocus)
this.SetFocus(true);
}
this.webView.SetDelegateObject((ScriptableObject) this);
this.webView.LoadFile(AssetStoreUtils.GetLoaderPath());
}
So what you need in order to call an asset store window is "rectangle", "this" prameter- which is the actual window, and (GUIVIEW).m_Parent
-which is a private member that exists in editorwindow and holds the docking area. That is actually the most problematic part. By reflection you cant always get the m_parent
. But this pararmeter is necessary when docking or un docking a window.
The asset store deals with this by calling js to update.
AssetStoreContext.GetInstance().docked = this.docked;
this.InvokeJSMethod("document.AssetStore", "updateDockStatus");
So, make a long story shot, this is not the right way to deal with it. After doing some investigation I realize what I was looking for is a connection between webview and tabbed window.
I found this DLL "UnityEditor.Web.WebViewEditorWindowTabs" which was almost perfect. worked as expected regarding to window util such as dock and un dock, but in order to recieve callbacks or call additional chromium features I needed the instance of the window, the object webview and the actual window. There is no easy way to get them, unfortunatly.
I ended up implementing my own "Create
" function that creates the window and take the webview at the same time. The nice thing about this DLL is the the m_parent
is not requierd and I don't need to use "set host" reflection function.
What I did was:
public static T CreateWebViewEditorWindow<T>(string title, string sourcesPath, int minWidth, int minHeight, int maxWidth, int maxHeight) where T : CustomWebViewEditorWindow, new(){
var createMethod = webViewEditorWindowType.GetMethod("Create", BindingFlags.Static | BindingFlags.Public | BindingFlags.FlattenHierarchy).MakeGenericMethod(webViewEditorWindowType);
var window = createMethod.Invoke(null, new object[] {
title,
sourcesPath,
minWidth,
minHeight,
maxWidth,
maxHeight
});
var customWebEditorWindow = new T { webViewEditorWindow = window };
EditorApplication.delayCall += () =>
{
EditorApplication.delayCall += () =>
{
webView = webViewEditorWindowType.GetField("m_WebView", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(customWebEditorWindow.webViewEditorWindow);
AddGlobalObject(Type.GetType("webView"));
};
};
I got this great Idea from this github:
https://github.com/kimsama/Unity-WebViewEditorWindow/blob/master/Assets/Editor/CustomWebViewEditorWindow.cs
So yes, I coppied the webvieweidtor decompiled "create window" template and took the webview object, the window and its instant. But I also wanted this script to talk with js, so I had to make an instant of jsproxymgr and give it all my globals. See the attached code for explenations.
From this point it gets to be a lot easier since I have all I need to work with chromium. Now I have the window, the window instant, and my webview object that I reflected from the DLL. I still want all the chromium functionality and for that I used reflection. For example:
public void LoadURL(string path)
{
try
{
if (webViewEditorWindow != null && webView != null)
{
MethodInfo invokeLoadURLMethod = webView.GetType().GetMethod("LoadURL", BindingFlags.Static | BindingFlags.Public | BindingFlags.FlattenHierarchy | BindingFlags.NonPublic | BindingFlags.Instance);
if (invokeLoadURLMethod != null)
{
object[] param = new object[] { path };
invokeLoadURLMethod.Invoke(webView, param);
}
}
}
catch (TargetInvocationException ex)
{
webView = null;
EditorApplication.update = null;
Debug.LogFormat("{0}", ex.Message);
return;
}
}
Using the webview I took when I implemented an instant of the the window.
So now I have a way to set up a web view window that can be docked and undocked, the webview object to call unity chromium wrapper and all that is left is to see how I can get callbacks from the HTML to my window.
For this I used unityAsync
this mechanism acts like coroutine. It is well documented way of send and recieve messages from js to unity by json format. I started by invoke the init with a link to the html file:
[MenuItem("Window/webViewImp")]
private static void Open()
{
string path = "file:///{0}/Assets/StreamingAssets/test/index.html";
var w = CreateWebViewEditorWindow<webViewImp>("webViewImp", path, 200, 530, 800, 600);
}
The first parameter is the name of the window, the second parameter is the full file name and the four other parameters are the rect (x ,y ,width , height). Motice that I take the actual window and not just scriptableObject
instance here with "var w". It will become useful when I execute js or load url. You have to make sure you have the same name of functions in the script such as in the html and don't forget to set them to be public.
See the example attached.