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