// Copyright (c) 2022 Vuplex Inc. All rights reserved.
//
// Licensed under the Vuplex Commercial Software Library License, you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// https://vuplex.com/commercial-library-license
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Only define BaseWebView.cs on supported platforms to avoid IL2CPP linking
// errors on unsupported platforms.
#if UNITY_EDITOR || UNITY_STANDALONE_WIN || UNITY_STANDALONE_OSX || UNITY_ANDROID || (UNITY_IOS && !VUPLEX_OMIT_IOS) || (UNITY_WEBGL && !VUPLEX_OMIT_WEBGL) || UNITY_WSA
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using UnityEngine;
using Vuplex.WebView.Internal;
namespace Vuplex.WebView {
///
/// The base IWebView implementation, which is extended for each platform.
///
public abstract class BaseWebView : MonoBehaviour {
public event EventHandler CloseRequested;
public event EventHandler ConsoleMessageLogged {
add {
_consoleMessageLogged += value;
if (_consoleMessageLogged != null && _consoleMessageLogged.GetInvocationList().Length == 1) {
_setConsoleMessageEventsEnabled(true);
}
}
remove {
_consoleMessageLogged -= value;
if (_consoleMessageLogged == null) {
_setConsoleMessageEventsEnabled(false);
}
}
}
public event EventHandler FocusedInputFieldChanged {
add {
_focusedInputFieldChanged += value;
if (_focusedInputFieldChanged != null && _focusedInputFieldChanged.GetInvocationList().Length == 1) {
_setFocusedInputFieldEventsEnabled(true);
}
}
remove {
_focusedInputFieldChanged -= value;
if (_focusedInputFieldChanged == null) {
_setFocusedInputFieldEventsEnabled(false);
}
}
}
public event EventHandler LoadProgressChanged;
public event EventHandler> MessageEmitted;
public event EventHandler PageLoadFailed;
public event EventHandler> TitleChanged;
public event EventHandler UrlChanged;
public bool IsDisposed { get; protected set; }
public bool IsInitialized { get { return _initState == InitState.Initialized; }}
public List PageLoadScripts { get; } = new List();
public Vector2Int Size { get; private set; }
public Texture2D Texture { get; protected set; }
public string Title { get; private set; } = "";
public string Url { get; private set; } = "";
public virtual Task CanGoBack() {
_assertValidState();
var taskSource = new TaskCompletionSource();
_pendingCanGoBackCallbacks.Add(taskSource.SetResult);
WebView_canGoBack(_nativeWebViewPtr);
return taskSource.Task;
}
public virtual Task CanGoForward() {
_assertValidState();
var taskSource = new TaskCompletionSource();
_pendingCanGoForwardCallbacks.Add(taskSource.SetResult);
WebView_canGoForward(_nativeWebViewPtr);
return taskSource.Task;
}
public virtual Task CaptureScreenshot() {
var texture = _getReadableTexture();
var bytes = ImageConversion.EncodeToPNG(texture);
Destroy(texture);
return Task.FromResult(bytes);
}
public virtual void Click(int xInPixels, int yInPixels, bool preventStealingFocus = false) {
_assertValidState();
_assertPointIsWithinBounds(xInPixels, yInPixels);
// On most platforms, the regular Click() method doesn't steal focus,
// So, the default is to ignore preventStealingFocus.
WebView_click(_nativeWebViewPtr, xInPixels, yInPixels);
}
public void Click(Vector2 normalizedPoint, bool preventStealingFocus = false) {
_assertValidState();
var pixelsPoint = _convertNormalizedToPixels(normalizedPoint);
Click(pixelsPoint.x, pixelsPoint.y, preventStealingFocus);
}
public virtual async void Copy() {
_assertValidState();
GUIUtility.systemCopyBuffer = await _getSelectedText();
}
public virtual Material CreateMaterial() {
var material = VXUtils.CreateDefaultMaterial();
material.mainTexture = Texture;
return material;
}
public virtual async void Cut() {
_assertValidState();
GUIUtility.systemCopyBuffer = await _getSelectedText();
SendKey("Backspace");
}
public virtual void Dispose() {
_assertValidState();
IsDisposed = true;
WebView_destroy(_nativeWebViewPtr);
_nativeWebViewPtr = IntPtr.Zero;
// To avoid a MissingReferenceException, verify that this script
// hasn't already been destroyed prior to accessing gameObject.
if (this != null) {
Destroy(gameObject);
}
}
public Task ExecuteJavaScript(string javaScript) {
var taskSource = new TaskCompletionSource();
ExecuteJavaScript(javaScript, taskSource.SetResult);
return taskSource.Task;
}
public virtual void ExecuteJavaScript(string javaScript, Action callback) {
_assertValidState();
string resultCallbackId = null;
if (callback != null) {
resultCallbackId = Guid.NewGuid().ToString();
_pendingJavaScriptResultCallbacks[resultCallbackId] = callback;
}
WebView_executeJavaScript(_nativeWebViewPtr, javaScript, resultCallbackId);
}
public virtual Task GetRawTextureData() {
var texture = _getReadableTexture();
var bytes = texture.GetRawTextureData();
Destroy(texture);
return Task.FromResult(bytes);
}
public virtual void GoBack() {
_assertValidState();
WebView_goBack(_nativeWebViewPtr);
}
public virtual void GoForward() {
_assertValidState();
WebView_goForward(_nativeWebViewPtr);
}
public virtual void LoadHtml(string html) {
_assertValidState();
WebView_loadHtml(_nativeWebViewPtr, html);
}
public virtual void LoadUrl(string url) {
_assertValidState();
WebView_loadUrl(_nativeWebViewPtr, _transformUrlIfNeeded(url));
}
public virtual void LoadUrl(string url, Dictionary additionalHttpHeaders) {
_assertValidState();
if (additionalHttpHeaders == null) {
LoadUrl(url);
} else {
var headerStrings = additionalHttpHeaders.Keys.Select(key => $"{key}: {additionalHttpHeaders[key]}").ToArray();
var newlineDelimitedHttpHeaders = String.Join("\n", headerStrings);
WebView_loadUrlWithHeaders(_nativeWebViewPtr, url, newlineDelimitedHttpHeaders);
}
}
public Vector2Int NormalizedToPoint(Vector2 normalizedPoint) {
return new Vector2Int((int)(normalizedPoint.x * (float)Size.x), (int)(normalizedPoint.y * (float)Size.y));
}
public virtual void Paste() {
_assertValidState();
var text = GUIUtility.systemCopyBuffer;
foreach (var character in text) {
SendKey(char.ToString(character));
}
}
public Vector2 PointToNormalized(int xInPixels, int yInPixels) {
return new Vector2((float)xInPixels / (float)Size.x, (float)yInPixels / (float)Size.y);
}
public virtual void PostMessage(string message) {
var escapedString = message.Replace("\\", "\\\\")
.Replace("'", "\\\\'")
.Replace("\n", "\\\\n");
ExecuteJavaScript($"vuplex._emit('message', {{ data: \'{escapedString}\' }})", null);
}
public virtual void Reload() {
_assertValidState();
WebView_reload(_nativeWebViewPtr);
}
public virtual void Resize(int width, int height) {
if (width == Size.x && height == Size.y) {
return;
}
_assertValidState();
_assertValidSize(width, height);
VXUtils.ThrowExceptionIfAbnormallyLarge(width, height);
Size = new Vector2Int(width, height);
_resize();
}
public virtual void Scroll(int scrollDeltaXInPixels, int scrollDeltaYInPixels) {
_assertValidState();
WebView_scroll(_nativeWebViewPtr, scrollDeltaXInPixels, scrollDeltaYInPixels);
}
public void Scroll(Vector2 normalizedScrollDelta) {
_assertValidState();
var scrollDeltaInPixels = _convertNormalizedToPixels(normalizedScrollDelta, false);
Scroll(scrollDeltaInPixels.x, scrollDeltaInPixels.y);
}
public virtual void Scroll(Vector2 normalizedScrollDelta, Vector2 normalizedPoint) {
_assertValidState();
var scrollDeltaInPixels = _convertNormalizedToPixels(normalizedScrollDelta, false);
var pointInPixels = _convertNormalizedToPixels(normalizedPoint);
WebView_scrollAtPoint(_nativeWebViewPtr, scrollDeltaInPixels.x, scrollDeltaInPixels.y, pointInPixels.x, pointInPixels.y);
}
public virtual void SelectAll() {
_assertValidState();
// If the focused element is an input with a select() method, then use that.
// Otherwise, travel up the DOM until we get to the body or a contenteditable
// element, and then select its contents.
ExecuteJavaScript(
@"(function() {
var element = document.activeElement || document.body;
while (!(element === document.body || element.getAttribute('contenteditable') === 'true')) {
if (typeof element.select === 'function') {
element.select();
return;
}
element = element.parentElement;
}
var range = document.createRange();
range.selectNodeContents(element);
var selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
})();",
null
);
}
public virtual void SendKey(string key) {
_assertValidState();
WebView_sendKey(_nativeWebViewPtr, key);
}
public static void SetCameraAndMicrophoneEnabled(bool enabled) => WebView_setCameraAndMicrophoneEnabled(enabled);
public virtual void SetFocused(bool focused) {
_assertValidState();
WebView_setFocused(_nativeWebViewPtr, focused);
}
public virtual void SetRenderingEnabled(bool enabled) {
_assertValidState();
WebView_setRenderingEnabled(_nativeWebViewPtr, enabled);
_renderingEnabled = enabled;
if (enabled && _currentNativeTexture != IntPtr.Zero) {
Texture.UpdateExternalTexture(_currentNativeTexture);
}
}
public virtual void StopLoad() {
_assertValidState();
WebView_stopLoad(_nativeWebViewPtr);
}
public Task WaitForNextPageLoadToFinish() {
if (_pageLoadFinishedTaskSource == null) {
_pageLoadFinishedTaskSource = new TaskCompletionSource();
}
return _pageLoadFinishedTaskSource.Task;
}
public virtual void ZoomIn() {
_assertValidState();
WebView_zoomIn(_nativeWebViewPtr);
}
public virtual void ZoomOut() {
_assertValidState();
WebView_zoomOut(_nativeWebViewPtr);
}
#region Non-public members
protected enum InitState {
Uninitialized,
InProgress,
Initialized
}
EventHandler _consoleMessageLogged;
protected IntPtr _currentNativeTexture;
#if (UNITY_STANDALONE_WIN && !UNITY_EDITOR) || UNITY_EDITOR_WIN
protected const string _dllName = "VuplexWebViewWindows";
#elif (UNITY_STANDALONE_OSX && !UNITY_EDITOR) || UNITY_EDITOR_OSX
protected const string _dllName = "VuplexWebViewMac";
#elif UNITY_WSA
protected const string _dllName = "VuplexWebViewUwp";
#elif UNITY_ANDROID
protected const string _dllName = "VuplexWebViewAndroid";
#else
protected const string _dllName = "__Internal";
#endif
EventHandler _focusedInputFieldChanged;
protected InitState _initState = InitState.Uninitialized;
protected TaskCompletionSource _initTaskSource;
Material _materialForBlitting;
protected Vector2Int _native2DPosition; // Used for Native 2D Mode.
protected IntPtr _nativeWebViewPtr;
TaskCompletionSource _pageLoadFinishedTaskSource;
List> _pendingCanGoBackCallbacks = new List>();
List> _pendingCanGoForwardCallbacks = new List>();
protected Dictionary> _pendingJavaScriptResultCallbacks = new Dictionary>();
protected bool _renderingEnabled = true;
// Used for Native 2D Mode. Use Size as the single source of truth for the size
// to ensure that both Size and Rect stay in sync when Resize() or SetRect() is called.
protected Rect _rect {
get { return new Rect(_native2DPosition, Size); }
set {
Size = new Vector2Int((int)value.width, (int)value.height);
_native2DPosition = new Vector2Int((int)value.x, (int)value.y);
}
}
static readonly Regex _streamingAssetsUrlRegex = new Regex(@"^streaming-assets:(//)?(.*)$", RegexOptions.IgnoreCase);
protected void _assertPointIsWithinBounds(int xInPixels, int yInPixels) {
var isValid = xInPixels >= 0 && xInPixels <= Size.x && yInPixels >= 0 && yInPixels <= Size.y;
if (!isValid) {
throw new ArgumentException($"The point provided ({xInPixels}px, {yInPixels}px) is not within the bounds of the webview (width: {Size.x}px, height: {Size.y}px).");
}
}
protected void _assertSingletonEventHandlerUnset(object handler, string eventName) {
if (handler != null) {
throw new InvalidOperationException(eventName + " supports only one event handler. Please remove the existing handler before adding a new one.");
}
}
void _assertValidSize(int width, int height) {
if (!(width > 0 && height > 0)) {
throw new ArgumentException($"Invalid size: ({width}, {height}). The width and height must both be greater than 0.");
}
}
protected void _assertValidState() {
if (!IsInitialized) {
throw new InvalidOperationException("Methods cannot be called on an uninitialized webview. Prior to calling the webview's methods, please initialize it first by calling IWebView.Init() and awaiting the Task it returns.");
}
if (IsDisposed) {
throw new InvalidOperationException("Methods cannot be called on a disposed webview.");
}
}
protected Vector2Int _convertNormalizedToPixels(Vector2 normalizedPoint, bool assertBetweenZeroAndOne = true) {
if (assertBetweenZeroAndOne) {
var isValid = normalizedPoint.x >= 0f && normalizedPoint.x <= 1f && normalizedPoint.y >= 0f && normalizedPoint.y <= 1f;
if (!isValid) {
throw new ArgumentException($"The normalized point provided is invalid. The x and y values of normalized points must be in the range of [0, 1], but the value provided was {normalizedPoint.ToString("n4")}. For more info, please see https://support.vuplex.com/articles/normalized-points");
}
}
return new Vector2Int((int)(normalizedPoint.x * Size.x), (int)(normalizedPoint.y * Size.y));
}
protected virtual Task _createTexture(int width, int height) {
VXUtils.ThrowExceptionIfAbnormallyLarge(width, height);
var texture = new Texture2D(
width,
height,
TextureFormat.RGBA32,
false,
false
);
#if UNITY_2020_2_OR_NEWER
// In Unity 2020.2, Unity's internal TexturesD3D11.cpp class on Windows logs an error if
// UpdateExternalTexture() is called on a Texture2D created from the constructor
// rather than from Texture2D.CreateExternalTexture(). So, rather than returning
// the original Texture2D created via the constructor, we return a copy created
// via CreateExternalTexture(). This approach is only used for 2020.2 and newer because
// it doesn't work in 2018.4 and instead causes a crash.
texture = Texture2D.CreateExternalTexture(
width,
height,
TextureFormat.RGBA32,
false,
false,
texture.GetNativeTexturePtr()
);
#endif
return Task.FromResult(texture);
}
protected virtual void _destroyNativeTexture(IntPtr nativeTexture) {
WebView_destroyTexture(nativeTexture, SystemInfo.graphicsDeviceType.ToString());
}
Texture2D _getReadableTexture() {
// https://support.unity3d.com/hc/en-us/articles/206486626-How-can-I-get-pixels-from-unreadable-textures-
RenderTexture tempRenderTexture = RenderTexture.GetTemporary(
Size.x,
Size.y,
0,
RenderTextureFormat.Default,
RenderTextureReadWrite.Linear
);
RenderTexture previousRenderTexture = RenderTexture.active;
RenderTexture.active = tempRenderTexture;
// Explicitly clear the temporary render texture, otherwise it can contain
// existing content that won't be overwritten by transparent pixels.
GL.Clear(true, true, Color.clear);
// Use the version of Graphics.Blit() that accepts a material
// so that any transformations needed are performed with the shader.
if (_materialForBlitting == null) {
_materialForBlitting = VXUtils.CreateDefaultMaterial();
}
Graphics.Blit(Texture, tempRenderTexture, _materialForBlitting);
Texture2D readableTexture = new Texture2D(Size.x, Size.y);
readableTexture.ReadPixels(new Rect(0, 0, tempRenderTexture.width, tempRenderTexture.height), 0, 0);
readableTexture.Apply();
RenderTexture.active = previousRenderTexture;
RenderTexture.ReleaseTemporary(tempRenderTexture);
return readableTexture;
}
Task _getSelectedText() {
// window.getSelection() doesn't work on the content of