BaseWebViewPrefab.cs 48 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053
  1. // Copyright (c) 2024 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 UnityEngine.XR;
  19. using Vuplex.WebView.Internal;
  20. namespace Vuplex.WebView {
  21. public abstract class BaseWebViewPrefab : MonoBehaviour {
  22. /// <summary>
  23. /// Indicates that the prefab was clicked. Note that the prefab automatically
  24. /// calls IWebView.Click() for you.
  25. /// </summary>
  26. /// <remarks>
  27. /// This event is not supported when running in [Native 2D Mode](https://support.vuplex.com/articles/native-2d-mode).
  28. /// </remarks>
  29. /// <example>
  30. /// <code>
  31. /// webViewPrefab.Clicked += (sender, eventArgs) => {
  32. /// Debug.Log("WebViewPrefab was clicked at point: " + eventArgs.Point);
  33. /// };
  34. /// </code>
  35. /// </example>
  36. public virtual event EventHandler<ClickedEventArgs> Clicked;
  37. /// <summary>
  38. /// Indicates that the prefab finished initializing,
  39. /// so its WebView property is ready to use.
  40. /// </summary>
  41. /// <seealso cref="WaitUntilInitialized"/>
  42. public event EventHandler Initialized;
  43. /// <summary>
  44. /// Indicates that the pointer (e.g. mouse cursor) entered the prefab.
  45. /// </summary>
  46. public event EventHandler PointerEntered;
  47. /// <summary>
  48. /// Indicates that the pointer (e.g. mouse cursor) exited the prefab.
  49. /// </summary>
  50. public event EventHandler PointerExited;
  51. /// <summary>
  52. /// Indicates that the prefab was scrolled. Note that the prefab automatically
  53. /// calls IWebView.Scroll() for you.
  54. /// </summary>
  55. /// <remarks>
  56. /// This event is not supported when running in [Native 2D Mode](https://support.vuplex.com/articles/native-2d-mode).
  57. /// </remarks>
  58. /// <example>
  59. /// webViewPrefab.Scrolled += (sender, eventArgs) => {
  60. /// Debug.Log($"WebViewPrefab was scrolled. Point: {eventArgs.Point}, scroll delta: {eventArgs.ScrollDelta}");
  61. /// };
  62. /// </example>
  63. public virtual event EventHandler<ScrolledEventArgs> Scrolled;
  64. /// <summary>
  65. /// Determines whether clicking is enabled. The default is `true`.
  66. /// </summary>
  67. /// <remarks>
  68. /// This property is ignored when running in [Native 2D Mode](https://support.vuplex.com/articles/native-2d-mode).
  69. /// </remarks>
  70. public bool ClickingEnabled = true;
  71. /// <summary>
  72. /// Determines whether the mouse cursor icon is automatically updated based on interaction with
  73. /// the web page. For example, hovering over a link causes the mouse cursor icon to turn into a pointer hand.
  74. /// The default is `true`. CursorIconsEnabled is currently only supported by 3D WebView for Windows and macOS.
  75. /// </summary>
  76. /// <seealso cref="IWithCursorType"/>
  77. [Label("Cursor Icons Enabled (Windows and macOS only)")]
  78. [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.")]
  79. public bool CursorIconsEnabled = true;
  80. /// <summary>
  81. /// Determines how the prefab handles drag interactions.
  82. /// </summary>
  83. /// <remarks>
  84. /// Important notes:
  85. /// <list type="bullet">
  86. /// <item>This property is ignored when running in [Native 2D Mode](https://support.vuplex.com/articles/native-2d-mode).</item>
  87. /// <item>
  88. /// For information on the limitations of drag interactions on iOS and UWP, please see
  89. /// [this article](https://support.vuplex.com/articles/hover-and-drag-limitations).
  90. /// </item>
  91. /// <item>
  92. /// The Android Gecko package doesn't support the HTML Drag and Drop API (GeckoView limitation).
  93. /// </item>
  94. /// </list>
  95. /// </remarks>
  96. /// <seealso href="https://support.vuplex.com/articles/dragging-scrollbar">When I drag a scrollbar, why does it scroll the wrong way?</seealso>
  97. [Tooltip("Determines how the prefab handles drag interactions. Note that This property is ignored when running in Native 2D Mode.")]
  98. public DragMode DragMode = DragMode.DragToScroll;
  99. /// <summary>
  100. /// Determines the threshold (in web pixels) for triggering a drag. The default is `20`.
  101. /// </summary>
  102. /// <remarks>
  103. /// <list type="bullet">
  104. /// <item>
  105. /// When the prefab's DragMode is set to DragToScroll, this property determines
  106. /// the distance that the pointer must drag before it's no longer
  107. /// considered a click.
  108. /// </item>
  109. /// <item>
  110. /// When the prefab's DragMode is set to DragWithinPage, this property determines
  111. /// the distance that the pointer must drag before it triggers
  112. /// a drag within the page.
  113. /// </item>
  114. /// <item>This property is ignored when running in [Native 2D Mode](https://support.vuplex.com/articles/native-2d-mode).</item>
  115. /// </list>
  116. /// </remarks>
  117. [Label("Drag Threshold (px)")]
  118. [Tooltip("Determines the threshold (in web pixels) for triggering a drag.")]
  119. public float DragThreshold = 20;
  120. /// <summary>
  121. /// Determines whether hover interactions are enabled. The default is `true`.
  122. /// </summary>
  123. /// <remarks>
  124. /// Important notes:
  125. /// <list type="bullet">
  126. /// <item>This property is ignored when running in [Native 2D Mode](https://support.vuplex.com/articles/native-2d-mode).</item>
  127. /// <item>
  128. /// For information on the limitations of hovering on iOS and UWP, please see
  129. /// [this article](https://support.vuplex.com/articles/hover-and-drag-limitations).
  130. /// </item>
  131. /// </list>
  132. /// </remarks>
  133. public bool HoveringEnabled = true;
  134. /// <summary>
  135. /// If you drag the prefab into the scene via the editor,
  136. /// you can set this property to make it so that the instance
  137. /// automatically loads the given URL after it initializes. To load a new URL
  138. /// at runtime, use IWebView.LoadUrl() instead.
  139. /// </summary>
  140. /// <seealso href="https://support.vuplex.com/articles/how-to-load-local-files">How to load local files</seealso>
  141. [Label("Initial URL (optional)")]
  142. [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().")]
  143. [HideInInspector]
  144. public string InitialUrl;
  145. /// <summary>
  146. /// Determines whether the webview automatically receives keyboard input from the native keyboard and the Keyboard prefab. The default is `true`.
  147. /// </summary>
  148. /// <seealso cref="NativeOnScreenKeyboardEnabled">NativeOnScreenKeyboardEnabled</seealso>
  149. /// <seealso href="https://support.vuplex.com/articles/keyboard">How does keyboard input work?</seealso>
  150. [Tooltip("Determines whether the webview automatically receives keyboard input from the native keyboard and the Keyboard prefab.")]
  151. public bool KeyboardEnabled = true;
  152. /// <summary>
  153. /// Determines whether JavaScript console messages from IWebView.ConsoleMessageLogged
  154. /// are printed to the Unity logs. The default is `false`.
  155. /// </summary>
  156. [Tooltip("Determines whether JavaScript console messages are printed to the Unity logs.")]
  157. public bool LogConsoleMessages = false;
  158. /// <summary>
  159. /// Gets or sets prefab's material.
  160. /// </summary>
  161. /// <remarks>
  162. /// This property is unused when running in [Native 2D Mode](https://support.vuplex.com/articles/native-2d-mode).
  163. /// </remarks>
  164. public Material Material {
  165. get => _view.Material;
  166. set => _view.Material = value;
  167. }
  168. /// <summary>
  169. /// Sets the webview's pixel density, which is its number of physical pixels per logical pixel.
  170. /// The default value is `1`, but increasing it to `2` can make web content appear sharper
  171. /// or less blurry on high DPI displays. PixelDensity is currently only supported by
  172. /// 3D WebView for Windows and macOS.
  173. /// </summary>
  174. /// <example>
  175. /// <code>
  176. /// // Increase the pixel density to 2 for high DPI screens.
  177. /// webViewPrefab.PixelDensity = 2;
  178. /// </code>
  179. /// </example>
  180. /// <seealso cref="IWithPixelDensity"/>
  181. [Label("Pixel Density (Windows and macOS only)")]
  182. [Tooltip("(Windows and macOS only) Sets the webview's pixel density.")]
  183. public float PixelDensity = 1;
  184. /// <summary>
  185. /// Determines whether the prefab enables remote debugging by calling Web.EnableRemoteDebugging().
  186. /// The default is `false`.
  187. /// </summary>
  188. /// <seealso href="https://support.vuplex.com/articles/how-to-debug-web-content"/>
  189. [Header("Debugging")]
  190. [Tooltip("Determines whether remote debugging is enabled.")]
  191. public bool RemoteDebuggingEnabled = false;
  192. /// <summary>
  193. /// Determines whether scrolling is enabled. The default is `true`.
  194. /// </summary>
  195. /// <remarks>
  196. /// This property is ignored when running in [Native 2D Mode](https://support.vuplex.com/articles/native-2d-mode).
  197. /// </remarks>
  198. public bool ScrollingEnabled = true;
  199. /// <summary>
  200. /// Gets or sets whether the instance is visible and responds to raycasts. The default is `true`.
  201. /// If you want to hide the webview, setting this property to false is often preferable to disabling
  202. /// the prefab's GameObject, because the latter stops the prefab's scripts from running, which may
  203. /// prevent it from initializating, for example.
  204. /// </summary>
  205. public virtual bool Visible {
  206. get => _visible;
  207. set {
  208. _visible = value;
  209. _view.gameObject.SetActive(value);
  210. if (_videoLayerIsEnabled) {
  211. _videoLayer.gameObject.SetActive(value);
  212. }
  213. }
  214. }
  215. /// <summary>
  216. /// Returns the prefab's IWebView instance, or `null` if the prefab hasn't finished
  217. /// initializing yet. To detect when the WebView property is no longer null,
  218. /// please use WaitUntilInitialized().
  219. /// </summary>
  220. /// <example>
  221. /// <code>
  222. /// await webViewPrefab.WaitUntilInitialized();
  223. /// // Now the WebView property is ready.
  224. /// webViewPrefab.WebView.LoadUrl("https://vuplex.com");
  225. /// </code>
  226. /// </example>
  227. public IWebView WebView {
  228. get {
  229. if (_cachedWebView == null) {
  230. if (_webViewGameObject == null) {
  231. return null;
  232. }
  233. _cachedWebView = _webViewGameObject.GetComponent<IWebView>();
  234. }
  235. return _cachedWebView;
  236. }
  237. private set {
  238. var monoBehaviour = value as MonoBehaviour;
  239. if (monoBehaviour == null) {
  240. throw new ArgumentException("The IWebView cannot be set successfully because it's not a MonoBehaviour.");
  241. }
  242. _webViewGameObject = monoBehaviour.gameObject;
  243. _cachedWebView = value;
  244. }
  245. }
  246. /// <summary>
  247. /// Converts the given point from web browser viewport coordinates to the
  248. /// corresponding Unity screen space coordinates of where the point is rendered
  249. /// in the application window. Note that web browser coordinates (the input parameters)
  250. /// treat the top left corner of the browser viewport as the (0, 0) origin. In contrast,
  251. /// Unity's screen space coordinates (the return value) treat the bottom left corner of the application's
  252. /// window as the (0, 0) origin.
  253. /// </summary>
  254. /// <example>
  255. /// <code>
  256. /// var screenPoint = webViewPrefab.BrowserToSreenPoint(200, 300);
  257. /// Debug.Log($"Point (200px, 300px) within the browser window is rendered at point {screenPoint} on the device screen.");
  258. /// </code>
  259. /// </example>
  260. public abstract Vector2 BrowserToScreenPoint(int xInPixels, int yInPixels);
  261. /// <summary>
  262. /// Destroys the instance and its children. Note that you don't have
  263. /// to call this method if you destroy the instance's parent with
  264. /// Object.Destroy().
  265. /// </summary>
  266. /// <example>
  267. /// <code>
  268. /// // The webview can no longer be used after it's destroyed.
  269. /// webViewPrefab.Destroy();
  270. /// </code>
  271. /// </example>
  272. public void Destroy() => Destroy(gameObject);
  273. public void SetCutoutRect(Rect rect) => _view.SetCutoutRect(rect);
  274. /// <summary>
  275. /// Sets options that can be used to alter the webview that the prefab creates
  276. /// during initialization. This method can only be called prior to
  277. /// when the prefab initializes (i.e. directly after instantiating it or setting it to active).
  278. /// </summary>
  279. /// <example>
  280. /// <code>
  281. /// using System;
  282. /// using UnityEngine;
  283. /// using Vuplex.WebView;
  284. ///
  285. /// class SetOptions : MonoBehaviour {
  286. ///
  287. /// // IMPORTANT: With this approach, you must set your WebViewPrefab or CanvasWebViewPrefab to inactive
  288. /// // in the scene hierarchy so that this script can manually activate it. Otherwise, this
  289. /// // script won't be able to call SetOptionsForInitialization() before the prefab initializes.
  290. /// //
  291. /// // TODO: Set this webViewPrefab field to the inactive WebViewPrefab or CanvasWebViewPrefab in your scene
  292. /// // via the Editor's Inspector tab.
  293. /// public BaseWebViewPrefab webViewPrefab;
  294. ///
  295. /// void Awake() {
  296. ///
  297. /// if (webViewPrefab.gameObject.activeInHierarchy) {
  298. /// 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.");
  299. /// }
  300. /// webViewPrefab.gameObject.SetActive(true);
  301. ///
  302. /// webViewPrefab.SetOptionsForInitialization(new WebViewOptions {
  303. /// preferredPlugins = new WebPluginType[] { WebPluginType.Android }
  304. /// });
  305. /// }
  306. /// }
  307. /// </code>
  308. /// </example>
  309. public void SetOptionsForInitialization(WebViewOptions options) {
  310. if (WebView != null) {
  311. throw new ArgumentException("SetOptionsForInitialization() was called after the prefab was already initialized. Please call it before initialization instead.");
  312. }
  313. _options = options;
  314. }
  315. /// <summary>
  316. /// By default, the prefab detects pointer input events like clicks through
  317. /// Unity's event system, but you can use this method to override the way that
  318. /// input events are detected.
  319. /// </summary>
  320. /// <example>
  321. /// <code>
  322. /// var yourCustomInputDetector = webViewPrefab.Collider.AddComponent&lt;YourCustomInputDetector&gt;();
  323. /// webViewPrefab.SetPointerInputDetector(yourCustomInputDetector);
  324. /// </code>
  325. /// </example>
  326. public void SetPointerInputDetector(IPointerInputDetector pointerInputDetector) {
  327. var previousPointerInputDetector = _pointerInputDetector;
  328. _pointerInputDetector = pointerInputDetector;
  329. // If WebView hasn't been set yet, then _initPointerInputDetector
  330. // will get called before it's set to initialize _pointerInputDetector.
  331. if (WebView != null) {
  332. _attachOrDetachPointerInputDetector(previousPointerInputDetector, false);
  333. _initPointerInputDetector(WebView);
  334. }
  335. }
  336. /// <summary>
  337. /// By default, the prefab creates a new IWebView during initialization. However,
  338. /// you can call this method before the prefab initializes to pass it an existing,
  339. /// initialized IWebView to use instead. This method can only be called prior to
  340. /// when the prefab initializes (i.e. directly after instantiating it or setting it to active).
  341. /// </summary>
  342. public void SetWebViewForInitialization(IWebView webView) {
  343. if (WebView != null) {
  344. throw new ArgumentException("SetWebViewForInitialization() was called after the prefab was already initialized. Please call it before initialization instead.");
  345. }
  346. if (webView != null && !webView.IsInitialized) {
  347. throw new ArgumentException("SetWebViewForInitialization(IWebView) was called with an uninitialized webview, but an initialized webview is required.");
  348. }
  349. _webViewForInitialization = webView;
  350. }
  351. /// <summary>
  352. /// Returns a task that completes when the prefab is initialized,
  353. /// which means that its WebView property is ready for use.
  354. /// </summary>
  355. /// <example>
  356. /// await webViewPrefab.WaitUntilInitialized();
  357. /// // Now the WebView property is ready.
  358. /// webViewPrefab.WebView.LoadUrl("https://vuplex.com");
  359. /// </example>
  360. public Task WaitUntilInitialized() {
  361. var taskSource = new TaskCompletionSource<bool>();
  362. var isInitialized = WebView != null;
  363. if (isInitialized) {
  364. taskSource.SetResult(true);
  365. } else {
  366. Initialized += (sender, e) => taskSource.SetResult(true);
  367. }
  368. return taskSource.Task;
  369. }
  370. #region Non-public members
  371. float _appliedResolution;
  372. [SerializeField]
  373. [HideInInspector]
  374. ViewportMaterialView _cachedVideoLayer;
  375. [SerializeField]
  376. [HideInInspector]
  377. ViewportMaterialView _cachedView;
  378. IWebView _cachedWebView;
  379. bool _consoleMessageLoggedHandlerAttached;
  380. bool _dragThresholdReached;
  381. bool _dragToScrollClickIsPending;
  382. bool _hasOverriddenCursorIcon;
  383. int _heightInPixels { get => (int)(_sizeInUnityUnits.y * _appliedResolution); }
  384. bool _keyboardHasBeenEnabled;
  385. bool _loggedDragWarning;
  386. static bool _loggedHoverWarning;
  387. protected WebViewOptions _options;
  388. [SerializeField]
  389. [HideInInspector]
  390. MonoBehaviour _pointerInputDetectorMonoBehaviour;
  391. IPointerInputDetector _pointerInputDetector {
  392. get => _pointerInputDetectorMonoBehaviour as IPointerInputDetector;
  393. set {
  394. var monoBehaviour = value as MonoBehaviour;
  395. if (monoBehaviour == null) {
  396. throw new ArgumentException("The provided IPointerInputDetector can't be successfully set because it's not a MonoBehaviour");
  397. }
  398. _pointerInputDetectorMonoBehaviour = monoBehaviour;
  399. }
  400. }
  401. bool _pointerIsDown;
  402. Vector2 _pointerDownNormalizedPoint;
  403. Vector2 _previousNormalizedDragPoint;
  404. Vector2 _previousMovePointerPoint;
  405. static bool _remoteDebuggingEnabled;
  406. protected Vector2 _sizeInUnityUnits;
  407. protected ViewportMaterialView _videoLayer {
  408. get {
  409. if (_cachedVideoLayer == null) {
  410. _cachedVideoLayer = _getVideoLayer();
  411. }
  412. return _cachedVideoLayer;
  413. }
  414. }
  415. bool _videoLayerIsEnabled {
  416. get => _videoLayer != null && _videoLayer.gameObject.activeSelf;
  417. set {
  418. if (_videoLayer != null) {
  419. _videoLayer.gameObject.SetActive(value);
  420. }
  421. }
  422. }
  423. Material _videoMaterial;
  424. protected ViewportMaterialView _view {
  425. get {
  426. if (_cachedView == null) {
  427. _cachedView = _getView();
  428. }
  429. return _cachedView;
  430. }
  431. }
  432. Material _viewMaterial;
  433. bool _visible = true;
  434. protected IWebView _webViewForInitialization;
  435. [SerializeField]
  436. [HideInInspector]
  437. GameObject _webViewGameObject;
  438. int _widthInPixels { get => (int)(_sizeInUnityUnits.x * _appliedResolution); }
  439. void _attachOrDetachPointerInputDetector(IPointerInputDetector detector, bool attach) {
  440. if (attach) {
  441. detector.BeganDrag += InputDetector_BeganDrag;
  442. detector.Dragged += InputDetector_Dragged;
  443. detector.PointerDown += InputDetector_PointerDown;
  444. detector.PointerEntered += InputDetector_PointerEntered;
  445. detector.PointerExited += InputDetector_PointerExited;
  446. detector.PointerMoved += InputDetector_PointerMoved;
  447. detector.PointerUp += InputDetector_PointerUp;
  448. detector.Scrolled += InputDetector_Scrolled;
  449. } else {
  450. detector.BeganDrag -= InputDetector_BeganDrag;
  451. detector.Dragged -= InputDetector_Dragged;
  452. detector.PointerDown -= InputDetector_PointerDown;
  453. detector.PointerEntered -= InputDetector_PointerEntered;
  454. detector.PointerExited -= InputDetector_PointerExited;
  455. detector.PointerMoved -= InputDetector_PointerMoved;
  456. detector.PointerUp -= InputDetector_PointerUp;
  457. detector.Scrolled -= InputDetector_Scrolled;
  458. }
  459. }
  460. void _attachWebViewEventHandlers(IWebView webView) {
  461. _enableConsoleMessagesIfNeeded(webView);
  462. // Needed for Vulkan support on Android.
  463. // See the comments in IWithChangingTexture.cs for details.
  464. var webViewWithChangingTexture = webView as IWithChangingTexture;
  465. if (webViewWithChangingTexture != null) {
  466. webViewWithChangingTexture.TextureChanged += WebView_TextureChanged;
  467. }
  468. // Needed for fallback video support on iOS.
  469. var webViewWithFallbackVideo = webView as IWithFallbackVideo;
  470. if (webViewWithFallbackVideo != null && !_options.disableVideo) {
  471. webViewWithFallbackVideo.VideoRectChanged += (sender, eventArgs) => _setVideoRect(eventArgs.Value);
  472. }
  473. }
  474. void _disableHoveringIfNeeded(bool preferNative2DMode) {
  475. #if (UNITY_IOS || UNITY_WSA) && !VUPLEX_NO_DISABLING_HOVER_FOR_PERFORMANCE
  476. if (!HoveringEnabled) {
  477. return;
  478. }
  479. if (preferNative2DMode) {
  480. // Hovering isn't detected in Native 2D Mode, so logging a warning is unnecessary.
  481. return;
  482. }
  483. HoveringEnabled = false;
  484. if (!_loggedHoverWarning) {
  485. _loggedHoverWarning = true;
  486. 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>.");
  487. }
  488. #endif
  489. }
  490. void _enableConsoleMessagesIfNeeded(IWebView webView) {
  491. if (LogConsoleMessages && !_consoleMessageLoggedHandlerAttached && webView != null) {
  492. _consoleMessageLoggedHandlerAttached = true;
  493. webView.ConsoleMessageLogged += WebView_ConsoleMessageLogged;
  494. }
  495. }
  496. void _enableNativeOnScreenKeyboardIfNeeded(IWebView webView) {
  497. if (webView is IWithNativeOnScreenKeyboard) {
  498. var nativeOnScreenKeyboardEnabled = _getNativeOnScreenKeyboardEnabled();
  499. (webView as IWithNativeOnScreenKeyboard).SetNativeOnScreenKeyboardEnabled(nativeOnScreenKeyboardEnabled);
  500. }
  501. }
  502. void _enableOrDisableKeyboardIfNeeded() {
  503. if (WebView != null && _keyboardHasBeenEnabled != KeyboardEnabled) {
  504. Internal.KeyboardManager.Instance.SetKeyboardEnabled(this, KeyboardEnabled);
  505. _keyboardHasBeenEnabled = KeyboardEnabled;
  506. }
  507. }
  508. void _enableRemoteDebuggingIfNeeded() {
  509. // Remote debugging can only be enabled once, before any webviews are initialized.
  510. if (RemoteDebuggingEnabled && !_remoteDebuggingEnabled) {
  511. _remoteDebuggingEnabled = true;
  512. try {
  513. Web.EnableRemoteDebugging();
  514. } catch (Exception ex) {
  515. 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);
  516. }
  517. }
  518. }
  519. protected abstract float _getResolution();
  520. protected abstract bool _getNativeOnScreenKeyboardEnabled();
  521. protected abstract float _getScrollingSensitivity();
  522. IWithTouch _getTouchIfSupported() {
  523. var webViewWithTouch = WebView as IWithTouch;
  524. // Touch is currently disabled for the Android Gecko package because it currently has an
  525. // issue where scrolling doesn't work correctly with touch.
  526. if (webViewWithTouch != null && WebView.PluginType != WebPluginType.AndroidGecko) {
  527. return webViewWithTouch;
  528. }
  529. return null;
  530. }
  531. protected abstract ViewportMaterialView _getVideoLayer();
  532. protected abstract ViewportMaterialView _getView();
  533. void _handleTrialExpired() {
  534. _view.Material = new Material(Resources.Load<Material>("TrialExpiredMaterial"));
  535. _videoLayer.gameObject.SetActive(false);
  536. }
  537. protected async Task _initBase(Rect rect, bool preferNative2DMode = false) {
  538. _throwExceptionIfInitialized();
  539. _sizeInUnityUnits = rect.size;
  540. _updateResolutionIfNeeded();
  541. _disableHoveringIfNeeded(preferNative2DMode);
  542. _enableRemoteDebuggingIfNeeded();
  543. // Note: this.WebView is only set after the webview has been initialized to guarantee
  544. // that the property is ready to use as long as it's not null.
  545. var webView = await _initWebView(rect, preferNative2DMode);
  546. if (this == null) {
  547. // This prefab was destroyed while waiting for the webview to initialize.
  548. webView.Dispose();
  549. return;
  550. }
  551. _initViews(webView);
  552. _enableNativeOnScreenKeyboardIfNeeded(webView);
  553. _attachWebViewEventHandlers(webView);
  554. // Init the pointer input detector just before setting WebView so that
  555. // SetPointerInputDetector() will behave correctly if it's called before WebView is set.
  556. if (!_native2DModeEnabled(webView)) {
  557. _initPointerInputDetector(webView);
  558. }
  559. // The webview is now fully initialized, so we can now set WebView and raise the Initialized event.
  560. WebView = webView;
  561. // Intentionally call KeyboardManager.SetKeyboardEnabled() before raising the Initialized event so that it can attach a listener
  562. // to the IWebView.FocusChanged event before the application is able to call IWebView.SetFocused().
  563. _enableOrDisableKeyboardIfNeeded();
  564. Initialized?.Invoke(this, EventArgs.Empty);
  565. // Lastly, load the InitialUrl.
  566. if (!String.IsNullOrWhiteSpace(InitialUrl)) {
  567. if (_webViewForInitialization != null) {
  568. WebViewLogger.LogWarning("Custom InitialUrl value will be ignored because an initialized webview was provided.");
  569. } else {
  570. webView.LoadUrl(InitialUrl.Trim());
  571. }
  572. }
  573. }
  574. void _initViews(IWebView webView) {
  575. if (_native2DModeEnabled(webView)) {
  576. if (_view != null) {
  577. _view.gameObject.SetActive(false);
  578. }
  579. _videoLayerIsEnabled = false;
  580. return;
  581. }
  582. // Initialize the main view.
  583. _viewMaterial = webView.CreateMaterial();
  584. _view.Material = _viewMaterial;
  585. // Initialize the video view (iOS only).
  586. var webViewWithFallbackVideo = webView as IWithFallbackVideo;
  587. if (webViewWithFallbackVideo != null && webViewWithFallbackVideo.FallbackVideoEnabled) {
  588. _videoMaterial = webViewWithFallbackVideo.CreateVideoMaterial();
  589. _videoLayer.Material = _videoMaterial;
  590. _setVideoRect(Rect.zero);
  591. } else {
  592. _videoLayerIsEnabled = false;
  593. }
  594. }
  595. async Task<IWebView> _initWebView(Rect rect, bool preferNative2DMode) {
  596. if (_webViewForInitialization != null) {
  597. return _webViewForInitialization;
  598. }
  599. var webView = Web.CreateWebView(_options.preferredPlugins);
  600. // Enable Native 2D Mode if needed.
  601. var enableNative2DMode = preferNative2DMode && webView is IWithNative2DMode;
  602. if (enableNative2DMode) {
  603. var native2DWebView = webView as IWithNative2DMode;
  604. try {
  605. await native2DWebView.InitInNative2DMode(rect);
  606. } catch (TrialExpiredException ex) {
  607. _handleTrialExpired();
  608. throw ex;
  609. }
  610. // Hide the webview if Visible has already been set to false.
  611. native2DWebView.SetVisible(_visible);
  612. return webView;
  613. }
  614. _updatePixelDensityIfNeeded(webView);
  615. // (iOS only) Enable fallback video if needed.
  616. var webViewWithFallbackVideo = webView as IWithFallbackVideo;
  617. if (webViewWithFallbackVideo != null && !_options.disableVideo) {
  618. webViewWithFallbackVideo.SetFallbackVideoEnabled(true);
  619. }
  620. try {
  621. await webView.Init(_widthInPixels, _heightInPixels);
  622. } catch (TrialExpiredException ex) {
  623. _handleTrialExpired();
  624. throw ex;
  625. }
  626. // (Windows and macOS only) Enable cursor icons if needed.
  627. var webViewWithCursorType = webView as IWithCursorType;
  628. if (webViewWithCursorType != null && CursorIconsEnabled && !XRSettings.enabled) {
  629. webViewWithCursorType.CursorTypeChanged += (sender, eventArgs) => {
  630. Internal.CursorHelper.SetCursorIcon(eventArgs.Value);
  631. _hasOverriddenCursorIcon = eventArgs.Value != "default";
  632. };
  633. }
  634. return webView;
  635. }
  636. void _initPointerInputDetector(IWebView webView) {
  637. if (_pointerInputDetector == null) {
  638. // Pass the argument `true` to find the IPointerInputDetector even if it's disabled.
  639. // Otherwise, if BaseWebViewPrefab.Visible is set to `false` before initialization,
  640. // the IPointerInputDetector won't be found, which will lead to a NullReferenceException.
  641. _pointerInputDetector = GetComponentInChildren<IPointerInputDetector>(true);
  642. }
  643. // Only enable the PointerMoved event if the webview implementation has MovePointer().
  644. _pointerInputDetector.PointerMovedEnabled = (webView as IWithMovablePointer) != null;
  645. _attachOrDetachPointerInputDetector(_pointerInputDetector, true);
  646. }
  647. void InputDetector_BeganDrag(object sender, EventArgs<Vector2> eventArgs) {
  648. _dragThresholdReached = false;
  649. _previousNormalizedDragPoint = _pointerDownNormalizedPoint;
  650. }
  651. void InputDetector_Dragged(object sender, EventArgs<Vector2> eventArgs) {
  652. if (DragMode == DragMode.Disabled || WebView == null) {
  653. return;
  654. }
  655. var newNormalizedDragPoint = eventArgs.Value;
  656. var totalDragDeltaInPixels = WebView.NormalizedToPoint(_pointerDownNormalizedPoint - newNormalizedDragPoint);
  657. if (!_dragThresholdReached) {
  658. // _dragThresholdReached needs to be saved, otherwise it could flip from true back
  659. // to false if the user drags back to the original point where the drag started.
  660. _dragThresholdReached = totalDragDeltaInPixels.magnitude > DragThreshold;
  661. }
  662. if (DragMode == DragMode.DragWithinPage) {
  663. if (_dragThresholdReached) {
  664. _movePointerIfNeeded(newNormalizedDragPoint);
  665. }
  666. return;
  667. }
  668. // DragMode is DragToScroll
  669. var webViewWithTouch = _getTouchIfSupported();
  670. if (webViewWithTouch != null && _dragThresholdReached && !_options.clickWithoutStealingFocus) {
  671. webViewWithTouch.SendTouchEvent(new TouchEvent {
  672. TouchID = 1,
  673. Point = newNormalizedDragPoint,
  674. Type = TouchEventType.Move
  675. });
  676. return;
  677. }
  678. var normalizedDragDelta = _previousNormalizedDragPoint - newNormalizedDragPoint;
  679. if (WebView.NormalizedToPoint(normalizedDragDelta) == Vector2Int.zero) {
  680. // The latest drag delta was less than one pixel of difference, so wait until it reaches at
  681. // least one pixel of change before setting _previousNormalizedDragPoint. Otherwise, small
  682. // drags will be ignored, resulting in scrolling to not work if the drag is very slow.
  683. return;
  684. }
  685. _previousNormalizedDragPoint = newNormalizedDragPoint;
  686. _scrollIfNeeded(normalizedDragDelta, _pointerDownNormalizedPoint);
  687. // Check whether to cancel a pending viewport click so that drag-to-scroll
  688. // doesn't unintentionally trigger a click.
  689. if (_dragToScrollClickIsPending && _dragThresholdReached) {
  690. _dragToScrollClickIsPending = false;
  691. }
  692. }
  693. protected virtual void InputDetector_PointerDown(object sender, PointerEventArgs eventArgs) {
  694. _pointerIsDown = true;
  695. _pointerDownNormalizedPoint = eventArgs.Point;
  696. if (!ClickingEnabled || WebView == null) {
  697. return;
  698. }
  699. if (DragMode == DragMode.DragToScroll) {
  700. var webViewWithTouch = _getTouchIfSupported();
  701. if (webViewWithTouch != null && !_options.clickWithoutStealingFocus) {
  702. webViewWithTouch.SendTouchEvent(new TouchEvent {
  703. TouchID = 1,
  704. Point = eventArgs.Point,
  705. Type = TouchEventType.Start
  706. });
  707. } else {
  708. // For DragToScroll(), defer calling PointerDown() or Click() so that the click can
  709. // be cancelled if the drag exceeds the threshold needed to become a scroll.
  710. _dragToScrollClickIsPending = true;
  711. }
  712. return;
  713. }
  714. // For DragMode.DragWithinPage and DragMode.Disabled, call PointerDown() immediately if the webview supports IWithPointerDownAndUp.
  715. // Note that PointerDown() doesn't currently support an option to avoid stealing focus, so Click() must be used in that case.
  716. var webViewWithPointerDown = WebView as IWithPointerDownAndUp;
  717. if (webViewWithPointerDown != null) {
  718. webViewWithPointerDown.PointerDown(eventArgs.Point, eventArgs.ToPointerOptions(_options.clickWithoutStealingFocus));
  719. } else if (DragMode == DragMode.DragWithinPage && webViewWithPointerDown == null && !_loggedDragWarning) {
  720. _loggedDragWarning = true;
  721. 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>.");
  722. }
  723. }
  724. void InputDetector_PointerEntered(object sender, EventArgs eventArgs) => PointerEntered?.Invoke(this, EventArgs.Empty);
  725. void InputDetector_PointerExited(object sender, EventArgs<Vector2> eventArgs) {
  726. if (HoveringEnabled) {
  727. // Remove the hover state when the pointer exits.
  728. _movePointerIfNeeded(eventArgs.Value, true);
  729. }
  730. if (_hasOverriddenCursorIcon) {
  731. Internal.CursorHelper.SetCursorIcon(null);
  732. _hasOverriddenCursorIcon = false;
  733. }
  734. PointerExited?.Invoke(this, EventArgs.Empty);
  735. }
  736. void InputDetector_PointerMoved(object sender, EventArgs<Vector2> eventArgs) {
  737. // InputDetector_Dragged handles calling MovePointer while dragging.
  738. if (_pointerIsDown || !HoveringEnabled) {
  739. return;
  740. }
  741. _movePointerIfNeeded(eventArgs.Value);
  742. }
  743. protected virtual void InputDetector_PointerUp(object sender, PointerEventArgs eventArgs) {
  744. _pointerIsDown = false;
  745. if (!ClickingEnabled || WebView == null) {
  746. return;
  747. }
  748. var pointerUpPoint = eventArgs.Point;
  749. var clickedEventArgs = new ClickedEventArgs(pointerUpPoint);
  750. if (DragMode == DragMode.DragToScroll) {
  751. var webViewWithTouch = _getTouchIfSupported();
  752. if (webViewWithTouch != null && !_options.clickWithoutStealingFocus) {
  753. webViewWithTouch.SendTouchEvent(new TouchEvent {
  754. TouchID = 1,
  755. Point = pointerUpPoint,
  756. Type = TouchEventType.End
  757. });
  758. Clicked?.Invoke(this, clickedEventArgs);
  759. return;
  760. }
  761. if (!_dragToScrollClickIsPending) {
  762. // The click was cancelled because it was processed as a scroll.
  763. return;
  764. }
  765. }
  766. _dragToScrollClickIsPending = false;
  767. var webViewWithPointerDownAndUp = WebView as IWithPointerDownAndUp;
  768. if (webViewWithPointerDownAndUp == null) {
  769. // When clickWithoutStealingFocus is enabled, use Click() because PointerDown() and PointerUp() don't support the preventStealingFocus parameter.
  770. WebView.Click(eventArgs.Point, _options.clickWithoutStealingFocus);
  771. } else {
  772. var pointerOptions = eventArgs.ToPointerOptions(_options.clickWithoutStealingFocus);
  773. if (DragMode == DragMode.DragToScroll) {
  774. // For DragToScroll, PointerDown() is deferred until just before PointerUp() in case the click is cancelled.
  775. webViewWithPointerDownAndUp.PointerDown(eventArgs.Point, pointerOptions);
  776. } else if (DragMode == DragMode.DragWithinPage) {
  777. // For DragWithinPage, use the point passed to PointerUp() if the DragThreshold wasn't reached.
  778. var totalDragDeltaInPixels = WebView.NormalizedToPoint(_pointerDownNormalizedPoint - eventArgs.Point);
  779. var dragThresholdReached = totalDragDeltaInPixels.magnitude > DragThreshold;
  780. if (!dragThresholdReached) {
  781. pointerUpPoint = _pointerDownNormalizedPoint;
  782. }
  783. }
  784. webViewWithPointerDownAndUp.PointerUp(pointerUpPoint, pointerOptions);
  785. }
  786. Clicked?.Invoke(this, clickedEventArgs);
  787. }
  788. void InputDetector_Scrolled(object sender, ScrolledEventArgs eventArgs) {
  789. var sensitivity = _getScrollingSensitivity();
  790. // The ScrollingSensivity is measured in Unity units because the argument
  791. // passed to Scroll(Vector2) was originally in Unity units (but is now a normalized value).
  792. var scaledScrollDeltaInUnityUnits = eventArgs.ScrollDelta * sensitivity;
  793. var normalizedScrollDelta = new Vector2(scaledScrollDeltaInUnityUnits.x / _sizeInUnityUnits.x, scaledScrollDeltaInUnityUnits.y / _sizeInUnityUnits.y);
  794. _scrollIfNeeded(normalizedScrollDelta, eventArgs.Point);
  795. }
  796. void _movePointerIfNeeded(Vector2 point, bool pointerLeave = false) {
  797. var webViewWithMovablePointer = WebView as IWithMovablePointer;
  798. if (webViewWithMovablePointer == null) {
  799. return;
  800. }
  801. if (point != _previousMovePointerPoint) {
  802. _previousMovePointerPoint = point;
  803. webViewWithMovablePointer.MovePointer(point, pointerLeave);
  804. }
  805. }
  806. bool _native2DModeEnabled(IWebView webView) => webView is IWithNative2DMode && (webView as IWithNative2DMode).Native2DModeEnabled;
  807. protected virtual void OnDestroy() {
  808. if (WebView != null && !WebView.IsDisposed) {
  809. WebView.Dispose();
  810. }
  811. if (KeyboardEnabled) {
  812. var keyboardManager = Internal.KeyboardManager.Instance;
  813. if (keyboardManager != null) {
  814. keyboardManager.SetKeyboardEnabled(this, false);
  815. }
  816. }
  817. if (_pointerInputDetector != null) {
  818. // Detach the pointer input detector in case the app has assigned a custom one that's not a child.
  819. _attachOrDetachPointerInputDetector(_pointerInputDetector, false);
  820. _pointerInputDetectorMonoBehaviour = null;
  821. }
  822. Destroy();
  823. // Unity doesn't automatically destroy materials and textures
  824. // when the GameObject is destroyed.
  825. if (_viewMaterial != null) {
  826. Destroy(_viewMaterial.mainTexture);
  827. Destroy(_viewMaterial);
  828. }
  829. if (_videoMaterial != null) {
  830. Destroy(_videoMaterial.mainTexture);
  831. Destroy(_videoMaterial);
  832. }
  833. if (_hasOverriddenCursorIcon) {
  834. Internal.CursorHelper.SetCursorIcon(null);
  835. _hasOverriddenCursorIcon = false;
  836. }
  837. }
  838. protected void _resizeWebViewIfNeeded() {
  839. if (WebView != null && WebView.Size != new Vector2(_widthInPixels, _heightInPixels)) {
  840. WebView.Resize(_widthInPixels, _heightInPixels);
  841. }
  842. }
  843. void _scrollIfNeeded(Vector2 scrollDelta, Vector2 point) {
  844. // scrollDelta can be zero when the user drags the cursor off the screen.
  845. if (!ScrollingEnabled || WebView == null || scrollDelta == Vector2.zero) {
  846. return;
  847. }
  848. WebView.Scroll(scrollDelta, point);
  849. Scrolled?.Invoke(this, new ScrolledEventArgs(scrollDelta, point));
  850. }
  851. protected abstract void _setVideoLayerPosition(Rect videoRect);
  852. void _setVideoRect(Rect videoRect) {
  853. if (_videoLayer == null) {
  854. return;
  855. }
  856. _view.SetCutoutRect(videoRect);
  857. _setVideoLayerPosition(videoRect);
  858. // This code applies a cropping rect to the video layer's shader based on what part of the video (if any)
  859. // falls outside of the viewport and therefore needs to be hidden. Note that the dimensions here are divided
  860. // by the videoRect's width or height, because in the videoLayer shader, the width of the videoRect is 1
  861. // and the height is 1 (i.e. the dimensions are normalized).
  862. float videoRectXMin = Math.Max(0, - 1 * videoRect.x / videoRect.width);
  863. float videoRectYMin = Math.Max(0, -1 * videoRect.y / videoRect.height);
  864. float videoRectXMax = Math.Min(1, (1 - videoRect.x) / videoRect.width);
  865. float videoRectYMax = Math.Min(1, (1 - videoRect.y) / videoRect.height);
  866. var videoCropRect = Rect.MinMaxRect(videoRectXMin, videoRectYMin, videoRectXMax, videoRectYMax);
  867. if (videoCropRect == new Rect(0, 0, 1, 1)) {
  868. // The entire video rect fits within the viewport, so set the cropt rect to zero to disable it.
  869. videoCropRect = Rect.zero;
  870. }
  871. _videoLayer.SetCropRect(videoCropRect);
  872. }
  873. void _throwExceptionIfInitialized() {
  874. if (WebView != null) {
  875. throw new InvalidOperationException("Init() cannot be called on a WebViewPrefab that has already been initialized.");
  876. }
  877. }
  878. protected virtual void Update() {
  879. _updateResolutionIfNeeded();
  880. _updatePixelDensityIfNeeded(WebView);
  881. _enableOrDisableKeyboardIfNeeded();
  882. _enableConsoleMessagesIfNeeded(WebView);
  883. }
  884. void _updatePixelDensityIfNeeded(IWebView webView) {
  885. var webViewWithPixelDensity = webView as IWithPixelDensity;
  886. if (webViewWithPixelDensity == null || PixelDensity == webViewWithPixelDensity.PixelDensity) {
  887. return;
  888. }
  889. try {
  890. webViewWithPixelDensity.SetPixelDensity(PixelDensity);
  891. } catch (ArgumentException ex) {
  892. WebViewLogger.LogError(ex.ToString());
  893. PixelDensity = 1;
  894. }
  895. }
  896. void _updateResolutionIfNeeded() {
  897. var resolution = _getResolution();
  898. if (_appliedResolution != resolution) {
  899. if (resolution > 0.0f) {
  900. _appliedResolution = resolution;
  901. _resizeWebViewIfNeeded();
  902. } else {
  903. WebViewLogger.LogWarning("Ignoring invalid Resolution: " + resolution);
  904. }
  905. }
  906. }
  907. void WebView_ConsoleMessageLogged(object sender, ConsoleMessageEventArgs eventArgs) {
  908. if (!LogConsoleMessages) {
  909. return;
  910. }
  911. var message = "[Web Console] " + eventArgs.Message;
  912. if (eventArgs.Source != null) {
  913. message += $" ({eventArgs.Source}:{eventArgs.Line})";
  914. }
  915. switch (eventArgs.Level) {
  916. case ConsoleMessageLevel.Error:
  917. WebViewLogger.LogError(message, false);
  918. break;
  919. case ConsoleMessageLevel.Warning:
  920. WebViewLogger.LogWarning(message, false);
  921. break;
  922. default:
  923. WebViewLogger.Log(message, false);
  924. break;
  925. }
  926. }
  927. void WebView_TextureChanged(object sender, EventArgs<Texture2D> eventArgs) {
  928. var oldTexture = _view.Texture;
  929. if (oldTexture is RenderTexture) {
  930. // The application replaced WebViewPrefab.Material.mainTexture with a RenderTexture
  931. // (for example, to implement MipMaps), so don't change the prefab's texture.
  932. return;
  933. }
  934. _view.Texture = eventArgs.Value;
  935. Destroy(oldTexture);
  936. }
  937. #endregion
  938. // Added in v3.5, removed in v3.7.
  939. [Obsolete("The WebViewPrefab.DragToScrollThreshold property has been removed. Please use DragThreshold instead: https://developer.vuplex.com/webview/WebViewPrefab#DragThreshold", true)]
  940. public float DragToScrollThreshold { get; set; }
  941. }
  942. }