// Copyright (c) 2025 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. #pragma warning disable CS0067 using System; using System.Collections.Generic; using System.Threading.Tasks; using UnityEngine; using Vuplex.WebView.Internal; namespace Vuplex.WebView { /// /// Mock IWebView implementation used for running in the Unity editor. /// /// /// MockWebView logs messages to the console to indicate when its methods are /// called, but it doesn't actually load or render web content. /// partial class MockWebView : MonoBehaviour, IWebView { public event EventHandler CloseRequested; public event EventHandler ConsoleMessageLogged; public event EventHandler> FocusChanged; public event EventHandler FocusedInputFieldChanged; 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; private set; } public bool IsInitialized { get; private set; } public List PageLoadScripts { get; } = new List(); public WebPluginType PluginType { get; } = WebPluginType.Mock; public Vector2Int Size { get; private set; } public Texture2D Texture { get; private set; } public string Title { get; private set; } = ""; public string Url { get; private set; } = ""; public Task CanGoBack() { _log("CanGoBack()"); OnCanGoBack(); return Task.FromResult(false); } public Task CanGoForward() { _log("CanGoForward()"); OnCanGoForward(); return Task.FromResult(false); } public Task CaptureScreenshot() { _log("CaptureScreenshot()"); OnCaptureScreenshot(); return Task.FromResult(new byte[0]); } public void Click(int xInPixels, int yInPixels, bool preventStealingFocus = false) { var pointIsValid = xInPixels >= 0 && xInPixels <= Size.x && yInPixels >= 0 && yInPixels <= Size.y; if (!pointIsValid) { 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)."); } _log($"Click({xInPixels}, {yInPixels}, {preventStealingFocus})"); } public void Click(Vector2 point, bool preventStealingFocus = false) { _assertValidNormalizedPoint(point); _log($"Click({point.ToString("n4")}, {preventStealingFocus})"); if (_moreDetailsClickRect.Contains(point)) { Application.OpenURL("https://support.vuplex.com/articles/mock-webview"); } } public void Copy() { _log("Copy()"); OnCopy(); } public Material CreateMaterial() { #if UNITY_SERVER return null; #else var material = new Material(Resources.Load("MockViewportMaterial")); // Create a copy of the texture so that an Exception won't be thrown when the prefab destroys it. // Also, explicitly use RGBA32 here so that the texture will be converted to RGBA32 if the editor // imported it as a different format. For example, when Texture Compression is set to ASTC in Android build settings, // the editor automatically imports new textures as ASTC, even though the Windows editor doesn't support that format. var texture = new Texture2D(material.mainTexture.width, material.mainTexture.height, TextureFormat.RGBA32, true); texture.SetPixels((material.mainTexture as Texture2D).GetPixels()); texture.Apply(); material.mainTexture = texture; return material; #endif } public void Cut() { _log("Cut()"); OnCut(); } public static Task DeleteCookies(string url, string cookieName = null) { if (url == null) { throw new ArgumentException("The url cannot be null."); } _log($"DeleteCookies(\"{url}\", \"{cookieName}\")"); return Task.FromResult(true); } public void Dispose() { IsDisposed = true; _log("Dispose()"); if (this != null) { Destroy(gameObject); } } public Task ExecuteJavaScript(string javaScript) { var taskSource = new TaskCompletionSource(); ExecuteJavaScript(javaScript, taskSource.SetResult); return taskSource.Task; } public void ExecuteJavaScript(string javaScript, Action callback) { _log($"ExecuteJavaScript(\"{_truncateIfNeeded(javaScript)}\")"); callback(""); OnExecuteJavaScript(); } public static Task GetCookies(string url, string cookieName = null) { if (url == null) { throw new ArgumentException("The url cannot be null."); } _log($"GetCookies(\"{url}\", \"{cookieName}\")"); var taskSource = new TaskCompletionSource(); return Task.FromResult(new Cookie[0]); } public Task GetRawTextureData() { _log("GetRawTextureData()"); OnGetRawTextureData(); return Task.FromResult(new byte[0]); } public void GoBack() { _log("GoBack()"); OnGoBack(); } public void GoForward() { _log("GoForward()"); OnGoForward(); } public Task Init(int width, int height) { Texture = new Texture2D(width, height, TextureFormat.RGBA32, false); Size = new Vector2Int(width, height); IsInitialized = true; DontDestroyOnLoad(gameObject); _log($"Init() width: {width.ToString("n4")}, height: {height.ToString("n4")}"); return Task.FromResult(true); } public static MockWebView Instantiate() => new GameObject("MockWebView").AddComponent(); public virtual void LoadHtml(string html) { var truncatedHtml = _truncateIfNeeded(html); Url = truncatedHtml; _log($"LoadHtml(\"{truncatedHtml}...\")"); OnLoadHtml(); _handlePageLoad(truncatedHtml); } public virtual void LoadUrl(string url) => LoadUrl(url, null); public virtual void LoadUrl(string url, Dictionary additionalHttpHeaders) { Url = url; _log($"LoadUrl(\"{url}\")"); OnLoadUrl(url); _handlePageLoad(url); } 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 void Paste() { _log("Paste()"); OnPaste(); } public Vector2 PointToNormalized(int xInPixels, int yInPixels) { return new Vector2((float)xInPixels / Size.x, (float)yInPixels / Size.y); } public void PostMessage(string data) => _log($"PostMessage(\"{data}\")"); public void Reload() => _log("Reload()"); public void Resize(int width, int height) { Size = new Vector2Int(width, height); _log($"Resize({width.ToString("n4")}, {height.ToString("n4")})"); } public void Scroll(int scrollDeltaX, int scrollDeltaY) => _log($"Scroll({scrollDeltaX}, {scrollDeltaY})"); public void Scroll(Vector2 delta) => _log($"Scroll({delta.ToString("n4")})"); public void Scroll(Vector2 delta, Vector2 point) { _assertValidNormalizedPoint(point); _log($"Scroll({delta.ToString("n4")}, {point.ToString("n4")})"); } public void SelectAll() => _log("SelectAll()"); public void SendKey(string input) => _log($"SendKey(\"{input}\")"); public static Task SetCookie(Cookie cookie) { if (cookie == null) { throw new ArgumentException("Cookie cannot be null."); } if (!cookie.IsValid) { throw new ArgumentException("Cannot set invalid cookie: " + cookie); } _log($"SetCookie({cookie}"); return Task.FromResult(true); } public void SetDefaultBackgroundEnabled(bool enabled) => _log($"SetDefaultBackgroundEnabled({enabled})"); public void SetFocused(bool focused) { _log($"SetFocused({focused})"); FocusChanged?.Invoke(this, new EventArgs(focused)); } public void SetRenderingEnabled(bool enabled) => _log($"SetRenderingEnabled({enabled})"); public void StopLoad() => _log("StopLoad()"); public Task WaitForNextPageLoadToFinish() { if (_pageLoadFinishedTaskSource == null) { _pageLoadFinishedTaskSource = new TaskCompletionSource(); } return _pageLoadFinishedTaskSource.Task; } public void ZoomIn() => _log("ZoomIn()"); public void ZoomOut() => _log("ZoomOut()"); Rect _moreDetailsClickRect = new Rect(0.67f, 0.7f, 0.17f, 0.11f); TaskCompletionSource _pageLoadFinishedTaskSource; // Partial methods implemented by other 3D WebView packages // to provide platform-specific warnings in the editor. partial void OnCanGoBack(); partial void OnCanGoForward(); partial void OnCaptureScreenshot(); partial void OnCopy(); partial void OnCut(); partial void OnExecuteJavaScript(); partial void OnGetRawTextureData(); partial void OnGoBack(); partial void OnGoForward(); partial void OnLoadHtml(); partial void OnLoadUrl(string url); partial void OnPaste(); partial void OnZoomIn(); partial void OnZoomOut(); void _assertValidNormalizedPoint(Vector2 normalizedPoint) { 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"); } } void _handlePageLoad(string url) { UrlChanged?.Invoke(this, new UrlChangedEventArgs(url)); LoadProgressChanged?.Invoke(this, new ProgressChangedEventArgs(ProgressChangeType.Started, 0)); LoadProgressChanged?.Invoke(this, new ProgressChangedEventArgs(ProgressChangeType.Finished, 1)); _pageLoadFinishedTaskSource?.SetResult(true); _pageLoadFinishedTaskSource = null; } static void _log(string message) { #if !VUPLEX_DISABLE_MOCK_WEBVIEW_LOGGING WebViewLogger.Log("[MockWebView] " + message); #endif } string _truncateIfNeeded(string str) { var maxLength = 25; if (str.Length <= maxLength) { return str; } return str.Substring(0, maxLength) + "..."; } [Obsolete(ObsoletionMessages.Blur, true)] public void Blur() {} [Obsolete(ObsoletionMessages.CanGoBack, true)] public void CanGoBack(Action callback) {} [Obsolete(ObsoletionMessages.CanGoForward, true)] public void CanGoForward(Action callback) {} [Obsolete(ObsoletionMessages.CaptureScreenshot, true)] public void CaptureScreenshot(Action callback) {} [Obsolete(ObsoletionMessages.DisableViewUpdates, true)] public void DisableViewUpdates() {} [Obsolete(ObsoletionMessages.EnableViewUpdates, true)] public void EnableViewUpdates() {} [Obsolete(ObsoletionMessages.Focus, true)] public void Focus() {} [Obsolete(ObsoletionMessages.GetRawTextureData, true)] public void GetRawTextureData(Action callback) {} [Obsolete(ObsoletionMessages.HandleKeyboardInput)] public void HandleKeyboardInput(string key) => SendKey(key); [Obsolete(ObsoletionMessages.Init, true)] public void Init(Texture2D texture, float width, float height) {} [Obsolete(ObsoletionMessages.Init2, true)] public void Init(Texture2D texture, float width, float height, Texture2D videoTexture) {} [Obsolete(ObsoletionMessages.PageLoadFailed)] public event EventHandler PageLoadFailed; [Obsolete(ObsoletionMessages.Resolution, true)] public float Resolution { get; } [Obsolete(ObsoletionMessages.SetResolution, true)] public void SetResolution(float pixelsPerUnityUnit) {} [Obsolete(ObsoletionMessages.SizeInPixels)] public Vector2 SizeInPixels { get => (Vector2)Size; } [Obsolete(ObsoletionMessages.VideoRectChanged, true)] public event EventHandler> VideoRectChanged; [Obsolete(ObsoletionMessages.VideoTexture, true)] public Texture2D VideoTexture { get; } } }