tauri_runtime/
webview.rs

1// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5//! A layer between raw [`Runtime`] webviews and Tauri.
6//!
7use crate::{window::is_label_valid, Rect, Runtime, UserEvent};
8
9use http::Request;
10use tauri_utils::config::{
11  BackgroundThrottlingPolicy, Color, WebviewUrl, WindowConfig, WindowEffectsConfig,
12};
13use url::Url;
14
15use std::{
16  borrow::Cow,
17  collections::HashMap,
18  hash::{Hash, Hasher},
19  path::PathBuf,
20  sync::Arc,
21};
22
23type UriSchemeProtocol = dyn Fn(&str, http::Request<Vec<u8>>, Box<dyn FnOnce(http::Response<Cow<'static, [u8]>>) + Send>)
24  + Send
25  + Sync
26  + 'static;
27
28type WebResourceRequestHandler =
29  dyn Fn(http::Request<Vec<u8>>, &mut http::Response<Cow<'static, [u8]>>) + Send + Sync;
30
31type NavigationHandler = dyn Fn(&Url) -> bool + Send;
32
33type OnPageLoadHandler = dyn Fn(Url, PageLoadEvent) + Send;
34
35type DownloadHandler = dyn Fn(DownloadEvent) -> bool + Send + Sync;
36
37/// Download event.
38pub enum DownloadEvent<'a> {
39  /// Download requested.
40  Requested {
41    /// The url being downloaded.
42    url: Url,
43    /// Represents where the file will be downloaded to.
44    /// Can be used to set the download location by assigning a new path to it.
45    /// The assigned path _must_ be absolute.
46    destination: &'a mut PathBuf,
47  },
48  /// Download finished.
49  Finished {
50    /// The URL of the original download request.
51    url: Url,
52    /// Potentially representing the filesystem path the file was downloaded to.
53    path: Option<PathBuf>,
54    /// Indicates if the download succeeded or not.
55    success: bool,
56  },
57}
58
59#[cfg(target_os = "android")]
60pub struct CreationContext<'a, 'b> {
61  pub env: &'a mut jni::JNIEnv<'b>,
62  pub activity: &'a jni::objects::JObject<'b>,
63  pub webview: &'a jni::objects::JObject<'b>,
64}
65
66/// Kind of event for the page load handler.
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum PageLoadEvent {
69  /// Page started to load.
70  Started,
71  /// Page finished loading.
72  Finished,
73}
74
75/// A webview that has yet to be built.
76pub struct PendingWebview<T: UserEvent, R: Runtime<T>> {
77  /// The label that the webview will be named.
78  pub label: String,
79
80  /// The [`WebviewAttributes`] that the webview will be created with.
81  pub webview_attributes: WebviewAttributes,
82
83  pub uri_scheme_protocols: HashMap<String, Box<UriSchemeProtocol>>,
84
85  /// How to handle IPC calls on the webview.
86  pub ipc_handler: Option<WebviewIpcHandler<T, R>>,
87
88  /// A handler to decide if incoming url is allowed to navigate.
89  pub navigation_handler: Option<Box<NavigationHandler>>,
90
91  /// The resolved URL to load on the webview.
92  pub url: String,
93
94  #[cfg(target_os = "android")]
95  #[allow(clippy::type_complexity)]
96  pub on_webview_created:
97    Option<Box<dyn Fn(CreationContext<'_, '_>) -> Result<(), jni::errors::Error> + Send>>,
98
99  pub web_resource_request_handler: Option<Box<WebResourceRequestHandler>>,
100
101  pub on_page_load_handler: Option<Box<OnPageLoadHandler>>,
102
103  pub download_handler: Option<Arc<DownloadHandler>>,
104}
105
106impl<T: UserEvent, R: Runtime<T>> PendingWebview<T, R> {
107  /// Create a new [`PendingWebview`] with a label from the given [`WebviewAttributes`].
108  pub fn new(
109    webview_attributes: WebviewAttributes,
110    label: impl Into<String>,
111  ) -> crate::Result<Self> {
112    let label = label.into();
113    if !is_label_valid(&label) {
114      Err(crate::Error::InvalidWindowLabel)
115    } else {
116      Ok(Self {
117        webview_attributes,
118        uri_scheme_protocols: Default::default(),
119        label,
120        ipc_handler: None,
121        navigation_handler: None,
122        url: "tauri://localhost".to_string(),
123        #[cfg(target_os = "android")]
124        on_webview_created: None,
125        web_resource_request_handler: None,
126        on_page_load_handler: None,
127        download_handler: None,
128      })
129    }
130  }
131
132  pub fn register_uri_scheme_protocol<
133    N: Into<String>,
134    H: Fn(&str, http::Request<Vec<u8>>, Box<dyn FnOnce(http::Response<Cow<'static, [u8]>>) + Send>)
135      + Send
136      + Sync
137      + 'static,
138  >(
139    &mut self,
140    uri_scheme: N,
141    protocol: H,
142  ) {
143    let uri_scheme = uri_scheme.into();
144    self
145      .uri_scheme_protocols
146      .insert(uri_scheme, Box::new(protocol));
147  }
148
149  #[cfg(target_os = "android")]
150  pub fn on_webview_created<
151    F: Fn(CreationContext<'_, '_>) -> Result<(), jni::errors::Error> + Send + 'static,
152  >(
153    mut self,
154    f: F,
155  ) -> Self {
156    self.on_webview_created.replace(Box::new(f));
157    self
158  }
159}
160
161/// A webview that is not yet managed by Tauri.
162#[derive(Debug)]
163pub struct DetachedWebview<T: UserEvent, R: Runtime<T>> {
164  /// Name of the window
165  pub label: String,
166
167  /// The [`crate::WebviewDispatch`] associated with the window.
168  pub dispatcher: R::WebviewDispatcher,
169}
170
171impl<T: UserEvent, R: Runtime<T>> Clone for DetachedWebview<T, R> {
172  fn clone(&self) -> Self {
173    Self {
174      label: self.label.clone(),
175      dispatcher: self.dispatcher.clone(),
176    }
177  }
178}
179
180impl<T: UserEvent, R: Runtime<T>> Hash for DetachedWebview<T, R> {
181  /// Only use the [`DetachedWebview`]'s label to represent its hash.
182  fn hash<H: Hasher>(&self, state: &mut H) {
183    self.label.hash(state)
184  }
185}
186
187impl<T: UserEvent, R: Runtime<T>> Eq for DetachedWebview<T, R> {}
188impl<T: UserEvent, R: Runtime<T>> PartialEq for DetachedWebview<T, R> {
189  /// Only use the [`DetachedWebview`]'s label to compare equality.
190  fn eq(&self, other: &Self) -> bool {
191    self.label.eq(&other.label)
192  }
193}
194
195/// The attributes used to create an webview.
196#[derive(Debug, Clone)]
197pub struct WebviewAttributes {
198  pub url: WebviewUrl,
199  pub user_agent: Option<String>,
200  pub initialization_scripts: Vec<String>,
201  pub data_directory: Option<PathBuf>,
202  pub drag_drop_handler_enabled: bool,
203  pub clipboard: bool,
204  pub accept_first_mouse: bool,
205  pub additional_browser_args: Option<String>,
206  pub window_effects: Option<WindowEffectsConfig>,
207  pub incognito: bool,
208  pub transparent: bool,
209  pub focus: bool,
210  pub bounds: Option<Rect>,
211  pub auto_resize: bool,
212  pub proxy_url: Option<Url>,
213  pub zoom_hotkeys_enabled: bool,
214  pub browser_extensions_enabled: bool,
215  pub extensions_path: Option<PathBuf>,
216  pub data_store_identifier: Option<[u8; 16]>,
217  pub use_https_scheme: bool,
218  pub devtools: Option<bool>,
219  pub background_color: Option<Color>,
220  pub background_throttling: Option<BackgroundThrottlingPolicy>,
221}
222
223impl From<&WindowConfig> for WebviewAttributes {
224  fn from(config: &WindowConfig) -> Self {
225    let mut builder = Self::new(config.url.clone())
226      .incognito(config.incognito)
227      .focused(config.focus)
228      .zoom_hotkeys_enabled(config.zoom_hotkeys_enabled)
229      .use_https_scheme(config.use_https_scheme)
230      .browser_extensions_enabled(config.browser_extensions_enabled)
231      .background_throttling(config.background_throttling.clone())
232      .devtools(config.devtools);
233
234    #[cfg(any(not(target_os = "macos"), feature = "macos-private-api"))]
235    {
236      builder = builder.transparent(config.transparent);
237    }
238    builder = builder.accept_first_mouse(config.accept_first_mouse);
239    if !config.drag_drop_enabled {
240      builder = builder.disable_drag_drop_handler();
241    }
242    if let Some(user_agent) = &config.user_agent {
243      builder = builder.user_agent(user_agent);
244    }
245    if let Some(additional_browser_args) = &config.additional_browser_args {
246      builder = builder.additional_browser_args(additional_browser_args);
247    }
248    if let Some(effects) = &config.window_effects {
249      builder = builder.window_effects(effects.clone());
250    }
251    if let Some(url) = &config.proxy_url {
252      builder = builder.proxy_url(url.to_owned());
253    }
254    if let Some(color) = config.background_color {
255      builder = builder.background_color(color);
256    }
257    builder
258  }
259}
260
261impl WebviewAttributes {
262  /// Initializes the default attributes for a webview.
263  pub fn new(url: WebviewUrl) -> Self {
264    Self {
265      url,
266      user_agent: None,
267      initialization_scripts: Vec::new(),
268      data_directory: None,
269      drag_drop_handler_enabled: true,
270      clipboard: false,
271      accept_first_mouse: false,
272      additional_browser_args: None,
273      window_effects: None,
274      incognito: false,
275      transparent: false,
276      focus: true,
277      bounds: None,
278      auto_resize: false,
279      proxy_url: None,
280      zoom_hotkeys_enabled: false,
281      browser_extensions_enabled: false,
282      data_store_identifier: None,
283      extensions_path: None,
284      use_https_scheme: false,
285      devtools: None,
286      background_color: None,
287      background_throttling: None,
288    }
289  }
290
291  /// Sets the user agent
292  #[must_use]
293  pub fn user_agent(mut self, user_agent: &str) -> Self {
294    self.user_agent = Some(user_agent.to_string());
295    self
296  }
297
298  /// Sets the init script.
299  #[must_use]
300  pub fn initialization_script(mut self, script: &str) -> Self {
301    self.initialization_scripts.push(script.to_string());
302    self
303  }
304
305  /// Data directory for the webview.
306  #[must_use]
307  pub fn data_directory(mut self, data_directory: PathBuf) -> Self {
308    self.data_directory.replace(data_directory);
309    self
310  }
311
312  /// Disables the drag and drop handler. This is required to use HTML5 drag and drop APIs on the frontend on Windows.
313  #[must_use]
314  pub fn disable_drag_drop_handler(mut self) -> Self {
315    self.drag_drop_handler_enabled = false;
316    self
317  }
318
319  /// Enables clipboard access for the page rendered on **Linux** and **Windows**.
320  ///
321  /// **macOS** doesn't provide such method and is always enabled by default,
322  /// but you still need to add menu item accelerators to use shortcuts.
323  #[must_use]
324  pub fn enable_clipboard_access(mut self) -> Self {
325    self.clipboard = true;
326    self
327  }
328
329  /// Sets whether clicking an inactive window also clicks through to the webview.
330  #[must_use]
331  pub fn accept_first_mouse(mut self, accept: bool) -> Self {
332    self.accept_first_mouse = accept;
333    self
334  }
335
336  /// Sets additional browser arguments. **Windows Only**
337  #[must_use]
338  pub fn additional_browser_args(mut self, additional_args: &str) -> Self {
339    self.additional_browser_args = Some(additional_args.to_string());
340    self
341  }
342
343  /// Sets window effects
344  #[must_use]
345  pub fn window_effects(mut self, effects: WindowEffectsConfig) -> Self {
346    self.window_effects = Some(effects);
347    self
348  }
349
350  /// Enable or disable incognito mode for the WebView.
351  #[must_use]
352  pub fn incognito(mut self, incognito: bool) -> Self {
353    self.incognito = incognito;
354    self
355  }
356
357  /// Enable or disable transparency for the WebView.
358  #[cfg(any(not(target_os = "macos"), feature = "macos-private-api"))]
359  #[must_use]
360  pub fn transparent(mut self, transparent: bool) -> Self {
361    self.transparent = transparent;
362    self
363  }
364
365  /// Whether the webview should be focused or not.
366  #[must_use]
367  pub fn focused(mut self, focus: bool) -> Self {
368    self.focus = focus;
369    self
370  }
371
372  /// Sets the webview to automatically grow and shrink its size and position when the parent window resizes.
373  #[must_use]
374  pub fn auto_resize(mut self) -> Self {
375    self.auto_resize = true;
376    self
377  }
378
379  /// Enable proxy for the WebView
380  #[must_use]
381  pub fn proxy_url(mut self, url: Url) -> Self {
382    self.proxy_url = Some(url);
383    self
384  }
385
386  /// Whether page zooming by hotkeys is enabled
387  ///
388  /// ## Platform-specific:
389  ///
390  /// - **Windows**: Controls WebView2's [`IsZoomControlEnabled`](https://learn.microsoft.com/en-us/microsoft-edge/webview2/reference/winrt/microsoft_web_webview2_core/corewebview2settings?view=webview2-winrt-1.0.2420.47#iszoomcontrolenabled) setting.
391  /// - **MacOS / Linux**: Injects a polyfill that zooms in and out with `ctrl/command` + `-/=`,
392  ///   20% in each step, ranging from 20% to 1000%. Requires `webview:allow-set-webview-zoom` permission
393  ///
394  /// - **Android / iOS**: Unsupported.
395  #[must_use]
396  pub fn zoom_hotkeys_enabled(mut self, enabled: bool) -> Self {
397    self.zoom_hotkeys_enabled = enabled;
398    self
399  }
400
401  /// Whether browser extensions can be installed for the webview process
402  ///
403  /// ## Platform-specific:
404  ///
405  /// - **Windows**: Enables the WebView2 environment's [`AreBrowserExtensionsEnabled`](https://learn.microsoft.com/en-us/microsoft-edge/webview2/reference/winrt/microsoft_web_webview2_core/corewebview2environmentoptions?view=webview2-winrt-1.0.2739.15#arebrowserextensionsenabled)
406  /// - **MacOS / Linux / iOS / Android** - Unsupported.
407  #[must_use]
408  pub fn browser_extensions_enabled(mut self, enabled: bool) -> Self {
409    self.browser_extensions_enabled = enabled;
410    self
411  }
412
413  /// Sets whether the custom protocols should use `https://<scheme>.localhost` instead of the default `http://<scheme>.localhost` on Windows and Android. Defaults to `false`.
414  ///
415  /// ## Note
416  ///
417  /// Using a `https` scheme will NOT allow mixed content when trying to fetch `http` endpoints and therefore will not match the behavior of the `<scheme>://localhost` protocols used on macOS and Linux.
418  ///
419  /// ## Warning
420  ///
421  /// Changing this value between releases will change the IndexedDB, cookies and localstorage location and your app will not be able to access the old data.
422  #[must_use]
423  pub fn use_https_scheme(mut self, enabled: bool) -> Self {
424    self.use_https_scheme = enabled;
425    self
426  }
427
428  /// Whether web inspector, which is usually called browser devtools, is enabled or not. Enabled by default.
429  ///
430  /// This API works in **debug** builds, but requires `devtools` feature flag to enable it in **release** builds.
431  ///
432  /// ## Platform-specific
433  ///
434  /// - macOS: This will call private functions on **macOS**.
435  /// - Android: Open `chrome://inspect/#devices` in Chrome to get the devtools window. Wry's `WebView` devtools API isn't supported on Android.
436  /// - iOS: Open Safari > Develop > [Your Device Name] > [Your WebView] to get the devtools window.
437  #[must_use]
438  pub fn devtools(mut self, enabled: Option<bool>) -> Self {
439    self.devtools = enabled;
440    self
441  }
442
443  /// Set the window and webview background color.
444  /// ## Platform-specific:
445  ///
446  /// - **Windows**: On Windows 7, alpha channel is ignored for the webview layer.
447  /// - **Windows**: On Windows 8 and newer, if alpha channel is not `0`, it will be ignored.
448  #[must_use]
449  pub fn background_color(mut self, color: Color) -> Self {
450    self.background_color = Some(color);
451    self
452  }
453
454  /// Change the default background throttling behaviour.
455  ///
456  /// By default, browsers use a suspend policy that will throttle timers and even unload
457  /// the whole tab (view) to free resources after roughly 5 minutes when a view became
458  /// minimized or hidden. This will pause all tasks until the documents visibility state
459  /// changes back from hidden to visible by bringing the view back to the foreground.
460  ///
461  /// ## Platform-specific
462  ///
463  /// - **Linux / Windows / Android**: Unsupported. Workarounds like a pending WebLock transaction might suffice.
464  /// - **iOS**: Supported since version 17.0+.
465  /// - **macOS**: Supported since version 14.0+.
466  ///
467  /// see https://github.com/tauri-apps/tauri/issues/5250#issuecomment-2569380578
468  #[must_use]
469  pub fn background_throttling(mut self, policy: Option<BackgroundThrottlingPolicy>) -> Self {
470    self.background_throttling = policy;
471    self
472  }
473}
474
475/// IPC handler.
476pub type WebviewIpcHandler<T, R> = Box<dyn Fn(DetachedWebview<T, R>, Request<String>) + Send>;