BaseWebViewPrefab.cs 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872
  1. // Copyright (c) 2022 Vuplex Inc. All rights reserved.
  2. //
  3. // Licensed under the Vuplex Commercial Software Library License, you may
  4. // not use this file except in compliance with the License. You may obtain
  5. // a copy of the License at
  6. //
  7. // https://vuplex.com/commercial-library-license
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. using System;
  15. using System.Threading.Tasks;
  16. using UnityEngine;
  17. using UnityEngine.EventSystems;
  18. using Vuplex.WebView.Internal;
  19. namespace Vuplex.WebView {
  20. public abstract class BaseWebViewPrefab : MonoBehaviour {
  21. /// <summary>
  22. /// Indicates that the prefab was clicked. Note that the prefab automatically
  23. /// calls IWebView.Click() for you.
  24. /// </summary>
  25. /// <remarks>
  26. /// This event is not supported when running in [Native 2D Mode](https://support.vuplex.com/articles/native-2d-mode).
  27. /// </remarks>
  28. /// <example>
  29. /// <code>
  30. /// webViewPrefab.Clicked += (sender, eventArgs) => {
  31. /// Debug.Log("WebViewPrefab was clicked at point: " + eventArgs.Point);
  32. /// };
  33. /// </code>
  34. /// </example>
  35. public virtual event EventHandler<ClickedEventArgs> Clicked;
  36. /// <summary>
  37. /// Indicates that the prefab finished initializing,
  38. /// so its WebView property is ready to use.
  39. /// </summary>
  40. /// <seealso cref="WaitUntilInitialized"/>
  41. public event EventHandler Initialized;
  42. /// <summary>
  43. /// Indicates that the prefab was scrolled. Note that the prefab automatically
  44. /// calls IWebView.Scroll() for you.
  45. /// </summary>
  46. /// <remarks>
  47. /// This event is not supported when running in [Native 2D Mode](https://support.vuplex.com/articles/native-2d-mode).
  48. /// </remarks>
  49. /// <example>
  50. /// webViewPrefab.Scrolled += (sender, eventArgs) => {
  51. /// Debug.Log($"WebViewPrefab was scrolled. Point: {eventArgs.Point}, scroll delta: {eventArgs.ScrollDelta}");
  52. /// };
  53. /// </example>
  54. public virtual event EventHandler<ScrolledEventArgs> Scrolled;
  55. /// <summary>
  56. /// Determines whether clicking is enabled. The default is `true`.
  57. /// </summary>
  58. /// <remarks>
  59. /// This property is ignored when running in [Native 2D Mode](https://support.vuplex.com/articles/native-2d-mode).
  60. /// </remarks>
  61. public bool ClickingEnabled = true;
  62. /// <summary>
  63. /// Determines whether the mouse cursor icon is automatically updated based on interaction with
  64. /// the web page. For example, hovering over a link causes the mouse cursor icon to turn into a pointer hand.
  65. /// The default is `true`. CursorIconsEnabled is currently only supported by 3D WebView for Windows and macOS.
  66. /// </summary>
  67. /// <seealso cref="IWithCursorType"/>
  68. [Label("Cursor Icons Enabled (Windows and macOS only)")]
  69. [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.")]
  70. public bool CursorIconsEnabled = true;
  71. /// <summary>
  72. /// Determines how the prefab handles drag interactions.
  73. /// </summary>
  74. /// <remarks>
  75. /// Important notes:
  76. /// <list type="bullet">
  77. /// <item>This property is ignored when running in [Native 2D Mode](https://support.vuplex.com/articles/native-2d-mode).</item>
  78. /// <item>
  79. /// For information on the limitations of drag interactions on iOS and UWP, please see
  80. /// [this article](https://support.vuplex.com/articles/hover-and-drag-limitations).
  81. /// </item>
  82. /// </list>
  83. /// </remarks>
  84. /// <seealso href="https://support.vuplex.com/articles/dragging-scrollbar">When I drag a scrollbar, why does it scroll the wrong way?</seealso>
  85. [Tooltip("Determines how the prefab handles drag interactions. Note that This property is ignored when running in Native 2D Mode.")]
  86. public DragMode DragMode = DragMode.DragToScroll;
  87. /// <summary>
  88. /// Determines the threshold (in web pixels) for triggering a drag. The default is `20`.
  89. /// </summary>
  90. /// <remarks>
  91. /// <list type="bullet">
  92. /// <item>
  93. /// When the prefab's DragMode is set to DragToScroll, this property determines
  94. /// the distance that the pointer must drag before it's no longer
  95. /// considered a click.
  96. /// </item>
  97. /// <item>
  98. /// When the prefab's DragMode is set to DragWithinPage, this property determines
  99. /// the distance that the pointer must drag before it triggers
  100. /// a drag within the page.
  101. /// </item>
  102. /// <item>This property is ignored when running in [Native 2D Mode](https://support.vuplex.com/articles/native-2d-mode).</item>
  103. /// </list>
  104. /// </remarks>
  105. [Label("Drag Threshold (px)")]
  106. [Tooltip("Determines the threshold (in web pixels) for triggering a drag.")]
  107. public float DragThreshold = 20;
  108. /// <summary>
  109. /// Determines whether hover interactions are enabled. The default is `true`.
  110. /// </summary>
  111. /// <remarks>
  112. /// Important notes:
  113. /// <list type="bullet">
  114. /// <item>This property is ignored when running in [Native 2D Mode](https://support.vuplex.com/articles/native-2d-mode).</item>
  115. /// <item>
  116. /// For information on the limitations of hovering on iOS and UWP, please see
  117. /// [this article](https://support.vuplex.com/articles/hover-and-drag-limitations).
  118. /// </item>
  119. /// </list>
  120. /// </remarks>
  121. public bool HoveringEnabled = true;
  122. /// <summary>
  123. /// If you drag the prefab into the scene via the editor,
  124. /// you can set this property to make it so that the instance
  125. /// automatically loads the given URL after it initializes. To load a new URL
  126. /// at runtime, use IWebView.LoadUrl() instead.
  127. /// </summary>
  128. /// <seealso href="https://support.vuplex.com/articles/how-to-load-local-files">How to load local files</seealso>
  129. [Label("Initial URL (optional)")]
  130. [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().")]
  131. [HideInInspector]
  132. public string InitialUrl;
  133. /// <summary>
  134. /// Determines whether JavaScript console messages from IWebView.ConsoleMessageLogged
  135. /// are printed to the Unity logs. The default is `false`.
  136. /// </summary>
  137. [Tooltip("Determines whether JavaScript console messages are printed to the Unity logs.")]
  138. public bool LogConsoleMessages = false;
  139. /// <summary>
  140. /// Gets or sets prefab's material.
  141. /// </summary>
  142. /// <remarks>
  143. /// This property is unused when running in [Native 2D Mode](https://support.vuplex.com/articles/native-2d-mode).
  144. /// </remarks>
  145. public Material Material {
  146. get { return _view.Material; }
  147. set { _view.Material = value; }
  148. }
  149. /// <summary>
  150. /// Sets the webview's pixel density, which is its number of physical pixels per logical pixel.
  151. /// The default value is `1`, but increasing it to `2` can make web content appear sharper
  152. /// or less blurry on high DPI displays. PixelDensity is currently only supported by
  153. /// 3D WebView for Windows and macOS.
  154. /// </summary>
  155. /// <example>
  156. /// <code>
  157. /// // Increase the pixel density to 2 for high DPI screens.
  158. /// webViewPrefab.PixelDensity = 2;
  159. /// </code>
  160. /// </example>
  161. /// <seealso cref="IWithPixelDensity"/>
  162. [Label("Pixel Density (Windows and macOS only)")]
  163. [Tooltip("(Windows and macOS only) Sets the webview's pixel density.")]
  164. public float PixelDensity = 1;
  165. /// <summary>
  166. /// Determines whether the prefab enables remote debugging by calling Web.EnableRemoteDebugging().
  167. /// The default is `false`.
  168. /// </summary>
  169. /// <seealso href="https://support.vuplex.com/articles/how-to-debug-web-content"/>
  170. [Header("Debugging")]
  171. [Tooltip("Determines whether remote debugging is enabled.")]
  172. public bool RemoteDebuggingEnabled = false;
  173. /// <summary>
  174. /// Determines whether scrolling is enabled. The default is `true`.
  175. /// </summary>
  176. /// <remarks>
  177. /// This property is ignored when running in [Native 2D Mode](https://support.vuplex.com/articles/native-2d-mode).
  178. /// </remarks>
  179. public bool ScrollingEnabled = true;
  180. /// <summary>
  181. /// Gets or sets whether the instance is visible. The default is `true`.
  182. /// </summary>
  183. public virtual bool Visible {
  184. get { return _view.Visible; }
  185. set {
  186. _view.Visible = value;
  187. if (_videoLayerIsEnabled) {
  188. _videoLayer.Visible = value;
  189. }
  190. }
  191. }
  192. /// <summary>
  193. /// Returns the prefab's IWebView instance, or `null` if the prefab hasn't finished
  194. /// initializing yet. To detect when the WebView property is no longer null,
  195. /// please use WaitUntilInitialized().
  196. /// </summary>
  197. /// <example>
  198. /// <code>
  199. /// await webViewPrefab.WaitUntilInitialized();
  200. /// // Now the WebView property is ready.
  201. /// webViewPrefab.WebView.LoadUrl("https://vuplex.com");
  202. /// </code>
  203. /// </example>
  204. public IWebView WebView {
  205. get {
  206. if (_cachedWebView == null) {
  207. if (_webViewGameObject == null) {
  208. return null;
  209. }
  210. _cachedWebView = _webViewGameObject.GetComponent<IWebView>();
  211. }
  212. return _cachedWebView;
  213. }
  214. private set {
  215. var monoBehaviour = value as MonoBehaviour;
  216. if (monoBehaviour == null) {
  217. throw new ArgumentException("The IWebView cannot be set successfully because it's not a MonoBehaviour.");
  218. }
  219. _webViewGameObject = monoBehaviour.gameObject;
  220. _cachedWebView = value;
  221. }
  222. }
  223. /// <summary>
  224. /// Destroys the instance and its children. Note that you don't have
  225. /// to call this method if you destroy the instance's parent with
  226. /// Object.Destroy().
  227. /// </summary>
  228. /// <example>
  229. /// <code>
  230. /// // The webview can no longer be used after it's destroyed.
  231. /// webViewPrefab.Destroy();
  232. /// </code>
  233. /// </example>
  234. public void Destroy() => Destroy(gameObject);
  235. public void SetCutoutRect(Rect rect) => _view.SetCutoutRect(rect);
  236. /// <summary>
  237. /// Sets options that can be used to alter the webview that the prefab creates
  238. /// during initialization. This method can only be called prior to
  239. /// when the prefab initializes (i.e. directly after instantiating it or setting it to active).
  240. /// </summary>
  241. public void SetOptionsForInitialization(WebViewOptions options) {
  242. if (WebView != null) {
  243. throw new ArgumentException("SetOptionsForInitialization() was called after the prefab was already initialized. Please call it before initialization instead.");
  244. }
  245. _options = options;
  246. }
  247. /// <summary>
  248. /// By default, the prefab detects pointer input events like clicks through
  249. /// Unity's event system, but you can use this method to override the way that
  250. /// input events are detected.
  251. /// </summary>
  252. /// <example>
  253. /// <code>
  254. /// var yourCustomInputDetector = webViewPrefab.Collider.AddComponent&lt;YourCustomInputDetector&gt;();
  255. /// webViewPrefab.SetPointerInputDetector(yourCustomInputDetector);
  256. /// </code>
  257. /// </example>
  258. public void SetPointerInputDetector(IPointerInputDetector pointerInputDetector) {
  259. var previousPointerInputDetector = _pointerInputDetector;
  260. _pointerInputDetector = pointerInputDetector;
  261. // If WebView hasn't been set yet, then _initPointerInputDetector
  262. // will get called before it's set to initialize _pointerInputDetector.
  263. if (WebView != null) {
  264. _initPointerInputDetector(WebView, previousPointerInputDetector);
  265. }
  266. }
  267. /// <summary>
  268. /// By default, the prefab creates a new IWebView during initialization. However,
  269. /// you can call this method before the prefab initializes to pass it an existing,
  270. /// initialized IWebView to use instead. This method can only be called prior to
  271. /// when the prefab initializes (i.e. directly after instantiating it or setting it to active).
  272. /// </summary>
  273. public void SetWebViewForInitialization(IWebView webView) {
  274. if (WebView != null) {
  275. throw new ArgumentException("SetWebViewForInitialization() was called after the prefab was already initialized. Please call it before initialization instead.");
  276. }
  277. if (webView != null && !webView.IsInitialized) {
  278. throw new ArgumentException("SetWebViewForInitialization(IWebView) was called with an uninitialized webview, but an initialized webview is required.");
  279. }
  280. _webViewForInitialization = webView;
  281. }
  282. /// <summary>
  283. /// Returns a task that completes when the prefab is initialized,
  284. /// which means that its WebView property is ready for use.
  285. /// </summary>
  286. /// <example>
  287. /// await webViewPrefab.WaitUntilInitialized();
  288. /// // Now the WebView property is ready.
  289. /// webViewPrefab.WebView.LoadUrl("https://vuplex.com");
  290. /// </example>
  291. public Task WaitUntilInitialized() {
  292. var taskSource = new TaskCompletionSource<bool>();
  293. var isInitialized = WebView != null;
  294. if (isInitialized) {
  295. taskSource.SetResult(true);
  296. } else {
  297. Initialized += (sender, e) => taskSource.SetResult(true);
  298. }
  299. return taskSource.Task;
  300. }
  301. #region Non-public members
  302. float _appliedResolution;
  303. [SerializeField]
  304. [HideInInspector]
  305. ViewportMaterialView _cachedVideoLayer;
  306. [SerializeField]
  307. [HideInInspector]
  308. ViewportMaterialView _cachedView;
  309. IWebView _cachedWebView;
  310. // Used for DragMode.DragToScroll and DragMode.Disabled
  311. bool _clickIsPending;
  312. bool _consoleMessageLoggedHandlerAttached;
  313. bool _dragThresholdReached;
  314. int _heightInPixels { get { return (int)(_sizeInUnityUnits.y * _appliedResolution); }}
  315. bool _loggedDragWarning;
  316. static bool _loggedHoverWarning;
  317. protected WebViewOptions _options;
  318. [SerializeField]
  319. [HideInInspector]
  320. MonoBehaviour _pointerInputDetectorMonoBehaviour;
  321. IPointerInputDetector _pointerInputDetector {
  322. get {
  323. return _pointerInputDetectorMonoBehaviour as IPointerInputDetector;
  324. }
  325. set {
  326. var monoBehaviour = value as MonoBehaviour;
  327. if (monoBehaviour == null) {
  328. throw new ArgumentException("The provided IPointerInputDetector can't be successfully set because it's not a MonoBehaviour");
  329. }
  330. _pointerInputDetectorMonoBehaviour = monoBehaviour;
  331. }
  332. }
  333. bool _pointerIsDown;
  334. Vector2 _pointerDownNormalizedPoint;
  335. Vector2 _previousNormalizedDragPoint;
  336. Vector2 _previousMovePointerPoint;
  337. static bool _remoteDebuggingEnabled;
  338. protected Vector2 _sizeInUnityUnits;
  339. protected ViewportMaterialView _videoLayer {
  340. get {
  341. if (_cachedVideoLayer == null) {
  342. _cachedVideoLayer = _getVideoLayer();
  343. }
  344. return _cachedVideoLayer;
  345. }
  346. }
  347. bool _videoLayerIsEnabled {
  348. get {
  349. return _videoLayer != null && _videoLayer.gameObject.activeSelf;
  350. }
  351. set {
  352. if (_videoLayer != null) {
  353. _videoLayer.gameObject.SetActive(value);
  354. }
  355. }
  356. }
  357. Material _videoMaterial;
  358. protected ViewportMaterialView _view {
  359. get {
  360. if (_cachedView == null) {
  361. _cachedView = _getView();
  362. }
  363. return _cachedView;
  364. }
  365. }
  366. Material _viewMaterial;
  367. protected IWebView _webViewForInitialization;
  368. [SerializeField]
  369. [HideInInspector]
  370. GameObject _webViewGameObject;
  371. int _widthInPixels { get { return (int)(_sizeInUnityUnits.x * _appliedResolution); }}
  372. void _attachWebViewEventHandlers(IWebView webView) {
  373. if (LogConsoleMessages) {
  374. _consoleMessageLoggedHandlerAttached = true;
  375. webView.ConsoleMessageLogged += WebView_ConsoleMessageLogged;
  376. }
  377. // Needed for Vulkan support on Android.
  378. // See the comments in IWithChangingTexture.cs for details.
  379. var webViewWithChangingTexture = webView as IWithChangingTexture;
  380. if (webViewWithChangingTexture != null) {
  381. webViewWithChangingTexture.TextureChanged += WebView_TextureChanged;
  382. }
  383. // Needed for fallback video support on iOS.
  384. var webViewWithFallbackVideo = webView as IWithFallbackVideo;
  385. if (webViewWithFallbackVideo != null && !_options.disableVideo) {
  386. webViewWithFallbackVideo.VideoRectChanged += (sender, eventArgs) => _setVideoRect(eventArgs.Value);
  387. }
  388. }
  389. Vector2Int _convertNormalizedToPixels(Vector2 normalizedPoint) {
  390. return new Vector2Int((int)(normalizedPoint.x * _widthInPixels), (int)(normalizedPoint.y * _heightInPixels));
  391. }
  392. void _disableHoveringIfNeeded(bool preferNative2DMode) {
  393. #if (UNITY_IOS || UNITY_WSA) && !VUPLEX_NO_DISABLING_HOVER_FOR_PERFORMANCE
  394. if (!HoveringEnabled) {
  395. return;
  396. }
  397. if (preferNative2DMode) {
  398. // Hovering isn't detected in Native 2D Mode, so logging a warning is unnecessary.
  399. return;
  400. }
  401. HoveringEnabled = false;
  402. if (!_loggedHoverWarning) {
  403. _loggedHoverWarning = true;
  404. 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 <em>https://support.vuplex.com/articles/hover-and-drag-limitations</em>.");
  405. }
  406. #endif
  407. }
  408. void _enableNativeOnScreenKeyboardIfNeeded(IWebView webView) {
  409. if (webView is IWithNativeOnScreenKeyboard) {
  410. var nativeOnScreenKeyboardEnabled = _getNativeOnScreenKeyboardEnabled();
  411. (webView as IWithNativeOnScreenKeyboard).SetNativeOnScreenKeyboardEnabled(nativeOnScreenKeyboardEnabled);
  412. }
  413. }
  414. void _enableRemoteDebuggingIfNeeded() {
  415. // Remote debugging can only be enabled once, before any webviews are initialized.
  416. if (RemoteDebuggingEnabled && !_remoteDebuggingEnabled) {
  417. _remoteDebuggingEnabled = true;
  418. try {
  419. Web.EnableRemoteDebugging();
  420. } catch (Exception ex) {
  421. 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);
  422. }
  423. }
  424. }
  425. protected abstract float _getResolution();
  426. protected abstract bool _getNativeOnScreenKeyboardEnabled();
  427. protected abstract float _getScrollingSensitivity();
  428. protected abstract ViewportMaterialView _getVideoLayer();
  429. protected abstract ViewportMaterialView _getView();
  430. protected async void _initBase(Rect rect, bool preferNative2DMode = false) {
  431. _throwExceptionIfInitialized();
  432. _sizeInUnityUnits = rect.size;
  433. _updateResolutionIfNeeded();
  434. _disableHoveringIfNeeded(preferNative2DMode);
  435. _enableRemoteDebuggingIfNeeded();
  436. // Note: this.WebView is only set after the webview has been initialized to guarantee
  437. // that the property is ready to use as long as it's not null.
  438. var webView = await _initWebView(rect, preferNative2DMode);
  439. _initViews(webView);
  440. _enableNativeOnScreenKeyboardIfNeeded(webView);
  441. _attachWebViewEventHandlers(webView);
  442. // Init the pointer input detector just before setting WebView so that
  443. // SetPointerInputDetector() will behave correctly if it's called before WebView is set.
  444. if (!_native2DModeEnabled(webView)) {
  445. _initPointerInputDetector(webView);
  446. }
  447. // The webview is now fully initialized, so we can now set WebView and raise the Initialized event.
  448. WebView = webView;
  449. Initialized?.Invoke(this, EventArgs.Empty);
  450. // Lastly, load the InitialUrl.
  451. if (!String.IsNullOrWhiteSpace(InitialUrl)) {
  452. if (_webViewForInitialization != null) {
  453. WebViewLogger.LogWarning("Custom InitialUrl value will be ignored because an initialized webview was provided.");
  454. } else {
  455. webView.LoadUrl(InitialUrl.Trim());
  456. }
  457. }
  458. }
  459. void _initViews(IWebView webView) {
  460. if (_native2DModeEnabled(webView)) {
  461. if (_view != null) {
  462. _view.Visible = false;
  463. _view.gameObject.SetActive(false);
  464. }
  465. _videoLayerIsEnabled = false;
  466. return;
  467. }
  468. // Initialize the main view.
  469. _viewMaterial = webView.CreateMaterial();
  470. _view.Material = _viewMaterial;
  471. // Initialize the video view (iOS only).
  472. var webViewWithFallbackVideo = webView as IWithFallbackVideo;
  473. if (webViewWithFallbackVideo != null && webViewWithFallbackVideo.FallbackVideoEnabled) {
  474. _videoMaterial = webViewWithFallbackVideo.CreateVideoMaterial();
  475. _videoLayer.Material = _videoMaterial;
  476. _setVideoRect(Rect.zero);
  477. } else {
  478. _videoLayerIsEnabled = false;
  479. }
  480. }
  481. async Task<IWebView> _initWebView(Rect rect, bool preferNative2DMode) {
  482. if (_webViewForInitialization != null) {
  483. return _webViewForInitialization;
  484. }
  485. var webView = Web.CreateWebView(_options.preferredPlugins);
  486. // Enable Native 2D Mode if needed.
  487. var enableNative2DMode = preferNative2DMode && webView is IWithNative2DMode;
  488. if (enableNative2DMode) {
  489. var native2DWebView = webView as IWithNative2DMode;
  490. await native2DWebView.InitInNative2DMode(rect);
  491. // Hide the webview if Visible has already been set to false.
  492. native2DWebView.SetVisible(_view.Visible);
  493. return webView;
  494. }
  495. _updatePixelDensityIfNeeded(webView);
  496. // (iOS only) Enable fallback video if needed.
  497. var webViewWithFallbackVideo = webView as IWithFallbackVideo;
  498. if (webViewWithFallbackVideo != null && !_options.disableVideo) {
  499. webViewWithFallbackVideo.SetFallbackVideoEnabled(true);
  500. }
  501. await webView.Init(_widthInPixels, _heightInPixels);
  502. // (Windows and macOS only) Enable cursor icons if needed.
  503. var webViewWithCursorType = webView as IWithCursorType;
  504. if (webViewWithCursorType != null && CursorIconsEnabled && !VXUtils.XRSettings.enabled) {
  505. webViewWithCursorType.CursorTypeChanged += (sender, eventArgs) => {
  506. Internal.CursorHelper.SetCursorIcon(eventArgs.Value);
  507. };
  508. }
  509. return webView;
  510. }
  511. void _initPointerInputDetector(IWebView webView, IPointerInputDetector previousPointerInputDetector = null) {
  512. if (previousPointerInputDetector != null) {
  513. previousPointerInputDetector.BeganDrag -= InputDetector_BeganDrag;
  514. previousPointerInputDetector.Dragged -= InputDetector_Dragged;
  515. previousPointerInputDetector.PointerDown -= InputDetector_PointerDown;
  516. previousPointerInputDetector.PointerExited -= InputDetector_PointerExited;
  517. previousPointerInputDetector.PointerMoved -= InputDetector_PointerMoved;
  518. previousPointerInputDetector.PointerUp -= InputDetector_PointerUp;
  519. previousPointerInputDetector.Scrolled -= InputDetector_Scrolled;
  520. }
  521. if (_pointerInputDetector == null) {
  522. _pointerInputDetector = GetComponentInChildren<IPointerInputDetector>();
  523. }
  524. // Only enable the PointerMoved event if the webview implementation has MovePointer().
  525. _pointerInputDetector.PointerMovedEnabled = (webView as IWithMovablePointer) != null;
  526. _pointerInputDetector.BeganDrag += InputDetector_BeganDrag;
  527. _pointerInputDetector.Dragged += InputDetector_Dragged;
  528. _pointerInputDetector.PointerDown += InputDetector_PointerDown;
  529. _pointerInputDetector.PointerExited += InputDetector_PointerExited;
  530. _pointerInputDetector.PointerMoved += InputDetector_PointerMoved;
  531. _pointerInputDetector.PointerUp += InputDetector_PointerUp;
  532. _pointerInputDetector.Scrolled += InputDetector_Scrolled;
  533. }
  534. void InputDetector_BeganDrag(object sender, EventArgs<Vector2> eventArgs) {
  535. _dragThresholdReached = false;
  536. _previousNormalizedDragPoint = _pointerDownNormalizedPoint;
  537. }
  538. void InputDetector_Dragged(object sender, EventArgs<Vector2> eventArgs) {
  539. if (DragMode == DragMode.Disabled || WebView == null) {
  540. return;
  541. }
  542. var newNormalizedDragPoint = eventArgs.Value;
  543. var previousNormalizedDragPoint = _previousNormalizedDragPoint;
  544. _previousNormalizedDragPoint = newNormalizedDragPoint;
  545. var totalDragDeltaInPixels = _convertNormalizedToPixels(_pointerDownNormalizedPoint - newNormalizedDragPoint);
  546. if (!_dragThresholdReached) {
  547. // _dragThresholdReached needs to be saved, otherwise it could flip from true back
  548. // to false if the user drags back to the original point where the drag started.
  549. _dragThresholdReached = totalDragDeltaInPixels.magnitude > DragThreshold;
  550. }
  551. if (DragMode == DragMode.DragWithinPage) {
  552. if (_dragThresholdReached) {
  553. _movePointerIfNeeded(newNormalizedDragPoint);
  554. }
  555. return;
  556. }
  557. // DragMode is DragToScroll
  558. var normalizedDragDelta = previousNormalizedDragPoint - newNormalizedDragPoint;
  559. _scrollIfNeeded(normalizedDragDelta, _pointerDownNormalizedPoint);
  560. // Check whether to cancel a pending viewport click so that drag-to-scroll
  561. // doesn't unintentionally trigger a click.
  562. if (_clickIsPending) {
  563. if (_dragThresholdReached) {
  564. _clickIsPending = false;
  565. }
  566. }
  567. }
  568. protected virtual void InputDetector_PointerDown(object sender, PointerEventArgs eventArgs) {
  569. _pointerIsDown = true;
  570. _pointerDownNormalizedPoint = eventArgs.Point;
  571. if (!ClickingEnabled || WebView == null) {
  572. return;
  573. }
  574. if (DragMode == DragMode.DragWithinPage) {
  575. var webViewWithPointerDown = WebView as IWithPointerDownAndUp;
  576. if (webViewWithPointerDown != null) {
  577. webViewWithPointerDown.PointerDown(eventArgs.Point, eventArgs.ToPointerOptions());
  578. return;
  579. } else if (!_loggedDragWarning) {
  580. _loggedDragWarning = true;
  581. 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 <em>https://developer.vuplex.com/webview/IWithPointerDownAndUp</em>.");
  582. // Fallback to setting _clickIsPending so Click() can be called.
  583. }
  584. }
  585. // Defer calling PointerDown() for DragToScroll so that the click can
  586. // be cancelled if drag exceeds the threshold needed to become a scroll.
  587. _clickIsPending = true;
  588. }
  589. void InputDetector_PointerExited(object sender, EventArgs eventArgs) {
  590. if (HoveringEnabled) {
  591. // Remove the hover state when the pointer exits.
  592. _movePointerIfNeeded(Vector2.zero);
  593. }
  594. }
  595. void InputDetector_PointerMoved(object sender, EventArgs<Vector2> eventArgs) {
  596. // InputDetector_Dragged handles calling MovePointer while dragging.
  597. if (_pointerIsDown || !HoveringEnabled) {
  598. return;
  599. }
  600. _movePointerIfNeeded(eventArgs.Value);
  601. }
  602. protected virtual void InputDetector_PointerUp(object sender, PointerEventArgs eventArgs) {
  603. _pointerIsDown = false;
  604. if (!ClickingEnabled || WebView == null) {
  605. return;
  606. }
  607. var webViewWithPointerDownAndUp = WebView as IWithPointerDownAndUp;
  608. if (DragMode == DragMode.DragWithinPage && webViewWithPointerDownAndUp != null) {
  609. var totalDragDeltaInPixels = _convertNormalizedToPixels(_pointerDownNormalizedPoint - eventArgs.Point);
  610. var dragThresholdReached = totalDragDeltaInPixels.magnitude > DragThreshold;
  611. var pointerUpPoint = dragThresholdReached ? eventArgs.Point : _pointerDownNormalizedPoint;
  612. webViewWithPointerDownAndUp.PointerUp(pointerUpPoint, eventArgs.ToPointerOptions());
  613. } else {
  614. if (!_clickIsPending) {
  615. return;
  616. }
  617. _clickIsPending = false;
  618. // PointerDown() and PointerUp() don't support the preventStealingFocus parameter.
  619. if (webViewWithPointerDownAndUp == null || _options.clickWithoutStealingFocus) {
  620. WebView.Click(eventArgs.Point, _options.clickWithoutStealingFocus);
  621. } else {
  622. var pointerOptions = eventArgs.ToPointerOptions();
  623. webViewWithPointerDownAndUp.PointerDown(eventArgs.Point, pointerOptions);
  624. webViewWithPointerDownAndUp.PointerUp(eventArgs.Point, pointerOptions);
  625. }
  626. }
  627. Clicked?.Invoke(this, new ClickedEventArgs(eventArgs.Point));
  628. }
  629. void InputDetector_Scrolled(object sender, ScrolledEventArgs eventArgs) {
  630. var sensitivity = _getScrollingSensitivity();
  631. // The ScrollingSensivity is measured in Unity units because the argument
  632. // passed to Scroll(Vector2) was originally in Unity units (but is now a normalized value).
  633. var scaledScrollDeltaInUnityUnits = eventArgs.ScrollDelta * sensitivity;
  634. var normalizedScrollDelta = new Vector2(scaledScrollDeltaInUnityUnits.x / _sizeInUnityUnits.x, scaledScrollDeltaInUnityUnits.y / _sizeInUnityUnits.y);
  635. _scrollIfNeeded(normalizedScrollDelta, eventArgs.Point);
  636. }
  637. void _movePointerIfNeeded(Vector2 point) {
  638. var webViewWithMovablePointer = WebView as IWithMovablePointer;
  639. if (webViewWithMovablePointer == null) {
  640. return;
  641. }
  642. if (point != _previousMovePointerPoint) {
  643. _previousMovePointerPoint = point;
  644. webViewWithMovablePointer.MovePointer(point);
  645. }
  646. }
  647. bool _native2DModeEnabled(IWebView webView) => webView is IWithNative2DMode && (webView as IWithNative2DMode).Native2DModeEnabled;
  648. protected virtual void OnDestroy() {
  649. if (WebView != null && !WebView.IsDisposed) {
  650. WebView.Dispose();
  651. }
  652. Destroy();
  653. // Unity doesn't automatically destroy materials and textures
  654. // when the GameObject is destroyed.
  655. if (_viewMaterial != null) {
  656. Destroy(_viewMaterial.mainTexture);
  657. Destroy(_viewMaterial);
  658. }
  659. if (_videoMaterial != null) {
  660. Destroy(_videoMaterial.mainTexture);
  661. Destroy(_videoMaterial);
  662. }
  663. }
  664. protected void _resizeWebViewIfNeeded() {
  665. if (WebView != null && WebView.Size != new Vector2(_widthInPixels, _heightInPixels)) {
  666. WebView.Resize(_widthInPixels, _heightInPixels);
  667. }
  668. }
  669. void _scrollIfNeeded(Vector2 scrollDelta, Vector2 point) {
  670. // scrollDelta can be zero when the user drags the cursor off the screen.
  671. if (!ScrollingEnabled || WebView == null || scrollDelta == Vector2.zero) {
  672. return;
  673. }
  674. WebView.Scroll(scrollDelta, point);
  675. Scrolled?.Invoke(this, new ScrolledEventArgs(scrollDelta, point));
  676. }
  677. protected abstract void _setVideoLayerPosition(Rect videoRect);
  678. void _setVideoRect(Rect videoRect) {
  679. if (_videoLayer == null) {
  680. return;
  681. }
  682. _view.SetCutoutRect(videoRect);
  683. _setVideoLayerPosition(videoRect);
  684. // This code applies a cropping rect to the video layer's shader based on what part of the video (if any)
  685. // falls outside of the viewport and therefore needs to be hidden. Note that the dimensions here are divided
  686. // by the videoRect's width or height, because in the videoLayer shader, the width of the videoRect is 1
  687. // and the height is 1 (i.e. the dimensions are normalized).
  688. float videoRectXMin = Math.Max(0, - 1 * videoRect.x / videoRect.width);
  689. float videoRectYMin = Math.Max(0, -1 * videoRect.y / videoRect.height);
  690. float videoRectXMax = Math.Min(1, (1 - videoRect.x) / videoRect.width);
  691. float videoRectYMax = Math.Min(1, (1 - videoRect.y) / videoRect.height);
  692. var videoCropRect = Rect.MinMaxRect(videoRectXMin, videoRectYMin, videoRectXMax, videoRectYMax);
  693. if (videoCropRect == new Rect(0, 0, 1, 1)) {
  694. // The entire video rect fits within the viewport, so set the cropt rect to zero to disable it.
  695. videoCropRect = Rect.zero;
  696. }
  697. _videoLayer.SetCropRect(videoCropRect);
  698. }
  699. void _throwExceptionIfInitialized() {
  700. if (WebView != null) {
  701. throw new InvalidOperationException("Init() cannot be called on a WebViewPrefab that has already been initialized.");
  702. }
  703. }
  704. protected virtual void Update() {
  705. _updateResolutionIfNeeded();
  706. _updatePixelDensityIfNeeded(WebView);
  707. // Check if LogConsoleMessages is changed from false to true at runtime.
  708. if (LogConsoleMessages && !_consoleMessageLoggedHandlerAttached && WebView != null) {
  709. _consoleMessageLoggedHandlerAttached = true;
  710. WebView.ConsoleMessageLogged += WebView_ConsoleMessageLogged;
  711. }
  712. }
  713. void _updatePixelDensityIfNeeded(IWebView webView) {
  714. var webViewWithPixelDensity = webView as IWithPixelDensity;
  715. if (webViewWithPixelDensity == null || PixelDensity == webViewWithPixelDensity.PixelDensity) {
  716. return;
  717. }
  718. try {
  719. webViewWithPixelDensity.SetPixelDensity(PixelDensity);
  720. } catch (ArgumentException ex) {
  721. WebViewLogger.LogError(ex.ToString());
  722. PixelDensity = 1;
  723. }
  724. }
  725. void _updateResolutionIfNeeded() {
  726. var resolution = _getResolution();
  727. if (_appliedResolution != resolution) {
  728. if (resolution > 0.0f) {
  729. _appliedResolution = resolution;
  730. _resizeWebViewIfNeeded();
  731. } else {
  732. WebViewLogger.LogWarning("Ignoring invalid Resolution: " + resolution);
  733. }
  734. }
  735. }
  736. void WebView_ConsoleMessageLogged(object sender, ConsoleMessageEventArgs eventArgs) {
  737. if (!LogConsoleMessages) {
  738. return;
  739. }
  740. var message = "[Web Console] " + eventArgs.Message;
  741. if (eventArgs.Source != null) {
  742. message += $" ({eventArgs.Source}:{eventArgs.Line})";
  743. }
  744. switch (eventArgs.Level) {
  745. case ConsoleMessageLevel.Error:
  746. WebViewLogger.LogError(message, false);
  747. break;
  748. case ConsoleMessageLevel.Warning:
  749. WebViewLogger.LogWarning(message, false);
  750. break;
  751. default:
  752. WebViewLogger.Log(message, false);
  753. break;
  754. }
  755. }
  756. void WebView_TextureChanged(object sender, EventArgs<Texture2D> eventArgs) {
  757. var oldTexture = _view.Texture;
  758. if (oldTexture is RenderTexture) {
  759. // The application replaced WebViewPrefab.Material.mainTexture with a RenderTexture
  760. // (for example, to implement MipMaps), so don't change the prefab's texture.
  761. return;
  762. }
  763. _view.Texture = eventArgs.Value;
  764. Destroy(oldTexture);
  765. }
  766. #endregion
  767. // Added in v3.5, removed in v3.7.
  768. [Obsolete("The WebViewPrefab.DragToScrollThreshold property has been removed. Please use DragThreshold instead: https://developer.vuplex.com/webview/WebViewPrefab#DragThreshold", true)]
  769. public float DragToScrollThreshold { get; set; }
  770. }
  771. }