// Copyright (c) 2024 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_VISIONOS && !VUPLEX_OMIT_VISIONOS) || (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 UnityEngine.Rendering;
namespace Vuplex.WebView.Internal {
///
/// 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> FocusChanged;
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 LoadFailed;
public event EventHandler LoadProgressChanged;
public event EventHandler> MessageEmitted;
public event EventHandler Terminated;
public event EventHandler> TitleChanged;
public event EventHandler UrlChanged;
public bool IsDisposed { get; protected set; }
public bool IsInitialized { get => _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 = _normalizedToPointAssertValid(normalizedPoint);
Click(pixelsPoint.x, pixelsPoint.y, preventStealingFocus);
}
public virtual async void Copy() {
_assertValidState();
GUIUtility.systemCopyBuffer = await _getSelectedText();
}
public virtual Material CreateMaterial() {
if (_native2DModeEnabled) {
VXUtils.LogNative2DModeWarning("CreateMaterial", "will return null");
return null;
}
var material = _createMaterial();
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, _transformUrlIfNeeded(url), newlineDelimitedHttpHeaders);
}
}
public Vector2Int NormalizedToPoint(Vector2 normalizedPoint) {
return new Vector2Int(
(int)Math.Round(normalizedPoint.x * (float)Size.x),
(int)Math.Round(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")
.Replace("\r", "\\r");
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);
_warnIfAbnormallyLarge(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 = NormalizedToPoint(normalizedScrollDelta);
Scroll(scrollDeltaInPixels.x, scrollDeltaInPixels.y);
}
public virtual void Scroll(Vector2 normalizedScrollDelta, Vector2 normalizedPoint) {
_assertValidState();
var scrollDeltaInPixels = NormalizedToPoint(normalizedScrollDelta);
var pointInPixels = _normalizedToPointAssertValid(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 SetDefaultBackgroundEnabled(bool enabled) {
_assertValidState();
WebView_setDefaultBackgroundEnabled(_nativeWebViewPtr, enabled);
}
public virtual void SetFocused(bool focused) {
_assertValidState();
WebView_setFocused(_nativeWebViewPtr, focused);
FocusChanged?.Invoke(this, new EventArgs(focused));
}
public virtual void SetRenderingEnabled(bool enabled) {
_assertValidState();
if (_native2DModeEnabled) {
VXUtils.LogNative2DModeWarning("SetRenderingEnabled");
return;
}
WebView_setRenderingEnabled(_nativeWebViewPtr, enabled);
_renderingEnabled = enabled;
}
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
}
// Anything over 19.4 megapixels (6k) is almost certainly a mistake.
protected virtual int _abnormallyLargeThreshold { get => 19400000; }
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;
TaskCompletionSource _initTaskSource;
Material _materialForBlitting;
protected bool _native2DModeEnabled; // Used for Native 2D Mode.
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 => new Rect(_native2DPosition, Size);
set {
Size = new Vector2Int((int)value.width, (int)value.height);
_native2DPosition = new Vector2Int((int)value.x, (int)value.y);
}
}
static string[] STANDARD_URI_SCHEMES = new string[] { "http:", "https:", "file:", "about:" };
static readonly Regex _streamingAssetsUrlRegex = new Regex(@"^streaming-assets:(//)?(.*)$", RegexOptions.IgnoreCase);
// Used for Native 2D Mode.
protected bool _visible;
protected void _assertNative2DModeEnabled() {
if (!_native2DModeEnabled) {
throw new InvalidOperationException("IWithNative2DMode methods can only be called on a webview with Native 2D Mode enabled.");
}
}
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 _assertSupportedGraphicsApi() {
var supportedApis = _getSupportedGraphicsApis();
if (supportedApis == null) {
// Graphics API validation is disabled for this platform.
return;
}
var isValid = supportedApis.ToList().Contains(SystemInfo.graphicsDeviceType);
if (isValid) {
return;
}
var listOfSupportedApis = supportedApis.Length == 1 ? supportedApis[0].ToString() : $"one of the following: {String.Join(", ", supportedApis)}";
var message = $"Unsupported Graphics API: The Graphics API is set to {SystemInfo.graphicsDeviceType}, which 3D WebView doesn't support on this platform. Please go to Player Settings -> Other Settings and set the Graphics API to {listOfSupportedApis}";
WebViewLogger.LogError(message);
throw new InvalidOperationException(message);
}
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 _normalizedToPointAssertValid(Vector2 normalizedPoint) {
var isValid = normalizedPoint.x >= 0f && normalizedPoint.x <= 1f && normalizedPoint.y >= 0f && normalizedPoint.y <= 1f;
if (isValid) {
return NormalizedToPoint(normalizedPoint);
}
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");
}
protected virtual Material _createMaterial() => VXUtils.CreateDefaultMaterial();
protected virtual Task _createTexture(int width, int height) {
_warnIfAbnormallyLarge(width, height);
var textureFormat = _getTextureFormat();
var texture = new Texture2D(
width,
height,
textureFormat,
false,
false
);
#if UNITY_2020_2_OR_NEWER
var originalTexture = texture;
// 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,
false,
false,
originalTexture.GetNativeTexturePtr()
);
// Destroy the original texture so that its memory is released.
Destroy(originalTexture);
#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 = _createMaterial();
}
Graphics.Blit(Texture, tempRenderTexture, _materialForBlitting);
Texture2D readableTexture = new Texture2D(Size.x, Size.y, TextureFormat.RGBA32, false);
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