1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384 |
- using System;
- using UnityEngine;
- using System.Collections.Generic;
- using System.Runtime.InteropServices;
- using System.Runtime.Serialization;
- using UnityEngine.Assertions;
- using UnityEngine.Serialization;
- #if UNITY_EDITOR
- using UnityEditor;
- #endif
- #if UNITY_5_5_OR_NEWER
- using UnityEngine.Profiling;
- #endif
- namespace ZenFulcrum.EmbeddedBrowser {
- /** Represents a browser "tab". */
- public class Browser : MonoBehaviour {
- private static int lastUpdateFrame;
- public static string LocalUrlPrefix { get { return BrowserNative.LocalUrlPrefix; } }
- /**
- * List of possible actions when a new window is opened.
- */
- [Flags]
- public enum NewWindowAction {
- /** Ignore attempts to open new windows. */
- Ignore = 1,
- /** Navigate the current window to the new window's URL. */
- Redirect,
- /**
- * Create a new Browser instance to handle rendering the new window in the scene.
- * For this to be useful, you'll need to supply an INewWindowHandler with an
- * implementation of your choosing.
- * (If you set this behavior in the inspector, it won't take effect until you call SetNewWindowHandler.)
- */
- NewBrowser,
- /**
- * Create a new OS window, outside the game, to show the page.
- * Controlling and interacting with the new window outside is limited, though you can use JavaScript calls
- * from the parent.
- * OS-level windows may have unexpected or incomplete behavior. Using this outside of debugging/testing
- * is not officially supported.
- */
- NewWindow,
- }
- protected IBrowserUI _uiHandler;
- protected bool uiHandlerAssigned = false;
- /**
- * Input handler.
- * If you don't assign anything, it will default to something useful, but you can replace
- * it or null it as desired.
- *
- * If do you want to use your own or disable it, be sure to assign something (or null) before WhenReady fires.
- */
- public IBrowserUI UIHandler {
- get { return _uiHandler; }
- set {
- uiHandlerAssigned = true;
- _uiHandler = value;
- }
- }
- [Tooltip("Initial URL to load.\n\nTo change at runtime use browser.Url to load a page.")]
- [SerializeField] private string _url = "";
- [Tooltip("Initial size.\n\nTo change at runtime use browser.Resize.")]
- [SerializeField] private int _width = 512, _height = 512;
- [Tooltip(@"Generate mipmaps?
- Generating mipmaps tends to be somewhat expensive, especially when updating a large texture every frame. Instead of
- generating mipmaps, try using one of the ""emulate mipmap"" shader variants.
- To change at runtime modify this value and call browser.Resize.")]
- public bool generateMipmap = false;
- [Tooltip(@"Base background color to use for pages.
- The texture will be cleared to this color until the page has rendered. Additionally, if baseColor.a is not
- fully opaque the browser will render transparently. (Don't forget to use an appropriate material for transparency.)
- Don't change this after the first Update() tick. (You can still tweak a page via EvalJS and CSS.)")]
- [FormerlySerializedAs("backgroundColor")]
- public Color32 baseColor = new Color32(0, 0, 0, 0);//default to transparent
- [Tooltip(@"Initial browser ""zoom level"". Negative numbers are smaller, zero is normal, positive numbers are larger.
- The size roughly doubles/halves for every four units added/removed.
- Note that zoom level is shared by all pages on the some domain.
- Also note that this zoom level may be persisted across runs.
- To change at runtime use browser.Zoom.")]
- //prefer deviceScale (not yet implemented) for DPI-style size changes.
- [SerializeField] private float _zoom = 0;
- /**
- * Fired when we get a console.log/warn/error from the page.
- * args: (message, source)
- *
- * (CEF's console event leaves a lot to be desired, we are unable to get the log level or additional arguments.)
- */
- public event Action<string, string> onConsoleMessage = (s, s1) => {};
- [Tooltip(@"Allow right-clicking to show a context menu on what parts of the page?
- May be changed at any time.
- ")]
- [FlagsField]
- public BrowserNative.ContextMenuOrigin allowContextMenuOn = BrowserNative.ContextMenuOrigin.Editable;
- [Tooltip(@"What should we do when a user/the page tries to open a new window?
- For ""New Browser"" to work, you need to assign NewWindowHandler to a handler of your creation.
- Don't use ""New Window"" outside debugging and testing.
- Use SetNewWindowHandler to adjust at runtime.
- ")]
- [SerializeField]
- private NewWindowAction newWindowAction = NewWindowAction.Redirect;
- [Obsolete("Use SetNewWindowHandler", true)]
- public INewWindowHandler NewWindowHandler { get; set; }
- /** If false, the texture won't be updated with new changes. */
- public bool EnableRendering { get; set; }
- /** If false, we won't process input with the UIHandler. */
- public bool EnableInput { get; set; }
- public CookieManager CookieManager { get; private set; }
- /** Handle to the native browser. */
- internal protected int browserId;
- /** Same as browserId, but will be set before the browser is ready and remain set even after it's disposed */
- private int unsafeBrowserId;
- /** Have we requested a native handle yet? (It may take a moment for the native browser to be ready.) */
- protected bool browserIdRequested = false;
- protected Texture2D texture;
- public Texture2D Texture { get { return texture; } }
- /** Called when the image canvas has changed or resized. */
- public event Action<Texture2D> afterResize = t => { };
- protected bool textureIsOurs = false;
- protected bool forceNextRender = true;
- protected bool isPopup = false;
- /** List of tasks to execute on the main thread. May be used on any thread, but lock before touching. */
- protected List<Action> thingsToDo = new List<Action>();
- /** List of callbacks to call when the page loads next. */
- protected List<Action> onloadActions = new List<Action>();
- /**
- * We pass delegates/closures to the native level. We must ensure that they don't get GC'd
- * while the native object still exists and might use them, so we keep track of them here
- * to prevent that.
- */
- protected List<object> thingsToRemember = new List<object>();
- /**
- * And, to make it more complicated, in some cases we can get GC'd (along with thingsToRemember and the
- * generated trampolines) before the native browser finishes shutting down.
- *
- * We use this to make sure {this} stays alive until the native side is done.
- *
- * Used across threads, lock before touching.
- */
- protected static Dictionary<int, List<object>> allThingsToRemember = new Dictionary<int, List<object>>();
- /** A callback. {args} is a JSON node with the top-level type of array. */
- public delegate void JSCallback(JSONNode args);
- protected delegate void JSResultFunc(JSONNode value, bool isError);
- private int nextCallbackId = 1;
- /** Registered callbacks that JS can call to us with. */
- protected Dictionary<int, JSResultFunc> registeredCallbacks = new Dictionary<int, JSResultFunc>();
- /**
- * We can't do much (go to url, navigate, etc) until the native browser is ready.
- * Most these actions will be queued for you and fired when we are ready.
- *
- * See also: WhenReady()
- */
- protected event BrowserNative.ReadyFunc onNativeReady;
- /**
- * Called when the page's onload fires. (Top frame only.)
- * loadData['status'] contains the status code, loadData['url'] the url
- */
- public event Action<JSONNode> onLoad = loadData => {};
- /**
- * Called when the top-level page has been fetched (but not necessarily parsed and run).
- * loadData['status'] contains the status code, loadData['url'] the url
- * (Top frame only.)
- */
- [Obsolete("Doesn't fire reliably due to its design. Consider using onLoad or onNavStateChange.")]
- public event Action<JSONNode> onFetch = loadData => {};
- /**
- * Called when a page fails to load.
- * Use QueuePageReplacer to inject a custom error page.
- * (Top frame only.)
- */
- public event Action<JSONNode> onFetchError = errCode => {};
- /**
- * Called when an SSL cert fails checks.
- * Use QueuePageReplacer to inject a custom error page.
- * (Top frame only.)
- */
- public event Action<JSONNode> onCertError = errInfo => {};
- /**
- * Called when a renderer process dies/is killed.
- * Use QueuePageReplacer to inject a custom error page; you might also choose to try reloading once or twice.
- */
- public event Action onSadTab = () => {};
- /**
- * Called after the browser's texture/image data is updated.
- */
- public event Action onTextureUpdated = () => {};
- /// <summary>
- /// Called when the browser's nav state changes.
- /// Presently these are considered nav state changes, but other things may be added in the future:
- /// - URL change
- /// - canGoForward/Back change
- /// - loading started or completed
- /// </summary>
- public event Action onNavStateChange = () => {};
- /**
- * Called when a download is started.
- * See BrowserNative.ChangeType.CHT_DOWNLOAD_STARTED for a list and explanation of
- * the elements in the JSON object.
- *
- * If a handler is given it should call DownloadCommand() to start or cancel the download (eventually).
- * Once a download is started onDownloadStatus will be called from time-to-time. Additionally, you can use
- * DownloadCommand to cancel, pause, or resume a running download.
- *
- * If this is null, no downloading will happen.
- */
- public Action<int, JSONNode> onDownloadStarted = null;
- /**
- * Called when a download has a status update.
- * See BrowserNative.ChangeType.CHT_DOWNLOAD_STATUS for a list and explanation of
- * the elements in the JSON object.
- *
- * NB: You may get status reports on downloads that haven't triggered onDownloadStarted yet.
- */
- public event Action<int, JSONNode> onDownloadStatus = (downloadId, info) => {};
- /**
- * Called when the element in the page with keyboard focus changes.
- * If tagName == "", then focus has been lost.
- */
- public event Action<string, bool, string> onNodeFocus = (tagName, editable, value) => {};
- /// <summary>
- /// Called when the browser (as a whole) gains/loses keyboard or mouse focus.
- /// </summary>
- public event Action<bool, bool> onBrowserFocus = (mouseFocused, keyboardFocused) => {};
- [HideInInspector]
- public readonly BrowserFocusState focusState = new BrowserFocusState();
- /// <summary>
- /// Called when any browser is created.
- /// </summary>
- public static event Action<Browser> onAnyBrowserCreated = browser => {};
- /// <summary>
- /// Called when any browser is destroyed.
- /// </summary>
- public static event Action<Browser> onAnyBrowserDestroyed = browser => {};
- private BrowserInput browserInput;
- private Browser overlay;
- /** We have to load a blank page before we can inject HTML. If we load a blank page, don't count it as the "loading". */
- private bool skipNextLoad;
- /** There may be a short moment between requesting a URL and when IsLoadedRaw turns false. We use this flag to help cope. */
- private bool loadPending;
- private BrowserNavState navState = new BrowserNavState();
- private bool newWindowHandlerSet = false;
- /**
- * This will sometimes contain an inner Browser that handles tasks such as
- * rendering alert()s and such.
- */
- protected DialogHandler dialogHandler;
- protected void Awake() {
- EnableRendering = true;
- EnableInput = true;
- CookieManager = new CookieManager(this);
- browserInput = new BrowserInput(this);
- if (!newWindowHandlerSet) {
- //(if another component calls SetNewWindowHandler in its Awake, we'll overwrite that
- //so only do this if it's not been called yet)
- SetNewWindowHandler(newWindowAction == NewWindowAction.NewBrowser ? NewWindowAction.Ignore : newWindowAction, null);
- }
- onNativeReady += id => {
- if (!uiHandlerAssigned) {
- var meshCollider = GetComponent<MeshCollider>();
- if (meshCollider) {
- var ui = gameObject.AddComponent<PointerUIMesh>();
- gameObject.AddComponent<CursorRendererOS>();
- UIHandler = ui;
- }
- }
- Resize(_width, _height);
- Zoom = _zoom;
- if (!isPopup && !string.IsNullOrEmpty(_url)) Url = _url;
- };
- onConsoleMessage += (message, source) => {
- var text = source + ": " + message;
- Debug.Log(text, this);
- };
- onFetchError += err => {
- //don't show anything if the error is a load abort
- if (err["error"] == "ERR_ABORTED") return;
- QueuePageReplacer(() => {
- LoadHTML(Resources.Load<TextAsset>("Browser/Errors").text, Url);
- CallFunction("setErrorInfo", err);
- }, -1000);
- };
- onCertError += err => {
- QueuePageReplacer(() => {
- LoadHTML(Resources.Load<TextAsset>("Browser/Errors").text, Url);
- CallFunction("setErrorInfo", err);
- }, -900);
- };
- onSadTab += () => {
- QueuePageReplacer(() => {
- LoadHTML(Resources.Load<TextAsset>("Browser/Errors").text, Url);
- CallFunction("showCrash");
- }, -1000);
- };
- onAnyBrowserCreated(this);
- }
- /** Returns true if the browser is ready to take orders. Most actions will be internally delayed until it is. */
- public bool IsReady {
- get { return browserId != 0; }
- }
- /**
- * The given callback will be called when the browser is ready to start taking commands.
- */
- public void WhenReady(Action callback) {
- if (IsReady) {
- //Call it later instead of now to help head off some subtle bugs that can be produced by such a scheme.
- //Call it at next update. (Since our script order is a little bit later than everyone else this usually will add no latency.)
- lock (thingsToDo) thingsToDo.Add(callback);
- } else {
- BrowserNative.ReadyFunc func = null;
- func = id => {
- try {
- callback();
- } catch (Exception ex) {
- Debug.LogException(ex);
- }
- onNativeReady -= func;
- };
- onNativeReady += func;
- }
- }
- /** Fires the given callback during th next Update/LateUpdate tick on the main thread. This may be called from any thread. */
- public void RunOnMainThread(Action callback) {
- lock (thingsToDo) thingsToDo.Add(callback);
- }
- /**
- * Calls the given callback the next time the page is loaded.
- * This will not fire right away if IsLoaded is true, it will wait for a new page to load.
- * Callbacks won't be fired yet if the url is some type of blank url ("", "about:blank", etc).
- */
- public void WhenLoaded(Action callback) {
- onloadActions.Add(callback);
- }
- /**
- * Sets up a new native browser.
- * If newBrowserId is zero, allocates a new browser and sets it up.
- * If newBrowserId is nonzero, takes ownership of that allocated browser and sets it up.
- *
- * Internal use only.
- */
- internal void RequestNativeBrowser(int newBrowserId = 0) {
- if (browserId != 0 || browserIdRequested) return;
- browserIdRequested = true;
- try {
- BrowserNative.LoadNative();
- } catch {
- gameObject.SetActive(false);
- throw;
- }
- int newId;
- if (newBrowserId == 0) {
- var settings = new BrowserNative.ZFBSettings() {
- bgR = baseColor.r,
- bgG = baseColor.g,
- bgB = baseColor.b,
- bgA = baseColor.a,
- offscreen = 1,
- };
- newId = BrowserNative.zfb_createBrowser(settings);
- } else {
- newId = newBrowserId;
- isPopup = true;//don't nav to our to URL, it will be loaded by the backend
- }
- unsafeBrowserId = newId;
- //Debug.Log("Requested browser for " + name + " " + newId);
- //We have a native browser, but it is invalid to do anything with it until it's ready.
- //Therefore, we don't set browserId until it's ready.
- //But we will put all our callbacks in place.
- //Don't let our remember list get destroyed until we are ready for that.
- lock (allThingsToRemember) allThingsToRemember[newId] = thingsToRemember;
- BrowserNative.ForwardJSCallFunc forwardCall = (bId, id, data, size) => {
- lock (thingsToDo) thingsToDo.Add(() => {
- JSResultFunc cb;
- if (!registeredCallbacks.TryGetValue(id, out cb)) {
- Debug.LogWarning("Got a JS callback for event " + id + ", but no such event is registered.");
- return;
- }
- var isError = false;
- if (data.StartsWith("fail-")) {
- isError = true;
- data = data.Substring(5);
- }
- JSONNode node;
- try {
- node = JSONNode.Parse(data);
- } catch (SerializationException) {
- Debug.LogWarning("Invalid JSON sent from browser: " + data);
- return;
- }
- try {
- cb(node, isError);
- } catch (Exception ex) {
- //user's function died, log it and carry on
- Debug.LogException(ex);
- return;
- }
- });
- };
- thingsToRemember.Add(forwardCall);
- BrowserNative.zfb_registerJSCallback(newId, forwardCall);
- BrowserNative.ChangeFunc changeCall = (id, type, arg1) => {
- //(Note: we may have been Object.Destroy'd at this point, so guard against that.)
- if (type == BrowserNative.ChangeType.CHT_BROWSER_CLOSE) {
- //We can't continue if the browser is closed, so goodbye.
- //At this point, we may or may not be destroyed, but if not, become destroyed.
- //Debug.Log("Got close notification for " + unsafeBrowserId);
- if (this) {
- //Need to be destroyed.
- lock (thingsToDo) thingsToDo.Add(() => {
- Destroy(gameObject);
- });
- } else {
- //If we are (Unity) destroyed, we won't get another update, so we can't rely on thingsToDo
- //That said, there's not anything else for us to do but step out of allThingsToRemember.
- }
- //The native side has acknowledged it's done, now we can finally let the native trampolines be GC'd
- lock (allThingsToRemember) {
- allThingsToRemember.Remove(unsafeBrowserId);
- }
- //Just in case someone tries to call something, make sure CheckSanity and such fail.
- browserId = 0;
- } else if (this) {
- lock (thingsToDo) thingsToDo.Add(() => OnItemChange(type, arg1));
- }
- };
- thingsToRemember.Add(changeCall);
- BrowserNative.zfb_registerChangeCallback(newId, changeCall);
- BrowserNative.DisplayDialogFunc dialogCall = (id, type, textPtr, promptTextPtr, urlPtr) => {
- var text = Util.PtrToStringUTF8(textPtr);
- var promptText = Util.PtrToStringUTF8(promptTextPtr);
- //var url = Util.PtrToStringUTF8(urlPtr);
- lock (thingsToDo) thingsToDo.Add(() => {
- CreateDialogHandler();
- dialogHandler.HandleDialog(type, text, promptText);
- });
- };
- thingsToRemember.Add(dialogCall);
- BrowserNative.zfb_registerDialogCallback(newId, dialogCall);
- BrowserNative.ShowContextMenuFunc contextCall = (id, json, x, y, origin) => {
- if (json != null && (allowContextMenuOn & origin) == 0) {
- //ignore this
- BrowserNative.zfb_sendContextMenuResults(browserId, -1);
- return;
- }
- lock (thingsToDo) thingsToDo.Add(() => {
- if (json != null) CreateDialogHandler();
- if (dialogHandler != null) dialogHandler.HandleContextMenu(json, x, y);
- });
- };
- thingsToRemember.Add(contextCall);
- BrowserNative.zfb_registerContextMenuCallback(newId, contextCall);
- BrowserNative.ConsoleFunc consoleCall = (id, message, source, line) => {
- lock (thingsToDo) thingsToDo.Add(() => {
- onConsoleMessage(message, source + ":" + line);
- });
- };
- thingsToRemember.Add(consoleCall);
- BrowserNative.zfb_registerConsoleCallback(newId, consoleCall);
- BrowserNative.ReadyFunc readyCall = id => {
- Assert.AreEqual(newId, id);
- //We could be on any thread at this time, so schedule the callbacks to fire during the next InputUpdate
- lock (thingsToDo) thingsToDo.Add(() => {
- browserId = newId;
- // ReSharper disable once PossibleNullReferenceException
- onNativeReady(browserId);
- });
- };
- thingsToRemember.Add(readyCall);
- BrowserNative.zfb_setReadyCallback(newId, readyCall);
- BrowserNative.NavStateFunc navStateCall = (id, back, forward, loading, urlRaw) => {
- var url = Util.PtrToStringUTF8(urlRaw);
- lock (thingsToDo) thingsToDo.Add(() => {
- navState.canGoBack = back;
- navState.canGoForward = forward;
- navState.loading = loading;
- navState.url = url;
- _url = url;//update the inspector
- onNavStateChange();
- });
- };
- thingsToRemember.Add(navStateCall);
- BrowserNative.zfb_registerNavStateCallback(newId, navStateCall);
- }
- protected void OnItemChange(BrowserNative.ChangeType type, string arg1) {
- //Debug.Log("ChangeType " + name + " " + type + " arg " + arg1 + " loaded " + IsLoaded);
- switch (type) {
- case BrowserNative.ChangeType.CHT_CURSOR:
- UpdateCursor();
- break;
- case BrowserNative.ChangeType.CHT_BROWSER_CLOSE:
- //handled directly on the calling thread, nothing to do here
- break;
- case BrowserNative.ChangeType.CHT_FETCH_FINISHED:
- #pragma warning disable 618
- onFetch(JSONNode.Parse(arg1));
- #pragma warning restore 618
- break;
- case BrowserNative.ChangeType.CHT_FETCH_FAILED:
- onFetchError(JSONNode.Parse(arg1));
- break;
- case BrowserNative.ChangeType.CHT_LOAD_FINISHED:
- if (skipNextLoad) {
- //deal with extra step we have to do to load HTML to an empty page
- skipNextLoad = false;
- return;
- }
- loadPending = false;
- if (onloadActions.Count != 0) {
- foreach (var action in onloadActions) action();
- onloadActions.Clear();
- }
- onLoad(JSONNode.Parse(arg1));
- break;
- case BrowserNative.ChangeType.CHT_CERT_ERROR:
- onCertError(JSONNode.Parse(arg1));
- break;
- case BrowserNative.ChangeType.CHT_SAD_TAB:
- onSadTab();
- break;
- case BrowserNative.ChangeType.CHT_DOWNLOAD_STARTED: {
- var info = JSONNode.Parse(arg1);
- if (onDownloadStarted != null) {
- onDownloadStarted(info["id"], info);
- } else {
- DownloadCommand(info["id"], BrowserNative.DownloadAction.Cancel);
- }
- break;
- }
- case BrowserNative.ChangeType.CHT_DOWNLOAD_STATUS: {
- var info = JSONNode.Parse(arg1);
- onDownloadStatus(info["id"], info);
- break;
- }
- case BrowserNative.ChangeType.CHT_FOCUSED_NODE: {
- var info = JSONNode.Parse(arg1);
- focusState.focusedTagName = info["TagName"];
- focusState.focusedNodeEditable = info["editable"];
- onNodeFocus(info["tagName"], info["editable"], info["value"]);
- break;
- }
- }
- }
- protected void CreateDialogHandler() {
- if (dialogHandler != null) return;
- DialogHandler.DialogCallback dialogCallback = (affirm, text1, text2) => {
- CheckSanity();
- BrowserNative.zfb_sendDialogResults(browserId, affirm, text1, text2);
- };
- DialogHandler.MenuCallback contextCallback = commandId => {
- CheckSanity();
- BrowserNative.zfb_sendContextMenuResults(browserId, commandId);
- };
- dialogHandler = DialogHandler.Create(this, dialogCallback, contextCallback);
- }
- /**
- * Call this before you do any native things with our browser instance.
- * If something terribly stupid is going on this may be able to bail out with an exception instead of
- * crashing everything.
- */
- protected void CheckSanity() {
- if (browserId == 0) throw new InvalidOperationException("No native browser allocated");
- if (!BrowserNative.SymbolsLoaded) throw new InvalidOperationException("Browser .dll not loaded");
- }
- /**
- * If we aren't ready, queues the given action to happen later and returns true.
- * Else calls CheckSanity and returns false.
- */
- internal bool DeferUnready(Action ifNotReady) {
- if (browserId == 0) {
- WhenReady(ifNotReady);
- return true;
- } else {
- CheckSanity();
- return false;
- }
- }
- protected void OnDisable() {
- //note: if you want a browser to stop, load a different page or destroy it
- //The browser will continue to run until destroyed.
- }
- protected void OnDestroy() {
- onAnyBrowserDestroyed(this);
- if (browserId == 0) return;
- if (dialogHandler) DestroyImmediate(dialogHandler.gameObject);
- dialogHandler = null;
- if (BrowserNative.SymbolsLoaded) BrowserNative.zfb_destroyBrowser(browserId);
- if (textureIsOurs) Destroy(texture);
- browserId = 0;
- texture = null;
- }
- public string Url {
- /**
- * Gets the current browser URL.
- * Note that if you just set the URL and the page hasn't loaded, this won't return the new value.
- * It always returns the current URL of the browser as we are most recently aware of.
- */
- get {
- return navState.url;
- }
- /** Shortcut for LoadURL(value, true) */
- set {
- LoadURL(value, true);
- }
- }
- /**
- * Navigates to the given URL. If force is true, it will go there right away.
- * If force is false, pages that wish to can prompt the user and possibly cancel the
- * navigation.
- */
- public void LoadURL(string url, bool force) {
- if (DeferUnready(() => LoadURL(url, force))) return;
- const string magicPrefix = "localGame://";
- if (url.StartsWith(magicPrefix)) {
- url = LocalUrlPrefix + url.Substring(magicPrefix.Length);
- }
- if (string.IsNullOrEmpty(url)) {
- //If we ask CEF to load "" it will crash. Try Url = "about:blank" or LoadHTML() instead.
- throw new ArgumentException("URL must be non-empty", "value");
- }
- loadPending = true;
- BrowserNative.zfb_goToURL(browserId, url, force);
- }
- /**
- * Loads the given HTML string as if it were the given URL.
- * For the URL use http://-like porotocols or else things may not work right. (In particular, the backend
- * might sanitize it to "about:blank" and things won't work right because it appears a page isn't loaded.)
- *
- * Note that, instead of using this, you can also load "data:" URIs into this.Url.
- * This allows pretty much any type of content to be loaded as the whole page.
- */
- public void LoadHTML(string html, string url = null) {
- if (DeferUnready(() => LoadHTML(html, url))) return;
- //Debug.Log("Load HTML " + html);
- loadPending = true;
- if (string.IsNullOrEmpty(url)) {
- url = LocalUrlPrefix + "custom";
- }
- if (string.IsNullOrEmpty(this.Url)) {
- //Nothing will happen if we don't have an initial page, so cause one.
- this.Url = "about:blank";
- skipNextLoad = true;
- }
- BrowserNative.zfb_goToHTML(browserId, html, url);
- }
- /// <summary>
- /// Sets how new popup windows are handled.
- /// </summary>
- /// <param name="action"></param>
- /// <param name="newWindowHandler">
- /// If action==NewBrowser, this handler will be invoked to create the browser in the scene.
- /// May be null otherwise.
- /// </param>
- public void SetNewWindowHandler(NewWindowAction action, INewWindowHandler newWindowHandler) {
- newWindowHandlerSet = true;
- if (action == NewWindowAction.NewBrowser && newWindowHandler == null) {
- throw new Exception("No new window handler supplied for NewBrowser action");
- }
- if (DeferUnready(() => SetNewWindowHandler(action, newWindowHandler))) return;
- var settings = new BrowserNative.ZFBSettings() {
- bgR = baseColor.r,
- bgG = baseColor.g,
- bgB = baseColor.b,
- bgA = baseColor.a,
- };
- BrowserNative.NewWindowFunc cb = (id, newBrowserId, urlPtr) => {
- #if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX
- var url = Util.PtrToStringUTF8(urlPtr);
- if (url == "about:inspector" || newWindowAction == NewWindowAction.NewWindow) lock (thingsToDo) {
- thingsToDo.Add(() => {
- PopUpBrowser.Create(newBrowserId);
- });
- return;
- }
- #endif
-
- lock (thingsToDo) {
- thingsToDo.Add(() => {
- var newBrowser = newWindowHandler.CreateBrowser(this);
- newBrowser.RequestNativeBrowser(newBrowserId);
- });
- }
- };
- thingsToRemember.Add(cb);
- BrowserNative.zfb_registerPopupCallback(browserId, (BrowserNative.NewWindowAction)action, settings, cb);
- }
- /**
- * Sends a command such as "select all", "undo", or "copy"
- * to the currently focused frame in th browser.
- */
- public void SendFrameCommand(BrowserNative.FrameCommand command) {
- if (DeferUnready(() => SendFrameCommand(command))) return;
- BrowserNative.zfb_sendCommandToFocusedFrame(browserId, command);
- }
- private Action pageReplacer;
- private float pageReplacerPriority;
- /**
- * Queues a function to replace the current page.
- *
- * This is used mostly in error handling. Namely, the default error handler will queue an error page at a low
- * priority, but your onLoadError callback can call this to queue its own error page.
- *
- * At the end of the tick, the {replacePage} callback with the highest priority will
- * be called. Typically {replacePage} will call LoadHTML to change things around.
- *
- * Default error handles will have a priority of less than -100.
- */
- public void QueuePageReplacer(Action replacePage, float priority) {
- if (pageReplacer == null || priority >= pageReplacerPriority) {
- pageReplacer = replacePage;
- pageReplacerPriority = priority;
- }
- }
- public bool CanGoBack {
- get {
- return navState.canGoBack;
- }
- }
- public void GoBack() {
- if (!IsReady) return;
- CheckSanity();
- BrowserNative.zfb_doNav(browserId, -1);
- }
- public bool CanGoForward {
- get {
- return navState.canGoForward;
- }
- }
- public void GoForward() {
- if (!IsReady) return;
- CheckSanity();
- BrowserNative.zfb_doNav(browserId, 1);
- }
- /**
- * Returns true if the browser is loading a page.
- * Unlike IsLoaded, this does not account for special case urls.
- */
- public bool IsLoadingRaw {
- get {
- return navState.loading;
- }
- }
- /**
- * Returns true if we have a page and it's loaded.
- * This will not return true if we haven't gone to a URL or we are on "about:blank"
- */
- public bool IsLoaded {
- get {
- if (!IsReady || loadPending) return false;
- if (navState.loading) return false;
- var url = Url;
- var urlIsBlank = string.IsNullOrEmpty(url) || url == "about:blank";
- return !urlIsBlank;
- }
- }
- public void Stop() {
- if (!IsReady) return;
- CheckSanity();
- BrowserNative.zfb_changeLoading(browserId, BrowserNative.LoadChange.LC_STOP);
- }
- /**
- * Reloads the current page.
- * If force is true, the cache will be skipped.
- */
- public void Reload(bool force = false) {
- if (!IsReady) return;
- CheckSanity();
- if (force) BrowserNative.zfb_changeLoading(browserId, BrowserNative.LoadChange.LC_FORCE_RELOAD);
- else BrowserNative.zfb_changeLoading(browserId, BrowserNative.LoadChange.LC_RELOAD);
- }
- /**
- * Show the development tools for the current page.
- *
- * If {show} is false the dev tools will be hidden, if possible.
- */
- public void ShowDevTools(bool show = true) {
- if (DeferUnready(() => ShowDevTools(show))) return;
- BrowserNative.zfb_showDevTools(browserId, show);
- }
- public Vector2 Size {
- get { return new Vector2(_width, _height); }
- }
- protected void _Resize(Texture2D newTexture, bool newTextureIsOurs) {
- var width = newTexture.width;
- var height = newTexture.height;
- if (textureIsOurs && texture && newTexture != texture) {
- Destroy(texture);
- }
- _width = width;
- _height = height;
- if (IsReady) BrowserNative.zfb_resize(browserId, width, height);
- else WhenReady(() => BrowserNative.zfb_resize(browserId, width, height));
- texture = newTexture;
- textureIsOurs = newTextureIsOurs;
- var renderer = GetComponent<Renderer>();
- if (renderer) renderer.material.mainTexture = texture;
- afterResize(texture);
- if (overlay) overlay.Resize(Texture);
- forceNextRender = true;
- }
- /**
- * Creates a new texture of the given size and starts rendering to that.
- */
- public void Resize(int width, int height) {
- var newTexture = new Texture2D(width, height, TextureFormat.ARGB32, generateMipmap);
- if (generateMipmap) newTexture.filterMode = FilterMode.Trilinear;
- newTexture.wrapMode = TextureWrapMode.Clamp;
- //Clear it to a color:
- var pixelCount = width * height;
- if (newTexture.mipmapCount > 1) {
- //generateMipmap doesn't tell us how many or how big, so quick hack to look it up:
- for (int i = 1; i < newTexture.mipmapCount; i++) {
- pixelCount += newTexture.GetPixels32(i).Length;
- }
- }
- BrowserNative.LoadSymbols();
- var pixelData = BrowserNative.zfb_flatColorTexture(
- pixelCount, baseColor.r, baseColor.g, baseColor.b, baseColor.a
- );
- newTexture.LoadRawTextureData(pixelData, pixelCount * 4);
- newTexture.Apply();
- BrowserNative.zfb_free(pixelData);
- _Resize(newTexture, true);
- }
- /** Tells the Browser to render to the given ARGB32 texture. */
- public void Resize(Texture2D newTexture) {
- Assert.AreEqual(TextureFormat.ARGB32, newTexture.format);
- _Resize(newTexture, false);
- }
- /** Sets and gets the current zoom level/DPI scaling factor. */
- public float Zoom {
- get { return _zoom; }
- set {
- if (DeferUnready(() => Zoom = value)) return;
- BrowserNative.zfb_setZoom(browserId, value);
- _zoom = value;
- }
- }
- /**
- * Evaluates JavaScript in the browser.
- * NB: This is JavaScript. Not UnityScript. If you try to feed this UnityScript it will choke and die.
- *
- * If IsLoaded is false, the script will be deferred until IsLoaded is true.
- *
- * The script is asynchronously executed in a separate process. To get the result value, yield on the returned
- * promise (in a coroutine) then take a look at promise.Value.
- *
- * To see script errors and debug issues, call ShowDevTools and use the inspector window to tackle
- * your problems. Also, keep an eye on console output (which gets forwarded to Debug.Log).
- *
- * If desired, you can fill out scriptURL with a URL for the file you are reading from. This can help fill out errors
- * with the correct filename and in some cases allow you to view the source in the inspector.
- */
- public IPromise<JSONNode> EvalJS(string script, string scriptURL = "scripted command") {
- //Debug.Log("Asked to EvalJS " + script + " loaded state: " + IsLoaded);
- var promise = new Promise<JSONNode>();
- var id = nextCallbackId++;
- var jsonScript = new JSONNode(script).AsJSON;
- var resultJS = @"try {"+
- "_zfb_event(" + id + ", JSON.stringify(eval(" + jsonScript + " )) || 'null');" +
- "} catch(ex) {" +
- "_zfb_event(" + id + ", 'fail-' + (JSON.stringify(ex.stack) || 'null'));" +
- "}"
- ;
- registeredCallbacks.Add(id, (val, isError) => {
- registeredCallbacks.Remove(id);
- if (isError) promise.Reject(new JSException(val));
- else promise.Resolve(val);
- });
- if (!IsLoaded) {
- WhenLoaded(() => _EvalJS(resultJS, scriptURL));
- } else {
- _EvalJS(resultJS, scriptURL);
- }
- return promise;
- }
- protected void _EvalJS(string script, string scriptURL) {
- BrowserNative.zfb_evalJS(browserId, script, scriptURL);
- }
- /**
- * Looks up {name} by evaluating it as JavaScript code, then calls it with the given arguments.
- *
- * If IsLoaded is false, the call will be deferred until IsLoaded is true.
- *
- * Because {name} is evaluated, you can use lookups like "MyGUI.show" or "Foo.getThing().doBob"
- *
- * The call itself is run asynchronously in a separate process. To get the value returned by the JS back, yield
- * on the promise CallFunction returns (in a coroutine) then take a look at promise.Value.
- *
- * Note that because JSONNode is implicitly convertible from many different types, you can often just
- * dump the values in directly when you call this:
- * int x = 5, y = 47;
- * browser.CallFunction("Menu.setPosition", x, y);
- * browser.CallFunction("Menu.setTitle", "Super Game");
- *
- */
- public IPromise<JSONNode> CallFunction(string name, params JSONNode[] arguments) {
- var js = name + "(";
- var sep = "";
- foreach (var arg in arguments) {
- js += sep + (arg ?? JSONNode.NullNode).AsJSON;
- sep = ", ";
- }
- js += ");";
- return EvalJS(js);
- }
- /**
- * Registers a JavaScript function in the Browser. When called, the given Mono {callback} will be executed.
- *
- * If IsLoaded is false, the in-page registration will be deferred until IsLoaded is true.
- *
- * The callback will be executed with one argument: a JSONNode array representing the arguments to the function
- * given in the browser. (Access the first argument with args[0], second with args[1], etc.)
- *
- * The arguments sent back-and forth must be JSON-able.
- *
- * The JavaScript process runs asynchronously. Callbacks triggered will be collected and fired during the next Update().
- *
- * {name} is evaluate-assigned JavaScript. You can use values like "myCallback", "MySystem.myCallback" (only if MySystem
- * already exists), or "GetThing().bobFunc" (if GetThing() returns an object you can use later).
- *
- */
- public void RegisterFunction(string name, JSCallback callback) {
- var id = nextCallbackId++;
- registeredCallbacks.Add(id, (value, error) => {
- //(we shouldn't be able to get an error here)
- callback(value);
- });
- var js = name + " = function() { _zfb_event(" + id + ", JSON.stringify(Array.prototype.slice.call(arguments))); };";
- EvalJS(js);
- }
- protected List<Action> thingsToDoClone = new List<Action>();
- protected void ProcessCallbacks() {
- while (thingsToDo.Count != 0) {
- Profiler.BeginSample("Browser.ProcessCallbacks", this);
- //It's not uncommon for some callbacks to add other callbacks
- //To keep from altering thingsToDo while iterating, we'll make a quick copy and use that.
- lock (thingsToDo) {
- thingsToDoClone.AddRange(thingsToDo);
- thingsToDo.Clear();
- }
- foreach (var thingToDo in thingsToDoClone) thingToDo();
- thingsToDoClone.Clear();
- Profiler.EndSample();
- }
- }
- protected void Update() {
- ProcessCallbacks();
- if (browserId == 0) {
- RequestNativeBrowser();
- return;//not ready yet or not loaded
- }
- if (!BrowserNative.SymbolsLoaded) return;
- HandleInput();
- }
- protected void LateUpdate() {
- //Note: we use LateUpdate here in hopes that commands issued during (anybody's) Update()
- //will have a better chance of being completed before we push the render
- if (lastUpdateFrame != Time.frameCount && BrowserNative.NativeLoaded) {
- Profiler.BeginSample("Browser.NativeTick");
- BrowserNative.zfb_tick();
- Profiler.EndSample();
- lastUpdateFrame = Time.frameCount;
- }
- if (browserId == 0) return;
- ProcessCallbacks();
- if (pageReplacer != null) {
- pageReplacer();
- pageReplacer = null;
- }
- if (browserId == 0) return;//not ready yet or not loaded
- if (EnableRendering) Render();
- }
- private Color32[] colorBuffer = null;
- protected void Render() {
- if (!BrowserNative.SymbolsLoaded) return;
- CheckSanity();
- BrowserNative.RenderData renderData;
- Profiler.BeginSample("Browser.UpdateTexture.zfb_getImage", this);
- {
- renderData = BrowserNative.zfb_getImage(browserId, forceNextRender);
- forceNextRender = false;
- if (renderData.pixels == IntPtr.Zero) return;//no changes
- if (renderData.w != texture.width || renderData.h != texture.height) {
- //Mismatch. Can happen, for example, when we resize and ask for a new image before the IPC layer gets back to us.
- return;
- }
- }
- Profiler.EndSample();
- if (texture.mipmapCount == 1) {
- Profiler.BeginSample("Browser.UpdateTexture.LoadRawTextureData", this);
- texture.LoadRawTextureData(renderData.pixels, renderData.w * renderData.h * 4);
- Profiler.EndSample();
- } else {
- //whelp, this is gonna be slow.
- //First, having Unity calculate mipmaps is slow. Second, we can't just LoadRawTextureData because it doesn't
- //contain mip levels. Third, LoadRawTextureData and Color32 have different in-memory byte orders (for our texture type).
- if (colorBuffer == null || colorBuffer.Length != renderData.w * renderData.h) {
- colorBuffer = new Color32[renderData.w * renderData.h];
- }
- Profiler.BeginSample("Browser.UpdateTexture.CopyData", this);
- var handle = GCHandle.Alloc(colorBuffer, GCHandleType.Pinned);
- BrowserNative.zfb_copyToColor32(renderData.pixels, handle.AddrOfPinnedObject(), renderData.w * renderData.h);
- handle.Free();
- Profiler.EndSample();
- Profiler.BeginSample("Browser.UpdateTexture.SetPixels32", this);
- texture.SetPixels32(colorBuffer);
- Profiler.EndSample();
- }
- Profiler.BeginSample("Browser.UpdateTexture.Apply", this);
- {
- texture.Apply(true);
- }
- Profiler.EndSample();
- onTextureUpdated();
- }
- /**
- * Adds the given browser as an overlay of this browser.
- *
- * The overlaid browser will appear transparently over the top of us on our texture.
- * {overlayBrowser} must not have an overlay and must be sized exactly the same as {this}.
- * Additionally, overlayBrowser.EnableRendering must be false. You still need to
- * do something to handle getting input to the right places. Overlays take a notable performance
- * hit on rendering (CPU alpha compositing).
- *
- * Overlays are used internally to implement context menus and pop-up dialogs (alert, onbeforeunload).
- * If the page causes any type of dialog, the overlay will be replaced.
- *
- * Overlays will be resized onto our texture when we are resized. The sizes must always match exactly.
- *
- * Remove the overlay (SetOverlay(null)) before closing either browser.
- *
- * (Note: though you can't set B as an overlay to A when B has an overlay, you can set
- * an overlay on B /while/ it is the overlay for A. For an example of this, try
- * right-clicking on the text area inside a prompt() popup. The context menu that
- * appears is an overlay to the overlay to the actual browser.)
- */
- public void SetOverlay(Browser overlayBrowser) {
- if (DeferUnready(() => SetOverlay(overlayBrowser))) return;
- if (overlayBrowser && overlayBrowser.DeferUnready(() => SetOverlay(overlayBrowser))) return;
- if (!overlayBrowser) {
- BrowserNative.zfb_setOverlay(browserId, 0);
- overlay = null;
- } else {
- overlay = overlayBrowser;
- if (
- !overlay.Texture ||
- (overlay.Texture.width != Texture.width || overlay.Texture.height != Texture.height)
- ) {
- overlay.Resize(Texture);
- }
-
- BrowserNative.zfb_setOverlay(browserId, overlayBrowser.browserId);
- }
- }
- protected void HandleInput() {
- if (_uiHandler == null || !EnableInput) return;
- CheckSanity();
- browserInput.HandleInput();
- }
- protected void OnApplicationFocus(bool focus) {
- if (!focus && browserInput != null) browserInput.HandleFocusLoss();
- }
- protected void OnApplicationPause(bool paused) {
- if (paused && browserInput != null) browserInput.HandleFocusLoss();
- }
- /**
- * Updates the cursor on our UIHandler.
- * Usually you don't need to call this, but if you are sharing input with an overlay, call this any time the
- * "focused" overlay changes.
- */
- public void UpdateCursor() {
- if (UIHandler == null) return;
- if (DeferUnready(UpdateCursor)) return;
- int w, h;
- var cursorType = BrowserNative.zfb_getMouseCursor(browserId, out w, out h);
- if (cursorType != BrowserNative.CursorType.Custom) {
- UIHandler.BrowserCursor.SetActiveCursor(cursorType);
- } else {
- if (w == 0 && h == 0) {
- //bad cursor
- UIHandler.BrowserCursor.SetActiveCursor(BrowserNative.CursorType.None);
- return;
- }
- var buffer = new Color32[w * h];
- int hx, hy;
- var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
- BrowserNative.zfb_getMouseCustomCursor(browserId, handle.AddrOfPinnedObject(), w, h, out hx, out hy);
- handle.Free();
- var tex = new Texture2D(w, h, TextureFormat.ARGB32, false);
- tex.SetPixels32(buffer);
- //in-memory only, no Apply()
- UIHandler.BrowserCursor.SetCustomCursor(tex, new Vector2(hx, hy));
- DestroyImmediate(tex);
- }
- }
- /**
- * Take an action on a download.
- *
- * At the outset:
- * Begin: Starts the download. Saves to the given file if given. If fileName is null, the user will be prompted.
- * Cancel: Does nothing with a download.
- *
- * After starting a download:
- * Pause, Cancel, Resume: Does what it says on the tin.
- *
- * Once a download is finished or canceled it is not valid to call this function for that download any more.
- *
- * fileName is ignored except when beginning a download.
- */
- public void DownloadCommand(int downloadId, BrowserNative.DownloadAction action, string fileName = null) {
- CheckSanity();
- BrowserNative.zfb_downloadCommand(browserId, downloadId, action, fileName);
- }
- /// <summary>
- /// Injects the given unicode text to the browser as if it had been typed.
- /// (No key press events are generated.)
- /// </summary>
- /// <param name="text"></param>
- public void TypeText(string text) {
- for (int i = 0; i < text.Length; i++) {
- var ev = new Event() {
- type = EventType.KeyDown,
- keyCode = 0,
- character = text[i],
- };
- browserInput.extraEventsToInject.Add(ev);
- }
- }
- /// <summary>
- /// Sends key presses/releases to the browser.
- /// </summary>
- /// <param name="key"></param>
- /// <param name="action"></param>
- public void PressKey(KeyCode key, KeyAction action = KeyAction.PressAndRelease) {
- if (action == KeyAction.Press || action == KeyAction.PressAndRelease) {
- var ev = new Event() {
- type = EventType.KeyDown,
- keyCode = key,
- character = (char)0,
- };
- browserInput.extraEventsToInject.Add(ev);
- }
- if (action == KeyAction.Release || action == KeyAction.PressAndRelease) {
- var ev = new Event() {
- type = EventType.KeyUp,
- keyCode = key,
- character = (char)0,
- };
- browserInput.extraEventsToInject.Add(ev);
- }
- }
- internal void _RaiseFocusEvent(bool mouseIsFocused, bool keyboardIsFocused) {
- focusState.hasMouseFocus = mouseIsFocused;
- focusState.hasKeyboardFocus = keyboardIsFocused;
- onBrowserFocus(mouseIsFocused, keyboardIsFocused);
- }
- }
- }
|