// 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.
using System;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.XR;
using Vuplex.WebView.Internal;
namespace Vuplex.WebView {
public abstract class BaseWebViewPrefab : MonoBehaviour {
///
/// Indicates that the prefab was clicked. Note that the prefab automatically
/// calls IWebView.Click() for you.
///
///
/// This event is not supported when running in [Native 2D Mode](https://support.vuplex.com/articles/native-2d-mode).
///
///
///
/// webViewPrefab.Clicked += (sender, eventArgs) => {
/// Debug.Log("WebViewPrefab was clicked at point: " + eventArgs.Point);
/// };
///
///
public virtual event EventHandler Clicked;
///
/// Indicates that the prefab finished initializing,
/// so its WebView property is ready to use.
///
///
public event EventHandler Initialized;
///
/// Indicates that the pointer (e.g. mouse cursor) entered the prefab.
///
public event EventHandler PointerEntered;
///
/// Indicates that the pointer (e.g. mouse cursor) exited the prefab.
///
public event EventHandler PointerExited;
///
/// Indicates that the prefab was scrolled. Note that the prefab automatically
/// calls IWebView.Scroll() for you.
///
///
/// This event is not supported when running in [Native 2D Mode](https://support.vuplex.com/articles/native-2d-mode).
///
///
/// webViewPrefab.Scrolled += (sender, eventArgs) => {
/// Debug.Log($"WebViewPrefab was scrolled. Point: {eventArgs.Point}, scroll delta: {eventArgs.ScrollDelta}");
/// };
///
public virtual event EventHandler Scrolled;
///
/// Determines whether clicking is enabled. The default is `true`.
///
///
/// This property is ignored when running in [Native 2D Mode](https://support.vuplex.com/articles/native-2d-mode).
///
public bool ClickingEnabled = true;
///
/// Determines whether the mouse cursor icon is automatically updated based on interaction with
/// the web page. For example, hovering over a link causes the mouse cursor icon to turn into a pointer hand.
/// The default is `true`. CursorIconsEnabled is currently only supported by 3D WebView for Windows and macOS.
///
///
[Label("Cursor Icons Enabled (Windows and macOS only)")]
[Tooltip("(Windows and macOS only) Sets whether the mouse cursor icon is automatically updated based on interaction with the web page. For example, hovering over a link causes the mouse cursor icon to turn into a pointer hand.")]
public bool CursorIconsEnabled = true;
///
/// Determines how the prefab handles drag interactions.
///
///
/// Important notes:
///
/// - This property is ignored when running in [Native 2D Mode](https://support.vuplex.com/articles/native-2d-mode).
/// -
/// For information on the limitations of drag interactions on iOS and UWP, please see
/// [this article](https://support.vuplex.com/articles/hover-and-drag-limitations).
///
/// -
/// The Android Gecko package doesn't support the HTML Drag and Drop API (GeckoView limitation).
///
///
///
/// When I drag a scrollbar, why does it scroll the wrong way?
[Tooltip("Determines how the prefab handles drag interactions. Note that This property is ignored when running in Native 2D Mode.")]
public DragMode DragMode = DragMode.DragToScroll;
///
/// Determines the threshold (in web pixels) for triggering a drag. The default is `20`.
///
///
///
/// -
/// When the prefab's 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 prefab's DragMode is set to DragWithinPage, this property determines
/// the distance that the pointer must drag before it triggers
/// a drag within the page.
///
/// - This property is ignored when running in [Native 2D Mode](https://support.vuplex.com/articles/native-2d-mode).
///
///
[Label("Drag Threshold (px)")]
[Tooltip("Determines the threshold (in web pixels) for triggering a drag.")]
public float DragThreshold = 20;
///
/// Determines whether hover interactions are enabled. The default is `true`.
///
///
/// Important notes:
///
/// - This property is ignored when running in [Native 2D Mode](https://support.vuplex.com/articles/native-2d-mode).
/// -
/// For information on the limitations of hovering on iOS and UWP, please see
/// [this article](https://support.vuplex.com/articles/hover-and-drag-limitations).
///
///
///
public bool HoveringEnabled = true;
///
/// If you drag the prefab into the scene via the editor,
/// you can set this property to make it so that the instance
/// automatically loads the given URL after it initializes. To load a new URL
/// at runtime, use IWebView.LoadUrl() instead.
///
/// How to load local files
[Label("Initial URL (optional)")]
[Tooltip("You can set this to the URL that you want to load, or you can leave it blank if you'd rather add a script to load content programmatically with IWebView.LoadUrl() or LoadHtml().")]
[HideInInspector]
public string InitialUrl;
///
/// Determines whether the webview automatically receives keyboard input from the native keyboard and the Keyboard prefab. The default is `true`.
///
/// NativeOnScreenKeyboardEnabled
/// How does keyboard input work?
[Tooltip("Determines whether the webview automatically receives keyboard input from the native keyboard and the Keyboard prefab.")]
public bool KeyboardEnabled = true;
///
/// Determines whether JavaScript console messages from IWebView.ConsoleMessageLogged
/// are printed to the Unity logs. The default is `false`.
///
[Tooltip("Determines whether JavaScript console messages are printed to the Unity logs.")]
public bool LogConsoleMessages = false;
///
/// Gets or sets prefab's material.
///
///
/// This property is unused when running in [Native 2D Mode](https://support.vuplex.com/articles/native-2d-mode).
///
public Material Material {
get => _view.Material;
set => _view.Material = value;
}
///
/// Sets the webview's pixel density, which is its number of physical pixels per logical pixel.
/// The default value is `1`, but increasing it to `2` can make web content appear sharper
/// or less blurry on high DPI displays. PixelDensity is currently only supported by
/// 3D WebView for Windows and macOS.
///
///
///
/// // Increase the pixel density to 2 for high DPI screens.
/// webViewPrefab.PixelDensity = 2;
///
///
///
[Label("Pixel Density (Windows and macOS only)")]
[Tooltip("(Windows and macOS only) Sets the webview's pixel density.")]
public float PixelDensity = 1;
///
/// Determines whether the prefab enables remote debugging by calling Web.EnableRemoteDebugging().
/// The default is `false`.
///
///
[Header("Debugging")]
[Tooltip("Determines whether remote debugging is enabled.")]
public bool RemoteDebuggingEnabled = false;
///
/// Determines whether scrolling is enabled. The default is `true`.
///
///
/// This property is ignored when running in [Native 2D Mode](https://support.vuplex.com/articles/native-2d-mode).
///
public bool ScrollingEnabled = true;
///
/// Gets or sets whether the instance is visible and responds to raycasts. The default is `true`.
/// If you want to hide the webview, setting this property to false is often preferable to disabling
/// the prefab's GameObject, because the latter stops the prefab's scripts from running, which may
/// prevent it from initializating, for example.
///
public virtual bool Visible {
get => _visible;
set {
_visible = value;
_view.gameObject.SetActive(value);
if (_videoLayerIsEnabled) {
_videoLayer.gameObject.SetActive(value);
}
}
}
///
/// Returns the prefab's IWebView instance, or `null` if the prefab hasn't finished
/// initializing yet. To detect when the WebView property is no longer null,
/// please use WaitUntilInitialized().
///
///
///
/// await webViewPrefab.WaitUntilInitialized();
/// // Now the WebView property is ready.
/// webViewPrefab.WebView.LoadUrl("https://vuplex.com");
///
///
public IWebView WebView {
get {
if (_cachedWebView == null) {
if (_webViewGameObject == null) {
return null;
}
_cachedWebView = _webViewGameObject.GetComponent();
}
return _cachedWebView;
}
private 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;
}
}
///
/// Converts the given point from web browser viewport coordinates to the
/// corresponding Unity screen space coordinates of where the point is rendered
/// in the application window. Note that web browser coordinates (the input parameters)
/// treat the top left corner of the browser viewport as the (0, 0) origin. In contrast,
/// Unity's screen space coordinates (the return value) treat the bottom left corner of the application's
/// window as the (0, 0) origin.
///
///
///
/// var screenPoint = webViewPrefab.BrowserToSreenPoint(200, 300);
/// Debug.Log($"Point (200px, 300px) within the browser window is rendered at point {screenPoint} on the device screen.");
///
///
public abstract Vector2 BrowserToScreenPoint(int xInPixels, int yInPixels);
///
/// 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().
///
///
///
/// // The webview can no longer be used after it's destroyed.
/// webViewPrefab.Destroy();
///
///
public void Destroy() => Destroy(gameObject);
public void SetCutoutRect(Rect rect) => _view.SetCutoutRect(rect);
///
/// Sets options that can be used to alter the webview that the prefab creates
/// during initialization. This method can only be called prior to
/// when the prefab initializes (i.e. directly after instantiating it or setting it to active).
///
///
///
/// using System;
/// using UnityEngine;
/// using Vuplex.WebView;
///
/// class SetOptions : MonoBehaviour {
///
/// // IMPORTANT: With this approach, you must set your WebViewPrefab or CanvasWebViewPrefab to inactive
/// // in the scene hierarchy so that this script can manually activate it. Otherwise, this
/// // script won't be able to call SetOptionsForInitialization() before the prefab initializes.
/// //
/// // TODO: Set this webViewPrefab field to the inactive WebViewPrefab or CanvasWebViewPrefab in your scene
/// // via the Editor's Inspector tab.
/// public BaseWebViewPrefab webViewPrefab;
///
/// void Awake() {
///
/// if (webViewPrefab.gameObject.activeInHierarchy) {
/// throw new Exception("The WebViewPrefab object is active in the Editor's scene view. Please set it to inactive so that this script can manually activate it.");
/// }
/// webViewPrefab.gameObject.SetActive(true);
///
/// webViewPrefab.SetOptionsForInitialization(new WebViewOptions {
/// preferredPlugins = new WebPluginType[] { WebPluginType.Android }
/// });
/// }
/// }
///
///
public void SetOptionsForInitialization(WebViewOptions options) {
if (WebView != null) {
throw new ArgumentException("SetOptionsForInitialization() was called after the prefab was already initialized. Please call it before initialization instead.");
}
_options = options;
}
///
/// 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.
///
///
///
/// var yourCustomInputDetector = webViewPrefab.Collider.AddComponent<YourCustomInputDetector>();
/// webViewPrefab.SetPointerInputDetector(yourCustomInputDetector);
///
///
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) {
_attachOrDetachPointerInputDetector(previousPointerInputDetector, false);
_initPointerInputDetector(WebView);
}
}
///
/// By default, the prefab creates a new IWebView during initialization. However,
/// you can call this method before the prefab initializes to pass it an existing,
/// initialized IWebView to use instead. This method can only be called prior to
/// when the prefab initializes (i.e. directly after instantiating it or setting it to active).
///
public void SetWebViewForInitialization(IWebView webView) {
if (WebView != null) {
throw new ArgumentException("SetWebViewForInitialization() was called after the prefab was already initialized. Please call it before initialization instead.");
}
if (webView != null && !webView.IsInitialized) {
throw new ArgumentException("SetWebViewForInitialization(IWebView) was called with an uninitialized webview, but an initialized webview is required.");
}
_webViewForInitialization = webView;
}
///
/// Returns a task that completes when the prefab is initialized,
/// which means that its WebView property is ready for use.
///
///
/// await webViewPrefab.WaitUntilInitialized();
/// // Now the WebView property is ready.
/// webViewPrefab.WebView.LoadUrl("https://vuplex.com");
///
public Task WaitUntilInitialized() {
var taskSource = new TaskCompletionSource();
var isInitialized = WebView != null;
if (isInitialized) {
taskSource.SetResult(true);
} else {
Initialized += (sender, e) => taskSource.SetResult(true);
}
return taskSource.Task;
}
#region Non-public members
float _appliedResolution;
[SerializeField]
[HideInInspector]
ViewportMaterialView _cachedVideoLayer;
[SerializeField]
[HideInInspector]
ViewportMaterialView _cachedView;
IWebView _cachedWebView;
bool _consoleMessageLoggedHandlerAttached;
bool _dragThresholdReached;
bool _dragToScrollClickIsPending;
bool _hasOverriddenCursorIcon;
int _heightInPixels { get => (int)(_sizeInUnityUnits.y * _appliedResolution); }
bool _keyboardHasBeenEnabled;
bool _loggedDragWarning;
static bool _loggedHoverWarning;
protected WebViewOptions _options;
[SerializeField]
[HideInInspector]
MonoBehaviour _pointerInputDetectorMonoBehaviour;
IPointerInputDetector _pointerInputDetector {
get => _pointerInputDetectorMonoBehaviour as IPointerInputDetector;
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");
}
_pointerInputDetectorMonoBehaviour = monoBehaviour;
}
}
bool _pointerIsDown;
Vector2 _pointerDownNormalizedPoint;
Vector2 _previousNormalizedDragPoint;
Vector2 _previousMovePointerPoint;
static bool _remoteDebuggingEnabled;
protected Vector2 _sizeInUnityUnits;
protected ViewportMaterialView _videoLayer {
get {
if (_cachedVideoLayer == null) {
_cachedVideoLayer = _getVideoLayer();
}
return _cachedVideoLayer;
}
}
bool _videoLayerIsEnabled {
get => _videoLayer != null && _videoLayer.gameObject.activeSelf;
set {
if (_videoLayer != null) {
_videoLayer.gameObject.SetActive(value);
}
}
}
Material _videoMaterial;
protected ViewportMaterialView _view {
get {
if (_cachedView == null) {
_cachedView = _getView();
}
return _cachedView;
}
}
Material _viewMaterial;
bool _visible = true;
protected IWebView _webViewForInitialization;
[SerializeField]
[HideInInspector]
GameObject _webViewGameObject;
int _widthInPixels { get => (int)(_sizeInUnityUnits.x * _appliedResolution); }
void _attachOrDetachPointerInputDetector(IPointerInputDetector detector, bool attach) {
if (attach) {
detector.BeganDrag += InputDetector_BeganDrag;
detector.Dragged += InputDetector_Dragged;
detector.PointerDown += InputDetector_PointerDown;
detector.PointerEntered += InputDetector_PointerEntered;
detector.PointerExited += InputDetector_PointerExited;
detector.PointerMoved += InputDetector_PointerMoved;
detector.PointerUp += InputDetector_PointerUp;
detector.Scrolled += InputDetector_Scrolled;
} else {
detector.BeganDrag -= InputDetector_BeganDrag;
detector.Dragged -= InputDetector_Dragged;
detector.PointerDown -= InputDetector_PointerDown;
detector.PointerEntered -= InputDetector_PointerEntered;
detector.PointerExited -= InputDetector_PointerExited;
detector.PointerMoved -= InputDetector_PointerMoved;
detector.PointerUp -= InputDetector_PointerUp;
detector.Scrolled -= InputDetector_Scrolled;
}
}
void _attachWebViewEventHandlers(IWebView webView) {
_enableConsoleMessagesIfNeeded(webView);
// Needed for Vulkan support on Android.
// See the comments in IWithChangingTexture.cs for details.
var webViewWithChangingTexture = webView as IWithChangingTexture;
if (webViewWithChangingTexture != null) {
webViewWithChangingTexture.TextureChanged += WebView_TextureChanged;
}
// Needed for fallback video support on iOS.
var webViewWithFallbackVideo = webView as IWithFallbackVideo;
if (webViewWithFallbackVideo != null && !_options.disableVideo) {
webViewWithFallbackVideo.VideoRectChanged += (sender, eventArgs) => _setVideoRect(eventArgs.Value);
}
}
void _disableHoveringIfNeeded(bool preferNative2DMode) {
#if (UNITY_IOS || UNITY_WSA) && !VUPLEX_NO_DISABLING_HOVER_FOR_PERFORMANCE
if (!HoveringEnabled) {
return;
}
if (preferNative2DMode) {
// Hovering isn't detected in Native 2D Mode, so logging a warning is unnecessary.
return;
}
HoveringEnabled = false;
if (!_loggedHoverWarning) {
_loggedHoverWarning = true;
WebViewLogger.LogWarning("WebViewPrefab.HoveringEnabled is automatically set to false by default on iOS and UWP in order to optimize performance. However, you can override this by adding the scripting symbol VUPLEX_NO_DISABLING_HOVER_FOR_PERFORMANCE in player settings. For more info, see https://support.vuplex.com/articles/hover-and-drag-limitations.");
}
#endif
}
void _enableConsoleMessagesIfNeeded(IWebView webView) {
if (LogConsoleMessages && !_consoleMessageLoggedHandlerAttached && webView != null) {
_consoleMessageLoggedHandlerAttached = true;
webView.ConsoleMessageLogged += WebView_ConsoleMessageLogged;
}
}
void _enableNativeOnScreenKeyboardIfNeeded(IWebView webView) {
if (webView is IWithNativeOnScreenKeyboard) {
var nativeOnScreenKeyboardEnabled = _getNativeOnScreenKeyboardEnabled();
(webView as IWithNativeOnScreenKeyboard).SetNativeOnScreenKeyboardEnabled(nativeOnScreenKeyboardEnabled);
}
}
void _enableOrDisableKeyboardIfNeeded() {
if (WebView != null && _keyboardHasBeenEnabled != KeyboardEnabled) {
Internal.KeyboardManager.Instance.SetKeyboardEnabled(this, KeyboardEnabled);
_keyboardHasBeenEnabled = KeyboardEnabled;
}
}
void _enableRemoteDebuggingIfNeeded() {
// Remote debugging can only be enabled once, before any webviews are initialized.
if (RemoteDebuggingEnabled && !_remoteDebuggingEnabled) {
_remoteDebuggingEnabled = true;
try {
Web.EnableRemoteDebugging();
} catch (Exception ex) {
WebViewLogger.LogError("An exception occurred while enabling remote debugging. On Windows and macOS, this can happen if a prefab with RemoteDebuggingEnabled = true is created after a prior webview has already been initialized. Exception message: " + ex);
}
}
}
protected abstract float _getResolution();
protected abstract bool _getNativeOnScreenKeyboardEnabled();
protected abstract float _getScrollingSensitivity();
IWithTouch _getTouchIfSupported() {
var webViewWithTouch = WebView as IWithTouch;
// Touch is currently disabled for the Android Gecko package because it currently has an
// issue where scrolling doesn't work correctly with touch.
if (webViewWithTouch != null && WebView.PluginType != WebPluginType.AndroidGecko) {
return webViewWithTouch;
}
return null;
}
protected abstract ViewportMaterialView _getVideoLayer();
protected abstract ViewportMaterialView _getView();
void _handleTrialExpired() {
_view.Material = new Material(Resources.Load("TrialExpiredMaterial"));
_videoLayer.gameObject.SetActive(false);
}
protected async Task _initBase(Rect rect, bool preferNative2DMode = false) {
_throwExceptionIfInitialized();
_sizeInUnityUnits = rect.size;
_updateResolutionIfNeeded();
_disableHoveringIfNeeded(preferNative2DMode);
_enableRemoteDebuggingIfNeeded();
// Note: this.WebView is only set after the webview has been initialized to guarantee
// that the property is ready to use as long as it's not null.
var webView = await _initWebView(rect, preferNative2DMode);
if (this == null) {
// This prefab was destroyed while waiting for the webview to initialize.
webView.Dispose();
return;
}
_initViews(webView);
_enableNativeOnScreenKeyboardIfNeeded(webView);
_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.
if (!_native2DModeEnabled(webView)) {
_initPointerInputDetector(webView);
}
// The webview is now fully initialized, so we can now set WebView and raise the Initialized event.
WebView = webView;
// Intentionally call KeyboardManager.SetKeyboardEnabled() before raising the Initialized event so that it can attach a listener
// to the IWebView.FocusChanged event before the application is able to call IWebView.SetFocused().
_enableOrDisableKeyboardIfNeeded();
Initialized?.Invoke(this, EventArgs.Empty);
// Lastly, load the InitialUrl.
if (!String.IsNullOrWhiteSpace(InitialUrl)) {
if (_webViewForInitialization != null) {
WebViewLogger.LogWarning("Custom InitialUrl value will be ignored because an initialized webview was provided.");
} else {
webView.LoadUrl(InitialUrl.Trim());
}
}
}
void _initViews(IWebView webView) {
if (_native2DModeEnabled(webView)) {
if (_view != null) {
_view.gameObject.SetActive(false);
}
_videoLayerIsEnabled = false;
return;
}
// Initialize the main view.
_viewMaterial = webView.CreateMaterial();
_view.Material = _viewMaterial;
// Initialize the video view (iOS only).
var webViewWithFallbackVideo = webView as IWithFallbackVideo;
if (webViewWithFallbackVideo != null && webViewWithFallbackVideo.FallbackVideoEnabled) {
_videoMaterial = webViewWithFallbackVideo.CreateVideoMaterial();
_videoLayer.Material = _videoMaterial;
_setVideoRect(Rect.zero);
} else {
_videoLayerIsEnabled = false;
}
}
async Task _initWebView(Rect rect, bool preferNative2DMode) {
if (_webViewForInitialization != null) {
return _webViewForInitialization;
}
var webView = Web.CreateWebView(_options.preferredPlugins);
// Enable Native 2D Mode if needed.
var enableNative2DMode = preferNative2DMode && webView is IWithNative2DMode;
if (enableNative2DMode) {
var native2DWebView = webView as IWithNative2DMode;
try {
await native2DWebView.InitInNative2DMode(rect);
} catch (TrialExpiredException ex) {
_handleTrialExpired();
throw ex;
}
// Hide the webview if Visible has already been set to false.
native2DWebView.SetVisible(_visible);
return webView;
}
_updatePixelDensityIfNeeded(webView);
// (iOS only) Enable fallback video if needed.
var webViewWithFallbackVideo = webView as IWithFallbackVideo;
if (webViewWithFallbackVideo != null && !_options.disableVideo) {
webViewWithFallbackVideo.SetFallbackVideoEnabled(true);
}
try {
await webView.Init(_widthInPixels, _heightInPixels);
} catch (TrialExpiredException ex) {
_handleTrialExpired();
throw ex;
}
// (Windows and macOS only) Enable cursor icons if needed.
var webViewWithCursorType = webView as IWithCursorType;
if (webViewWithCursorType != null && CursorIconsEnabled && !XRSettings.enabled) {
webViewWithCursorType.CursorTypeChanged += (sender, eventArgs) => {
Internal.CursorHelper.SetCursorIcon(eventArgs.Value);
_hasOverriddenCursorIcon = eventArgs.Value != "default";
};
}
return webView;
}
void _initPointerInputDetector(IWebView webView) {
if (_pointerInputDetector == null) {
// Pass the argument `true` to find the IPointerInputDetector even if it's disabled.
// Otherwise, if BaseWebViewPrefab.Visible is set to `false` before initialization,
// the IPointerInputDetector won't be found, which will lead to a NullReferenceException.
_pointerInputDetector = GetComponentInChildren(true);
}
// Only enable the PointerMoved event if the webview implementation has MovePointer().
_pointerInputDetector.PointerMovedEnabled = (webView as IWithMovablePointer) != null;
_attachOrDetachPointerInputDetector(_pointerInputDetector, true);
}
void InputDetector_BeganDrag(object sender, EventArgs eventArgs) {
_dragThresholdReached = false;
_previousNormalizedDragPoint = _pointerDownNormalizedPoint;
}
void InputDetector_Dragged(object sender, EventArgs eventArgs) {
if (DragMode == DragMode.Disabled || WebView == null) {
return;
}
var newNormalizedDragPoint = eventArgs.Value;
var totalDragDeltaInPixels = WebView.NormalizedToPoint(_pointerDownNormalizedPoint - newNormalizedDragPoint);
if (!_dragThresholdReached) {
// _dragThresholdReached needs to be saved, otherwise it could flip from true back
// to false if the user drags back to the original point where the drag started.
_dragThresholdReached = totalDragDeltaInPixels.magnitude > DragThreshold;
}
if (DragMode == DragMode.DragWithinPage) {
if (_dragThresholdReached) {
_movePointerIfNeeded(newNormalizedDragPoint);
}
return;
}
// DragMode is DragToScroll
var webViewWithTouch = _getTouchIfSupported();
if (webViewWithTouch != null && _dragThresholdReached && !_options.clickWithoutStealingFocus) {
webViewWithTouch.SendTouchEvent(new TouchEvent {
TouchID = 1,
Point = newNormalizedDragPoint,
Type = TouchEventType.Move
});
return;
}
var normalizedDragDelta = _previousNormalizedDragPoint - newNormalizedDragPoint;
if (WebView.NormalizedToPoint(normalizedDragDelta) == Vector2Int.zero) {
// The latest drag delta was less than one pixel of difference, so wait until it reaches at
// least one pixel of change before setting _previousNormalizedDragPoint. Otherwise, small
// drags will be ignored, resulting in scrolling to not work if the drag is very slow.
return;
}
_previousNormalizedDragPoint = newNormalizedDragPoint;
_scrollIfNeeded(normalizedDragDelta, _pointerDownNormalizedPoint);
// Check whether to cancel a pending viewport click so that drag-to-scroll
// doesn't unintentionally trigger a click.
if (_dragToScrollClickIsPending && _dragThresholdReached) {
_dragToScrollClickIsPending = false;
}
}
protected virtual void InputDetector_PointerDown(object sender, PointerEventArgs eventArgs) {
_pointerIsDown = true;
_pointerDownNormalizedPoint = eventArgs.Point;
if (!ClickingEnabled || WebView == null) {
return;
}
if (DragMode == DragMode.DragToScroll) {
var webViewWithTouch = _getTouchIfSupported();
if (webViewWithTouch != null && !_options.clickWithoutStealingFocus) {
webViewWithTouch.SendTouchEvent(new TouchEvent {
TouchID = 1,
Point = eventArgs.Point,
Type = TouchEventType.Start
});
} else {
// For DragToScroll(), defer calling PointerDown() or Click() so that the click can
// be cancelled if the drag exceeds the threshold needed to become a scroll.
_dragToScrollClickIsPending = true;
}
return;
}
// For DragMode.DragWithinPage and DragMode.Disabled, call PointerDown() immediately if the webview supports IWithPointerDownAndUp.
// Note that PointerDown() doesn't currently support an option to avoid stealing focus, so Click() must be used in that case.
var webViewWithPointerDown = WebView as IWithPointerDownAndUp;
if (webViewWithPointerDown != null) {
webViewWithPointerDown.PointerDown(eventArgs.Point, eventArgs.ToPointerOptions(_options.clickWithoutStealingFocus));
} else if (DragMode == DragMode.DragWithinPage && webViewWithPointerDown == null && !_loggedDragWarning) {
_loggedDragWarning = true;
WebViewLogger.LogWarning($"The WebViewPrefab's DragMode is set to DragWithinPage, but the webview implementation for this platform ({WebView.PluginType}) doesn't support the PointerDown() and PointerUp() methods needed for dragging within a page. For more info, see https://developer.vuplex.com/webview/IWithPointerDownAndUp.");
}
}
void InputDetector_PointerEntered(object sender, EventArgs eventArgs) => PointerEntered?.Invoke(this, EventArgs.Empty);
void InputDetector_PointerExited(object sender, EventArgs eventArgs) {
if (HoveringEnabled) {
// Remove the hover state when the pointer exits.
_movePointerIfNeeded(eventArgs.Value, true);
}
if (_hasOverriddenCursorIcon) {
Internal.CursorHelper.SetCursorIcon(null);
_hasOverriddenCursorIcon = false;
}
PointerExited?.Invoke(this, EventArgs.Empty);
}
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 pointerUpPoint = eventArgs.Point;
var clickedEventArgs = new ClickedEventArgs(pointerUpPoint);
if (DragMode == DragMode.DragToScroll) {
var webViewWithTouch = _getTouchIfSupported();
if (webViewWithTouch != null && !_options.clickWithoutStealingFocus) {
webViewWithTouch.SendTouchEvent(new TouchEvent {
TouchID = 1,
Point = pointerUpPoint,
Type = TouchEventType.End
});
Clicked?.Invoke(this, clickedEventArgs);
return;
}
if (!_dragToScrollClickIsPending) {
// The click was cancelled because it was processed as a scroll.
return;
}
}
_dragToScrollClickIsPending = false;
var webViewWithPointerDownAndUp = WebView as IWithPointerDownAndUp;
if (webViewWithPointerDownAndUp == null) {
// When clickWithoutStealingFocus is enabled, use Click() because PointerDown() and PointerUp() don't support the preventStealingFocus parameter.
WebView.Click(eventArgs.Point, _options.clickWithoutStealingFocus);
} else {
var pointerOptions = eventArgs.ToPointerOptions(_options.clickWithoutStealingFocus);
if (DragMode == DragMode.DragToScroll) {
// For DragToScroll, PointerDown() is deferred until just before PointerUp() in case the click is cancelled.
webViewWithPointerDownAndUp.PointerDown(eventArgs.Point, pointerOptions);
} else if (DragMode == DragMode.DragWithinPage) {
// For DragWithinPage, use the point passed to PointerUp() if the DragThreshold wasn't reached.
var totalDragDeltaInPixels = WebView.NormalizedToPoint(_pointerDownNormalizedPoint - eventArgs.Point);
var dragThresholdReached = totalDragDeltaInPixels.magnitude > DragThreshold;
if (!dragThresholdReached) {
pointerUpPoint = _pointerDownNormalizedPoint;
}
}
webViewWithPointerDownAndUp.PointerUp(pointerUpPoint, pointerOptions);
}
Clicked?.Invoke(this, clickedEventArgs);
}
void InputDetector_Scrolled(object sender, ScrolledEventArgs eventArgs) {
var sensitivity = _getScrollingSensitivity();
// The ScrollingSensivity is measured in Unity units because the argument
// passed to Scroll(Vector2) was originally in Unity units (but is now a normalized value).
var scaledScrollDeltaInUnityUnits = eventArgs.ScrollDelta * sensitivity;
var normalizedScrollDelta = new Vector2(scaledScrollDeltaInUnityUnits.x / _sizeInUnityUnits.x, scaledScrollDeltaInUnityUnits.y / _sizeInUnityUnits.y);
_scrollIfNeeded(normalizedScrollDelta, eventArgs.Point);
}
void _movePointerIfNeeded(Vector2 point, bool pointerLeave = false) {
var webViewWithMovablePointer = WebView as IWithMovablePointer;
if (webViewWithMovablePointer == null) {
return;
}
if (point != _previousMovePointerPoint) {
_previousMovePointerPoint = point;
webViewWithMovablePointer.MovePointer(point, pointerLeave);
}
}
bool _native2DModeEnabled(IWebView webView) => webView is IWithNative2DMode && (webView as IWithNative2DMode).Native2DModeEnabled;
protected virtual void OnDestroy() {
if (WebView != null && !WebView.IsDisposed) {
WebView.Dispose();
}
if (KeyboardEnabled) {
var keyboardManager = Internal.KeyboardManager.Instance;
if (keyboardManager != null) {
keyboardManager.SetKeyboardEnabled(this, false);
}
}
if (_pointerInputDetector != null) {
// Detach the pointer input detector in case the app has assigned a custom one that's not a child.
_attachOrDetachPointerInputDetector(_pointerInputDetector, false);
_pointerInputDetectorMonoBehaviour = null;
}
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);
}
if (_hasOverriddenCursorIcon) {
Internal.CursorHelper.SetCursorIcon(null);
_hasOverriddenCursorIcon = false;
}
}
protected void _resizeWebViewIfNeeded() {
if (WebView != null && WebView.Size != new Vector2(_widthInPixels, _heightInPixels)) {
WebView.Resize(_widthInPixels, _heightInPixels);
}
}
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);
Scrolled?.Invoke(this, new ScrolledEventArgs(scrollDelta, point));
}
protected abstract void _setVideoLayerPosition(Rect videoRect);
void _setVideoRect(Rect videoRect) {
if (_videoLayer == null) {
return;
}
_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 = Rect.zero;
}
_videoLayer.SetCropRect(videoCropRect);
}
void _throwExceptionIfInitialized() {
if (WebView != null) {
throw new InvalidOperationException("Init() cannot be called on a WebViewPrefab that has already been initialized.");
}
}
protected virtual void Update() {
_updateResolutionIfNeeded();
_updatePixelDensityIfNeeded(WebView);
_enableOrDisableKeyboardIfNeeded();
_enableConsoleMessagesIfNeeded(WebView);
}
void _updatePixelDensityIfNeeded(IWebView webView) {
var webViewWithPixelDensity = webView as IWithPixelDensity;
if (webViewWithPixelDensity == null || PixelDensity == webViewWithPixelDensity.PixelDensity) {
return;
}
try {
webViewWithPixelDensity.SetPixelDensity(PixelDensity);
} catch (ArgumentException ex) {
WebViewLogger.LogError(ex.ToString());
PixelDensity = 1;
}
}
void _updateResolutionIfNeeded() {
var resolution = _getResolution();
if (_appliedResolution != resolution) {
if (resolution > 0.0f) {
_appliedResolution = resolution;
_resizeWebViewIfNeeded();
} else {
WebViewLogger.LogWarning("Ignoring invalid Resolution: " + resolution);
}
}
}
void WebView_ConsoleMessageLogged(object sender, ConsoleMessageEventArgs eventArgs) {
if (!LogConsoleMessages) {
return;
}
var message = "[Web Console] " + eventArgs.Message;
if (eventArgs.Source != null) {
message += $" ({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;
}
}
void WebView_TextureChanged(object sender, EventArgs eventArgs) {
var oldTexture = _view.Texture;
if (oldTexture is RenderTexture) {
// The application replaced WebViewPrefab.Material.mainTexture with a RenderTexture
// (for example, to implement MipMaps), so don't change the prefab's texture.
return;
}
_view.Texture = eventArgs.Value;
Destroy(oldTexture);
}
#endregion
// Added in v3.5, removed in v3.7.
[Obsolete("The WebViewPrefab.DragToScrollThreshold property has been removed. Please use DragThreshold instead: https://developer.vuplex.com/webview/WebViewPrefab#DragThreshold", true)]
public float DragToScrollThreshold { get; set; }
}
}