/**
* Copyright (c) 2021 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.
*/
using System;
using UnityEngine;
using UnityEngine.EventSystems;
#if NET_4_6 || NET_STANDARD_2_0
using System.Threading.Tasks;
#endif
namespace Vuplex.WebView {
public abstract class BaseWebViewPrefab : MonoBehaviour {
///
/// Indicates that the prefab was clicked. Note that the prefab automatically
/// calls the `IWebView.Click()` method for you.
///
public event EventHandler Clicked;
///
/// Indicates that the prefab finished initializing,
/// so its `WebView` property is ready to use.
///
///
public event EventHandler Initialized;
///
/// Indicates that the prefab was scrolled. Note that the prefab automatically
/// calls the `IWebView.Scroll()` method for you.
///
public event EventHandler Scrolled;
///
/// If you drag the prefab into the scene via the editor,
/// you can set this property to make it so that the instance
/// automatically initializes itself with the given URL. To load a new URL
/// after the prefab has been initialized, use `IWebView.LoadUrl()` instead.
///
[Label("Initial URL to load (optional)")]
[Tooltip("Or you can leave the Initial URL blank if you want to initialize the prefab programmatically by calling Init().")]
[HideInInspector]
public string InitialUrl;
[Header("Other Settings")]
///
/// Determines how the prefab handles drag interactions.
///
[Tooltip("Note: \"Drag Within Page\" is not supported on iOS or UWP.")]
public DragMode DragMode = DragMode.DragToScroll;
///
/// Clicking is enabled by default, but can be disabled by
/// setting this property to `false`.
///
public bool ClickingEnabled = true;
///
/// Hover interactions are enabled by default, but can be disabled by
/// setting this property to `false`.
/// Note that hovering only works for webview implementations that
/// support the `IWithMovablePointer` interface (i.e. Android, Windows, and macOS).
///
[Tooltip("Note: Hovering is not supported on iOS or UWP.")]
public bool HoveringEnabled = true;
///
/// Scrolling is enabled by default, but can be disabled by
/// setting this property to `false`.
///
public bool ScrollingEnabled = true;
///
/// Determines the threshold (in web pixels) for triggering a drag. The default is 20.
///
///
/// When the `DragMode` is set to `DragToScroll`, this property determines
/// the distance that the pointer must drag before it's no longer
/// considered a click.
///
///
/// When the `DragMode` is set to `DragWithinPage`, this property determines
/// the distance that the pointer must drag before it triggers
/// a drag within the page.
///
[Label("Drag Threshold (px)")]
[Tooltip("Determines the threshold (in web pixels) for triggering a drag.")]
public float DragThreshold = 20;
[Obsolete("The WebViewPrefab.DragToScrollThreshold property is obsolete. Please use DragThreshold instead.")]
public float DragToScrollThreshold { get; set; }
[Header("Debugging")]
///
/// Determines whether remote debugging is enabled with `Web.EnableRemoteDebugging()`.
///
[Tooltip("Determines whether remote debugging is enabled with Web.EnableRemoteDebugging().")]
public bool RemoteDebuggingEnabled = false;
///
/// Determines whether JavaScript console messages from `IWebView.ConsoleMessageLogged`
/// are printed to the Unity logs.
///
[Tooltip("Determines whether JavaScript console messages are printed to the Unity logs.")]
public bool LogConsoleMessages = false;
///
/// The prefab's material.
///
public Material Material {
get {
return _view.Material;
}
set {
_view.Material = value;
}
}
///
/// Controls whether the instance is visible or hidden.
///
public virtual bool Visible {
get {
return _view.Visible;
}
set {
_view.Visible = value;
if (_videoLayer != null) {
_videoLayer.Visible = value;
}
}
}
///
/// A reference to the prefab's `IWebView` instance, which
/// is available after the `Initialized` event is raised.
/// Before initialization is complete, this property is `null`.
///
public IWebView WebView { get { return _webView; }}
///
/// Destroys the instance and its children. Note that you don't have
/// to call this method if you destroy the instance's parent with
/// `Object.Destroy()`.
///
public void Destroy() {
UnityEngine.Object.Destroy(gameObject);
}
public void SetCutoutRect(Rect rect) {
_view.SetCutoutRect(rect);
}
///
/// By default, the prefab detects pointer input events like clicks through
/// Unity's event system, but you can use this method to override the way that
/// input events are detected.
///
public void SetPointerInputDetector(IPointerInputDetector pointerInputDetector) {
var previousPointerInputDetector = _pointerInputDetector;
_pointerInputDetector = pointerInputDetector;
// If _webView hasn't been set yet, then _initPointerInputDetector
// will get called before it's set to initialize _pointerInputDetector.
if (_webView != null) {
_initPointerInputDetector(_webView, previousPointerInputDetector);
}
}
#if NET_4_6 || NET_STANDARD_2_0
///
/// Returns a task that resolves when the prefab is initialized
/// (i.e. when its `WebView` property is ready for use).
///
///
public Task WaitUntilInitialized() {
var taskCompletionSource = new TaskCompletionSource();
var isInitialized = _webView != null;
if (isInitialized) {
taskCompletionSource.SetResult(true);
} else {
Initialized += (sender, e) => taskCompletionSource.SetResult(true);
}
return taskCompletionSource.Task;
}
#endif
[SerializeField]
[HideInInspector]
ViewportMaterialView _cachedVideoLayer;
[SerializeField]
[HideInInspector]
ViewportMaterialView _cachedView;
IWebView _cachedWebView;
// Used for DragMode.DragToScroll and DragMode.Disabled
bool _clickIsPending;
bool _consoleMessageLoggedHandlerAttached;
bool _loggedDragWarning;
WebViewOptions _options;
[SerializeField]
[HideInInspector]
GameObject _pointerInputDetectorGameObject;
IPointerInputDetector _pointerInputDetector {
get {
return _pointerInputDetectorGameObject == null ? null :
_pointerInputDetectorGameObject.GetComponent();
}
set {
var monoBehaviour = value as MonoBehaviour;
if (monoBehaviour == null) {
throw new ArgumentException("The provided IPointerInputDetector can't be successfully set because it's not a MonoBehaviour");
}
_pointerInputDetectorGameObject = monoBehaviour.gameObject;
}
}
bool _pointerIsDown;
Vector2 _pointerDownRatioPoint;
Vector2 _previousDragPoint;
Vector2 _previousMovePointerPoint;
static bool _remoteDebuggingEnabled;
protected ViewportMaterialView _videoLayer {
get {
if (_cachedVideoLayer == null) {
_cachedVideoLayer = _getVideoLayer();
}
return _cachedVideoLayer;
}
}
bool _videoLayerDisabled;
Material _videoMaterial;
protected ViewportMaterialView _view {
get {
if (_cachedView == null) {
_cachedView = _getView();
}
return _cachedView;
}
}
Material _viewMaterial;
[SerializeField]
[HideInInspector]
GameObject _webViewGameObject;
protected IWebView _webView {
get {
if (_cachedWebView == null) {
if (_webViewGameObject == null) {
return null;
}
_cachedWebView = _webViewGameObject.GetComponent();
}
return _cachedWebView;
}
set {
var monoBehaviour = value as MonoBehaviour;
if (monoBehaviour == null) {
throw new ArgumentException("The IWebView cannot be set successfully because it's not a MonoBehaviour.");
}
_webViewGameObject = monoBehaviour.gameObject;
_cachedWebView = value;
}
}
void _attachWebViewEventHandlers(IWebView webView) {
if (!_options.disableVideo) {
webView.VideoRectChanged += (sender, e) => _setVideoRect(e.Value);
}
if (LogConsoleMessages) {
_consoleMessageLoggedHandlerAttached = true;
webView.ConsoleMessageLogged += WebView_ConsoleMessageLogged;
}
}
protected abstract Vector2 _convertRatioPointToUnityUnits(Vector2 point);
protected abstract float _getInitialResolution();
protected abstract float _getScrollingSensitivity();
protected abstract ViewportMaterialView _getVideoLayer();
protected abstract ViewportMaterialView _getView();
protected void _init(float width, float height, WebViewOptions options = new WebViewOptions(), IWebView initializedWebView = null) {
_throwExceptionIfInitialized();
// Remote debugging can only be enabled once, before any webviews are initialized.
if (RemoteDebuggingEnabled && !_remoteDebuggingEnabled) {
_remoteDebuggingEnabled = true;
Web.EnableRemoteDebugging();
}
_options = options;
// Only set _webView *after* the webview has been initialized to guarantee
// that WebViewPrefab.WebView is ready to use as long as it's not null.
var webView = initializedWebView == null ? Web.CreateWebView(_options.preferredPlugins) : initializedWebView;
Web.CreateMaterial(viewMaterial => {
_viewMaterial = viewMaterial;
_view.Material = viewMaterial;
_initWebViewIfReady(webView, width, height);
});
if (_options.disableVideo) {
_videoLayerDisabled = true;
if (_videoLayer != null) {
_videoLayer.Visible = false;
}
_initWebViewIfReady(webView, width, height);
} else {
Web.CreateVideoMaterial(videoMaterial => {
if (videoMaterial == null) {
_videoLayerDisabled = true;
if (_videoLayer != null) {
_videoLayer.Visible = false;
}
} else {
_videoMaterial = videoMaterial;
_videoLayer.Material = videoMaterial;
_setVideoRect(new Rect(0, 0, 0, 0));
}
_initWebViewIfReady(webView, width, height);
});
}
}
void _initPointerInputDetector(IWebView webView, IPointerInputDetector previousPointerInputDetector = null) {
if (previousPointerInputDetector != null) {
previousPointerInputDetector.BeganDrag -= InputDetector_BeganDrag;
previousPointerInputDetector.Dragged -= InputDetector_Dragged;
previousPointerInputDetector.PointerDown -= InputDetector_PointerDown;
previousPointerInputDetector.PointerExited -= InputDetector_PointerExited;
previousPointerInputDetector.PointerMoved -= InputDetector_PointerMoved;
previousPointerInputDetector.PointerUp -= InputDetector_PointerUp;
previousPointerInputDetector.Scrolled -= InputDetector_Scrolled;
}
if (_pointerInputDetector == null) {
_pointerInputDetector = GetComponentInChildren();
}
// Only enable the PointerMoved event if the webview implementation has MovePointer().
_pointerInputDetector.PointerMovedEnabled = (webView as IWithMovablePointer) != null;
_pointerInputDetector.BeganDrag += InputDetector_BeganDrag;
_pointerInputDetector.Dragged += InputDetector_Dragged;
_pointerInputDetector.PointerDown += InputDetector_PointerDown;
_pointerInputDetector.PointerExited += InputDetector_PointerExited;
_pointerInputDetector.PointerMoved += InputDetector_PointerMoved;
_pointerInputDetector.PointerUp += InputDetector_PointerUp;
_pointerInputDetector.Scrolled += InputDetector_Scrolled;
}
void _initWebViewIfReady(IWebView webView, float width, float height) {
if (_view.Texture == null || (!_videoLayerDisabled && _videoLayer.Texture == null)) {
// Wait until both views' textures are ready.
return;
}
var initializedWebViewWasProvided = webView.IsInitialized;
if (initializedWebViewWasProvided) {
// An initialized webview was provided via WebViewPrefab.Init(IWebView),
// so just hook up its existing textures.
_view.Texture = webView.Texture;
if (_videoLayer != null) {
_videoLayer.Texture = webView.VideoTexture;
}
} else {
// Set the resolution prior to initializing the webview
// so the initial size is correct.
var initialResolution = _getInitialResolution();
if (initialResolution <= 0) {
WebViewLogger.LogWarningFormat("Invalid value for InitialResolution ({0}) will be ignored.", initialResolution);
} else {
webView.SetResolution(initialResolution);
}
var videoTexture = _videoLayer == null ? null : _videoLayer.Texture;
webView.Init(_view.Texture, width, height, videoTexture);
}
_attachWebViewEventHandlers(webView);
// Init the pointer input detector just before setting _webView so that
// SetPointerInputDetector() will behave correctly if it's called before _webView is set.
_initPointerInputDetector(webView);
// _webView can be set now that the webview is initialized.
_webView = webView;
var handler = Initialized;
if (handler != null) {
handler(this, EventArgs.Empty);
}
if (!String.IsNullOrEmpty(InitialUrl)) {
if (initializedWebViewWasProvided) {
WebViewLogger.LogWarning("Custom InitialUrl value will be ignored because an initialized webview was provided.");
} else {
var url = InitialUrl.Trim();
if (!url.Contains(":")) {
url = "http://" + url;
}
webView.LoadUrl(url);
}
}
}
void InputDetector_BeganDrag(object sender, EventArgs eventArgs) {
_previousDragPoint = _convertRatioPointToUnityUnits(_pointerDownRatioPoint);
}
void InputDetector_Dragged(object sender, EventArgs eventArgs) {
if (DragMode == DragMode.Disabled || _webView == null) {
return;
}
var point = eventArgs.Value;
var previousDragPoint = _previousDragPoint;
var newDragPoint = _convertRatioPointToUnityUnits(point);
_previousDragPoint = newDragPoint;
var totalDragDelta = _convertRatioPointToUnityUnits(_pointerDownRatioPoint) - newDragPoint;
if (DragMode == DragMode.DragWithinPage) {
var dragThresholdReached = totalDragDelta.magnitude * _webView.Resolution > DragThreshold;
if (dragThresholdReached) {
_movePointerIfNeeded(point);
}
return;
}
// DragMode == DragMode.DragToScroll
var dragDelta = previousDragPoint - newDragPoint;
_scrollIfNeeded(dragDelta, _pointerDownRatioPoint);
// Check whether to cancel a pending viewport click so that drag-to-scroll
// doesn't unintentionally trigger a click.
if (_clickIsPending) {
var dragThresholdReached = totalDragDelta.magnitude * _webView.Resolution > DragThreshold;
if (dragThresholdReached) {
_clickIsPending = false;
}
}
}
protected virtual void InputDetector_PointerDown(object sender, PointerEventArgs eventArgs) {
_pointerIsDown = true;
_pointerDownRatioPoint = eventArgs.Point;
if (!ClickingEnabled || _webView == null) {
return;
}
if (DragMode == DragMode.DragWithinPage) {
var webViewWithPointerDown = _webView as IWithPointerDownAndUp;
if (webViewWithPointerDown != null) {
webViewWithPointerDown.PointerDown(eventArgs.Point, eventArgs.ToPointerOptions());
return;
} else if (!_loggedDragWarning) {
_loggedDragWarning = true;
WebViewLogger.LogWarningFormat("The WebViewPrefab's DragMode is set to DragWithinPage, but the webview implementation for this platform ({0}) doesn't support the PointerDown and PointerUp methods needed for dragging within a page. For more info, see https://developer.vuplex.com/webview/IWithPointerDownAndUp.", _webView.PluginType);
// Fallback to setting _clickIsPending so Click() can be called.
}
}
// Defer calling PointerDown() for DragToScroll so that the click can
// be cancelled if drag exceeds the threshold needed to become a scroll.
_clickIsPending = true;
}
void InputDetector_PointerExited(object sender, EventArgs eventArgs) {
if (HoveringEnabled) {
// Remove the hover state when the pointer exits.
_movePointerIfNeeded(Vector2.zero);
}
}
void InputDetector_PointerMoved(object sender, EventArgs eventArgs) {
// InputDetector_Dragged handles calling MovePointer while dragging.
if (_pointerIsDown || !HoveringEnabled) {
return;
}
_movePointerIfNeeded(eventArgs.Value);
}
protected virtual void InputDetector_PointerUp(object sender, PointerEventArgs eventArgs) {
_pointerIsDown = false;
if (!ClickingEnabled || _webView == null) {
return;
}
var webViewWithPointerDownAndUp = _webView as IWithPointerDownAndUp;
if (DragMode == DragMode.DragWithinPage && webViewWithPointerDownAndUp != null) {
var totalDragDelta = _convertRatioPointToUnityUnits(_pointerDownRatioPoint) - _convertRatioPointToUnityUnits(eventArgs.Point);
var dragThresholdReached = totalDragDelta.magnitude * _webView.Resolution > DragThreshold;
var pointerUpPoint = dragThresholdReached ? eventArgs.Point : _pointerDownRatioPoint;
webViewWithPointerDownAndUp.PointerUp(pointerUpPoint, eventArgs.ToPointerOptions());
} else {
if (!_clickIsPending) {
return;
}
_clickIsPending = false;
// PointerDown() and PointerUp() don't support the preventStealingFocus parameter.
if (webViewWithPointerDownAndUp == null || _options.clickWithoutStealingFocus) {
_webView.Click(eventArgs.Point, _options.clickWithoutStealingFocus);
} else {
var pointerOptions = eventArgs.ToPointerOptions();
webViewWithPointerDownAndUp.PointerDown(eventArgs.Point, pointerOptions);
webViewWithPointerDownAndUp.PointerUp(eventArgs.Point, pointerOptions);
}
}
var handler = Clicked;
if (handler != null) {
handler(this, new ClickedEventArgs(eventArgs.Point));
}
}
void InputDetector_Scrolled(object sender, ScrolledEventArgs eventArgs) {
var sensitivity = _getScrollingSensitivity();
var scaledScrollDelta = new Vector2(
eventArgs.ScrollDelta.x * sensitivity,
eventArgs.ScrollDelta.y * sensitivity
);
_scrollIfNeeded(scaledScrollDelta, eventArgs.Point);
}
void _movePointerIfNeeded(Vector2 point) {
var webViewWithMovablePointer = _webView as IWithMovablePointer;
if (webViewWithMovablePointer == null) {
return;
}
if (point != _previousMovePointerPoint) {
_previousMovePointerPoint = point;
webViewWithMovablePointer.MovePointer(point);
}
}
void OnDestroy() {
if (_webView != null && !_webView.IsDisposed) {
_webView.Dispose();
}
Destroy();
// Unity doesn't automatically destroy materials and textures
// when the GameObject is destroyed.
if (_viewMaterial != null) {
Destroy(_viewMaterial.mainTexture);
Destroy(_viewMaterial);
}
if (_videoMaterial != null) {
Destroy(_videoMaterial.mainTexture);
Destroy(_videoMaterial);
}
}
void _scrollIfNeeded(Vector2 scrollDelta, Vector2 point) {
// scrollDelta can be zero when the user drags the cursor off the screen.
if (!ScrollingEnabled || _webView == null || scrollDelta == Vector2.zero) {
return;
}
_webView.Scroll(scrollDelta, point);
var handler = Scrolled;
if (handler != null) {
handler(this, new ScrolledEventArgs(scrollDelta, point));
}
}
protected abstract void _setVideoLayerPosition(Rect videoRect);
void _setVideoRect(Rect videoRect) {
_view.SetCutoutRect(videoRect);
_setVideoLayerPosition(videoRect);
// This code applies a cropping rect to the video layer's shader based on what part of the video (if any)
// falls outside of the viewport and therefore needs to be hidden. Note that the dimensions here are divided
// by the videoRect's width or height, because in the videoLayer shader, the width of the videoRect is 1
// and the height is 1 (i.e. the dimensions are normalized).
float videoRectXMin = Math.Max(0, - 1 * videoRect.x / videoRect.width);
float videoRectYMin = Math.Max(0, -1 * videoRect.y / videoRect.height);
float videoRectXMax = Math.Min(1, (1 - videoRect.x) / videoRect.width);
float videoRectYMax = Math.Min(1, (1 - videoRect.y) / videoRect.height);
var videoCropRect = Rect.MinMaxRect(videoRectXMin, videoRectYMin, videoRectXMax, videoRectYMax);
if (videoCropRect == new Rect(0, 0, 1, 1)) {
// The entire video rect fits within the viewport, so set the cropt rect to zero to disable it.
videoCropRect = new Rect(0, 0, 0, 0);
}
_videoLayer.SetCropRect(videoCropRect);
}
void _throwExceptionIfInitialized() {
if (_webView != null) {
throw new InvalidOperationException("Init() cannot be called on a WebViewPrefab that has already been initialized.");
}
}
void Update() {
// Check if LogConsoleMessages is changed from false to true at runtime.
if (LogConsoleMessages && !_consoleMessageLoggedHandlerAttached && _webView != null) {
_consoleMessageLoggedHandlerAttached = true;
_webView.ConsoleMessageLogged += WebView_ConsoleMessageLogged;
}
}
void WebView_ConsoleMessageLogged(object sender, ConsoleMessageEventArgs eventArgs) {
if (!LogConsoleMessages) {
return;
}
var message = "[Web Console] " + eventArgs.Message;
if (eventArgs.Source != null) {
message += String.Format(" ({0}:{1})", eventArgs.Source, eventArgs.Line);
}
switch (eventArgs.Level) {
case ConsoleMessageLevel.Error:
WebViewLogger.LogError(message, false);
break;
case ConsoleMessageLevel.Warning:
WebViewLogger.LogWarning(message, false);
break;
default:
WebViewLogger.Log(message, false);
break;
}
}
}
}