// 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