tauri/webview/
mod.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//! The Tauri webview types and functions.
6
7pub(crate) mod plugin;
8mod webview_window;
9
10pub use webview_window::{WebviewWindow, WebviewWindowBuilder};
11
12use http::HeaderMap;
13use serde::Serialize;
14use tauri_macros::default_runtime;
15pub use tauri_runtime::webview::PageLoadEvent;
16pub use tauri_runtime::Cookie;
17#[cfg(desktop)]
18use tauri_runtime::{
19  dpi::{PhysicalPosition, PhysicalSize, Position, Size},
20  WindowDispatch,
21};
22use tauri_runtime::{
23  webview::{DetachedWebview, InitializationScript, PendingWebview, WebviewAttributes},
24  WebviewDispatch,
25};
26pub use tauri_utils::config::Color;
27use tauri_utils::config::{BackgroundThrottlingPolicy, WebviewUrl, WindowConfig};
28pub use url::Url;
29
30use crate::{
31  app::{UriSchemeResponder, WebviewEvent},
32  event::{EmitArgs, EventTarget},
33  ipc::{
34    CallbackFn, CommandArg, CommandItem, CommandScope, GlobalScope, Invoke, InvokeBody,
35    InvokeError, InvokeMessage, InvokeResolver, Origin, OwnedInvokeResponder, ScopeObject,
36  },
37  manager::AppManager,
38  sealed::{ManagerBase, RuntimeOrDispatch},
39  AppHandle, Emitter, Event, EventId, EventLoopMessage, EventName, Listener, Manager,
40  ResourceTable, Runtime, Window,
41};
42
43use std::{
44  borrow::Cow,
45  hash::{Hash, Hasher},
46  path::{Path, PathBuf},
47  sync::{Arc, Mutex, MutexGuard},
48};
49
50pub(crate) type WebResourceRequestHandler =
51  dyn Fn(http::Request<Vec<u8>>, &mut http::Response<Cow<'static, [u8]>>) + Send + Sync;
52pub(crate) type NavigationHandler = dyn Fn(&Url) -> bool + Send;
53pub(crate) type UriSchemeProtocolHandler =
54  Box<dyn Fn(&str, http::Request<Vec<u8>>, UriSchemeResponder) + Send + Sync>;
55pub(crate) type OnPageLoad<R> = dyn Fn(Webview<R>, PageLoadPayload<'_>) + Send + Sync + 'static;
56
57pub(crate) type DownloadHandler<R> = dyn Fn(Webview<R>, DownloadEvent<'_>) -> bool + Send + Sync;
58
59#[derive(Clone, Serialize)]
60pub(crate) struct CreatedEvent {
61  pub(crate) label: String,
62}
63
64/// Download event for the [`WebviewBuilder#method.on_download`] hook.
65#[non_exhaustive]
66pub enum DownloadEvent<'a> {
67  /// Download requested.
68  Requested {
69    /// The url being downloaded.
70    url: Url,
71    /// Represents where the file will be downloaded to.
72    /// Can be used to set the download location by assigning a new path to it.
73    /// The assigned path _must_ be absolute.
74    destination: &'a mut PathBuf,
75  },
76  /// Download finished.
77  Finished {
78    /// The URL of the original download request.
79    url: Url,
80    /// Potentially representing the filesystem path the file was downloaded to.
81    ///
82    /// A value of `None` being passed instead of a `PathBuf` does not necessarily indicate that the download
83    /// did not succeed, and may instead indicate some other failure - always check the third parameter if you need to
84    /// know if the download succeeded.
85    ///
86    /// ## Platform-specific:
87    ///
88    /// - **macOS**: The second parameter indicating the path the file was saved to is always empty, due to API
89    ///   limitations.
90    path: Option<PathBuf>,
91    /// Indicates if the download succeeded or not.
92    success: bool,
93  },
94}
95
96/// The payload for the [`WebviewBuilder::on_page_load`] hook.
97#[derive(Debug, Clone)]
98pub struct PageLoadPayload<'a> {
99  pub(crate) url: &'a Url,
100  pub(crate) event: PageLoadEvent,
101}
102
103impl<'a> PageLoadPayload<'a> {
104  /// The page URL.
105  pub fn url(&self) -> &'a Url {
106    self.url
107  }
108
109  /// The page load event.
110  pub fn event(&self) -> PageLoadEvent {
111    self.event
112  }
113}
114
115/// The IPC invoke request.
116///
117/// # Stability
118///
119/// This struct is **NOT** part of the public stable API and is only meant to be used
120/// by internal code and external testing/fuzzing tools or custom invoke systems.
121#[derive(Debug)]
122pub struct InvokeRequest {
123  /// The invoke command.
124  pub cmd: String,
125  /// The success callback.
126  pub callback: CallbackFn,
127  /// The error callback.
128  pub error: CallbackFn,
129  /// URL of the frame that requested this command.
130  pub url: Url,
131  /// The body of the request.
132  pub body: InvokeBody,
133  /// The request headers.
134  pub headers: HeaderMap,
135  /// The invoke key. Must match what was passed to the app manager.
136  pub invoke_key: String,
137}
138
139/// The platform webview handle. Accessed with [`Webview#method.with_webview`];
140#[cfg(feature = "wry")]
141#[cfg_attr(docsrs, doc(cfg(feature = "wry")))]
142pub struct PlatformWebview(tauri_runtime_wry::Webview);
143
144#[cfg(feature = "wry")]
145impl PlatformWebview {
146  /// Returns [`webkit2gtk::WebView`] handle.
147  #[cfg(any(
148    target_os = "linux",
149    target_os = "dragonfly",
150    target_os = "freebsd",
151    target_os = "netbsd",
152    target_os = "openbsd"
153  ))]
154  #[cfg_attr(
155    docsrs,
156    doc(cfg(any(
157      target_os = "linux",
158      target_os = "dragonfly",
159      target_os = "freebsd",
160      target_os = "netbsd",
161      target_os = "openbsd"
162    )))
163  )]
164  pub fn inner(&self) -> webkit2gtk::WebView {
165    self.0.clone()
166  }
167
168  /// Returns the WebView2 controller.
169  #[cfg(windows)]
170  #[cfg_attr(docsrs, doc(cfg(windows)))]
171  pub fn controller(
172    &self,
173  ) -> webview2_com::Microsoft::Web::WebView2::Win32::ICoreWebView2Controller {
174    self.0.controller.clone()
175  }
176
177  /// Returns the [WKWebView] handle.
178  ///
179  /// [WKWebView]: https://developer.apple.com/documentation/webkit/wkwebview
180  #[cfg(any(target_os = "macos", target_os = "ios"))]
181  #[cfg_attr(docsrs, doc(cfg(any(target_os = "macos", target_os = "ios"))))]
182  pub fn inner(&self) -> *mut std::ffi::c_void {
183    self.0.webview
184  }
185
186  /// Returns WKWebView [controller] handle.
187  ///
188  /// [controller]: https://developer.apple.com/documentation/webkit/wkusercontentcontroller
189  #[cfg(any(target_os = "macos", target_os = "ios"))]
190  #[cfg_attr(docsrs, doc(cfg(any(target_os = "macos", target_os = "ios"))))]
191  pub fn controller(&self) -> *mut std::ffi::c_void {
192    self.0.manager
193  }
194
195  /// Returns [NSWindow] associated with the WKWebView webview.
196  ///
197  /// [NSWindow]: https://developer.apple.com/documentation/appkit/nswindow
198  #[cfg(target_os = "macos")]
199  #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))]
200  pub fn ns_window(&self) -> *mut std::ffi::c_void {
201    self.0.ns_window
202  }
203
204  /// Returns [UIViewController] used by the WKWebView webview NSWindow.
205  ///
206  /// [UIViewController]: https://developer.apple.com/documentation/uikit/uiviewcontroller
207  #[cfg(target_os = "ios")]
208  #[cfg_attr(docsrs, doc(cfg(target_os = "ios")))]
209  pub fn view_controller(&self) -> *mut std::ffi::c_void {
210    self.0.view_controller
211  }
212
213  /// Returns handle for JNI execution.
214  #[cfg(target_os = "android")]
215  pub fn jni_handle(&self) -> tauri_runtime_wry::wry::JniHandle {
216    self.0
217  }
218}
219
220macro_rules! unstable_struct {
221    (#[doc = $doc:expr] $($tokens:tt)*) => {
222      #[cfg(any(test, feature = "unstable"))]
223      #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))]
224      #[doc = $doc]
225      pub $($tokens)*
226
227      #[cfg(not(any(test, feature = "unstable")))]
228      pub(crate) $($tokens)*
229    }
230}
231
232unstable_struct!(
233  #[doc = "A builder for a webview."]
234  struct WebviewBuilder<R: Runtime> {
235    pub(crate) label: String,
236    pub(crate) webview_attributes: WebviewAttributes,
237    pub(crate) web_resource_request_handler: Option<Box<WebResourceRequestHandler>>,
238    pub(crate) navigation_handler: Option<Box<NavigationHandler>>,
239    pub(crate) on_page_load_handler: Option<Box<OnPageLoad<R>>>,
240    pub(crate) download_handler: Option<Arc<DownloadHandler<R>>>,
241  }
242);
243
244#[cfg_attr(not(feature = "unstable"), allow(dead_code))]
245impl<R: Runtime> WebviewBuilder<R> {
246  /// Initializes a webview builder with the given webview label and URL to load.
247  ///
248  /// # Known issues
249  ///
250  /// On Windows, this function deadlocks when used in a synchronous command or event handlers, see [the Webview2 issue].
251  /// You should use `async` commands and separate threads when creating webviews.
252  ///
253  /// # Examples
254  ///
255  /// - Create a webview in the setup hook:
256  ///
257  #[cfg_attr(
258    feature = "unstable",
259    doc = r####"
260```
261tauri::Builder::default()
262  .setup(|app| {
263    let window = tauri::window::WindowBuilder::new(app, "label").build()?;
264    let webview_builder = tauri::webview::WebviewBuilder::new("label", tauri::WebviewUrl::App("index.html".into()));
265    let webview = window.add_child(webview_builder, tauri::LogicalPosition::new(0, 0), window.inner_size().unwrap());
266    Ok(())
267  });
268```
269  "####
270  )]
271  ///
272  /// - Create a webview in a separate thread:
273  ///
274  #[cfg_attr(
275    feature = "unstable",
276    doc = r####"
277```
278tauri::Builder::default()
279  .setup(|app| {
280    let handle = app.handle().clone();
281    std::thread::spawn(move || {
282      let window = tauri::window::WindowBuilder::new(&handle, "label").build().unwrap();
283      let webview_builder = tauri::webview::WebviewBuilder::new("label", tauri::WebviewUrl::App("index.html".into()));
284      window.add_child(webview_builder, tauri::LogicalPosition::new(0, 0), window.inner_size().unwrap());
285    });
286    Ok(())
287  });
288```
289   "####
290  )]
291  ///
292  /// - Create a webview in a command:
293  ///
294  #[cfg_attr(
295    feature = "unstable",
296    doc = r####"
297```
298#[tauri::command]
299async fn create_window(app: tauri::AppHandle) {
300  let window = tauri::window::WindowBuilder::new(&app, "label").build().unwrap();
301  let webview_builder = tauri::webview::WebviewBuilder::new("label", tauri::WebviewUrl::External("https://tauri.app/".parse().unwrap()));
302  window.add_child(webview_builder, tauri::LogicalPosition::new(0, 0), window.inner_size().unwrap());
303}
304```
305  "####
306  )]
307  ///
308  /// [the Webview2 issue]: https://github.com/tauri-apps/wry/issues/583
309  pub fn new<L: Into<String>>(label: L, url: WebviewUrl) -> Self {
310    Self {
311      label: label.into(),
312      webview_attributes: WebviewAttributes::new(url),
313      web_resource_request_handler: None,
314      navigation_handler: None,
315      on_page_load_handler: None,
316      download_handler: None,
317    }
318  }
319
320  /// Initializes a webview builder from a [`WindowConfig`] from tauri.conf.json.
321  /// Keep in mind that you can't create 2 webviews with the same `label` so make sure
322  /// that the initial webview was closed or change the label of the new [`WebviewBuilder`].
323  ///
324  /// # Known issues
325  ///
326  /// On Windows, this function deadlocks when used in a synchronous command or event handlers, see [the Webview2 issue].
327  /// You should use `async` commands and separate threads when creating webviews.
328  ///
329  /// # Examples
330  ///
331  /// - Create a webview in a command:
332  ///
333  #[cfg_attr(
334    feature = "unstable",
335    doc = r####"
336```
337#[tauri::command]
338async fn create_window(app: tauri::AppHandle) {
339  let window = tauri::window::WindowBuilder::new(&app, "label").build().unwrap();
340  let webview_builder = tauri::webview::WebviewBuilder::from_config(&app.config().app.windows.get(0).unwrap().clone());
341  window.add_child(webview_builder, tauri::LogicalPosition::new(0, 0), window.inner_size().unwrap());
342}
343```
344  "####
345  )]
346  ///
347  /// [the Webview2 issue]: https://github.com/tauri-apps/wry/issues/583
348  pub fn from_config(config: &WindowConfig) -> Self {
349    Self {
350      label: config.label.clone(),
351      webview_attributes: WebviewAttributes::from(config),
352      web_resource_request_handler: None,
353      navigation_handler: None,
354      on_page_load_handler: None,
355      download_handler: None,
356    }
357  }
358
359  /// Defines a closure to be executed when the webview makes an HTTP request for a web resource, allowing you to modify the response.
360  ///
361  /// Currently only implemented for the `tauri` URI protocol.
362  ///
363  /// **NOTE:** Currently this is **not** executed when using external URLs such as a development server,
364  /// but it might be implemented in the future. **Always** check the request URL.
365  ///
366  /// # Examples
367  ///
368  #[cfg_attr(
369    feature = "unstable",
370    doc = r####"
371```rust,no_run
372use tauri::{
373  utils::config::{Csp, CspDirectiveSources, WebviewUrl},
374  window::WindowBuilder,
375  webview::WebviewBuilder,
376};
377use http::header::HeaderValue;
378use std::collections::HashMap;
379tauri::Builder::default()
380  .setup(|app| {
381    let window = tauri::window::WindowBuilder::new(app, "label").build()?;
382
383    let webview_builder = WebviewBuilder::new("core", WebviewUrl::App("index.html".into()))
384      .on_web_resource_request(|request, response| {
385        if request.uri().scheme_str() == Some("tauri") {
386          // if we have a CSP header, Tauri is loading an HTML file
387          //  for this example, let's dynamically change the CSP
388          if let Some(csp) = response.headers_mut().get_mut("Content-Security-Policy") {
389            // use the tauri helper to parse the CSP policy to a map
390            let mut csp_map: HashMap<String, CspDirectiveSources> = Csp::Policy(csp.to_str().unwrap().to_string()).into();
391            csp_map.entry("script-src".to_string()).or_insert_with(Default::default).push("'unsafe-inline'");
392            // use the tauri helper to get a CSP string from the map
393            let csp_string = Csp::from(csp_map).to_string();
394            *csp = HeaderValue::from_str(&csp_string).unwrap();
395          }
396        }
397      });
398
399    let webview = window.add_child(webview_builder, tauri::LogicalPosition::new(0, 0), window.inner_size().unwrap())?;
400
401    Ok(())
402  });
403```
404  "####
405  )]
406  pub fn on_web_resource_request<
407    F: Fn(http::Request<Vec<u8>>, &mut http::Response<Cow<'static, [u8]>>) + Send + Sync + 'static,
408  >(
409    mut self,
410    f: F,
411  ) -> Self {
412    self.web_resource_request_handler.replace(Box::new(f));
413    self
414  }
415
416  /// Defines a closure to be executed when the webview navigates to a URL. Returning `false` cancels the navigation.
417  ///
418  /// # Examples
419  ///
420  #[cfg_attr(
421    feature = "unstable",
422    doc = r####"
423```rust,no_run
424use tauri::{
425  utils::config::{Csp, CspDirectiveSources, WebviewUrl},
426  window::WindowBuilder,
427  webview::WebviewBuilder,
428};
429use http::header::HeaderValue;
430use std::collections::HashMap;
431tauri::Builder::default()
432  .setup(|app| {
433    let window = tauri::window::WindowBuilder::new(app, "label").build()?;
434
435    let webview_builder = WebviewBuilder::new("core", WebviewUrl::App("index.html".into()))
436      .on_navigation(|url| {
437        // allow the production URL or localhost on dev
438        url.scheme() == "tauri" || (cfg!(dev) && url.host_str() == Some("localhost"))
439      });
440
441    let webview = window.add_child(webview_builder, tauri::LogicalPosition::new(0, 0), window.inner_size().unwrap())?;
442    Ok(())
443  });
444```
445  "####
446  )]
447  pub fn on_navigation<F: Fn(&Url) -> bool + Send + 'static>(mut self, f: F) -> Self {
448    self.navigation_handler.replace(Box::new(f));
449    self
450  }
451
452  /// Set a download event handler to be notified when a download is requested or finished.
453  ///
454  /// Returning `false` prevents the download from happening on a [`DownloadEvent::Requested`] event.
455  ///
456  /// # Examples
457  ///
458  #[cfg_attr(
459    feature = "unstable",
460    doc = r####"
461```rust,no_run
462use tauri::{
463  utils::config::{Csp, CspDirectiveSources, WebviewUrl},
464  window::WindowBuilder,
465  webview::{DownloadEvent, WebviewBuilder},
466};
467
468tauri::Builder::default()
469  .setup(|app| {
470    let window = WindowBuilder::new(app, "label").build()?;
471    let webview_builder = WebviewBuilder::new("core", WebviewUrl::App("index.html".into()))
472      .on_download(|webview, event| {
473        match event {
474          DownloadEvent::Requested { url, destination } => {
475            println!("downloading {}", url);
476            *destination = "/home/tauri/target/path".into();
477          }
478          DownloadEvent::Finished { url, path, success } => {
479            println!("downloaded {} to {:?}, success: {}", url, path, success);
480          }
481          _ => (),
482        }
483        // let the download start
484        true
485      });
486
487    let webview = window.add_child(webview_builder, tauri::LogicalPosition::new(0, 0), window.inner_size().unwrap())?;
488    Ok(())
489  });
490```
491  "####
492  )]
493  pub fn on_download<F: Fn(Webview<R>, DownloadEvent<'_>) -> bool + Send + Sync + 'static>(
494    mut self,
495    f: F,
496  ) -> Self {
497    self.download_handler.replace(Arc::new(f));
498    self
499  }
500
501  /// Defines a closure to be executed when a page load event is triggered.
502  /// The event can be either [`PageLoadEvent::Started`] if the page has started loading
503  /// or [`PageLoadEvent::Finished`] when the page finishes loading.
504  ///
505  /// # Examples
506  ///
507  #[cfg_attr(
508    feature = "unstable",
509    doc = r####"
510```rust,no_run
511use tauri::{
512  utils::config::{Csp, CspDirectiveSources, WebviewUrl},
513  window::WindowBuilder,
514  webview::{PageLoadEvent, WebviewBuilder},
515};
516use http::header::HeaderValue;
517use std::collections::HashMap;
518tauri::Builder::default()
519  .setup(|app| {
520    let window = tauri::window::WindowBuilder::new(app, "label").build()?;
521    let webview_builder = WebviewBuilder::new("core", WebviewUrl::App("index.html".into()))
522      .on_page_load(|webview, payload| {
523        match payload.event() {
524          PageLoadEvent::Started => {
525            println!("{} finished loading", payload.url());
526          }
527          PageLoadEvent::Finished => {
528            println!("{} finished loading", payload.url());
529          }
530        }
531      });
532    let webview = window.add_child(webview_builder, tauri::LogicalPosition::new(0, 0), window.inner_size().unwrap())?;
533    Ok(())
534  });
535```
536  "####
537  )]
538  pub fn on_page_load<F: Fn(Webview<R>, PageLoadPayload<'_>) + Send + Sync + 'static>(
539    mut self,
540    f: F,
541  ) -> Self {
542    self.on_page_load_handler.replace(Box::new(f));
543    self
544  }
545
546  pub(crate) fn into_pending_webview<M: Manager<R>>(
547    mut self,
548    manager: &M,
549    window_label: &str,
550  ) -> crate::Result<PendingWebview<EventLoopMessage, R>> {
551    let mut pending = PendingWebview::new(self.webview_attributes, self.label.clone())?;
552    pending.navigation_handler = self.navigation_handler.take();
553    pending.web_resource_request_handler = self.web_resource_request_handler.take();
554
555    if let Some(download_handler) = self.download_handler.take() {
556      let label = pending.label.clone();
557      let manager = manager.manager_owned();
558      pending.download_handler.replace(Arc::new(move |event| {
559        if let Some(w) = manager.get_webview(&label) {
560          download_handler(
561            w,
562            match event {
563              tauri_runtime::webview::DownloadEvent::Requested { url, destination } => {
564                DownloadEvent::Requested { url, destination }
565              }
566              tauri_runtime::webview::DownloadEvent::Finished { url, path, success } => {
567                DownloadEvent::Finished { url, path, success }
568              }
569            },
570          )
571        } else {
572          false
573        }
574      }));
575    }
576
577    let label_ = pending.label.clone();
578    let manager_ = manager.manager_owned();
579    pending
580      .on_page_load_handler
581      .replace(Box::new(move |url, event| {
582        if let Some(w) = manager_.get_webview(&label_) {
583          if let Some(handler) = self.on_page_load_handler.as_ref() {
584            handler(w, PageLoadPayload { url: &url, event });
585          }
586        }
587      }));
588
589    manager
590      .manager()
591      .webview
592      .prepare_webview(manager, pending, window_label)
593  }
594
595  /// Creates a new webview on the given window.
596  #[cfg(desktop)]
597  pub(crate) fn build(
598    self,
599    window: Window<R>,
600    position: Position,
601    size: Size,
602  ) -> crate::Result<Webview<R>> {
603    let app_manager = window.manager();
604
605    let mut pending = self.into_pending_webview(&window, window.label())?;
606
607    pending.webview_attributes.bounds = Some(tauri_runtime::Rect { size, position });
608
609    let use_https_scheme = pending.webview_attributes.use_https_scheme;
610
611    let webview = match &mut window.runtime() {
612      RuntimeOrDispatch::Dispatch(dispatcher) => dispatcher.create_webview(pending),
613      _ => unimplemented!(),
614    }
615    .map(|webview| {
616      app_manager
617        .webview
618        .attach_webview(window.clone(), webview, use_https_scheme)
619    })?;
620
621    Ok(webview)
622  }
623}
624
625/// Webview attributes.
626impl<R: Runtime> WebviewBuilder<R> {
627  /// Sets whether clicking an inactive window also clicks through to the webview.
628  #[must_use]
629  pub fn accept_first_mouse(mut self, accept: bool) -> Self {
630    self.webview_attributes.accept_first_mouse = accept;
631    self
632  }
633
634  /// Adds the provided JavaScript to a list of scripts that should be run after the global object has been created,
635  /// but before the HTML document has been parsed and before any other script included by the HTML document is run.
636  ///
637  /// Since it runs on all top-level document navigations,
638  /// it's recommended to check the `window.location` to guard your script from running on unexpected origins.
639  ///
640  /// This is executed only on the main frame.
641  /// If you only want to run it in all frames, use [Self::initialization_script_for_all_frames] instead.
642  ///
643  /// ## Platform-specific
644  ///
645  /// - **Windows:** scripts are always added to subframes.
646  /// - **Android:** When [addDocumentStartJavaScript] is not supported,
647  ///   we prepend initialization scripts to each HTML head (implementation only supported on custom protocol URLs).
648  ///   For remote URLs, we use [onPageStarted] which is not guaranteed to run before other scripts.
649  ///
650  /// # Examples
651  ///
652  #[cfg_attr(
653    feature = "unstable",
654    doc = r####"
655```rust
656use tauri::{WindowBuilder, Runtime};
657
658const INIT_SCRIPT: &str = r#"
659  if (window.location.origin === 'https://tauri.app') {
660    console.log("hello world from js init script");
661
662    window.__MY_CUSTOM_PROPERTY__ = { foo: 'bar' };
663  }
664"#;
665
666fn main() {
667  tauri::Builder::default()
668    .setup(|app| {
669      let window = tauri::window::WindowBuilder::new(app, "label").build()?;
670      let webview_builder = tauri::webview::WebviewBuilder::new("label", tauri::WebviewUrl::App("index.html".into()))
671        .initialization_script(INIT_SCRIPT);
672      let webview = window.add_child(webview_builder, tauri::LogicalPosition::new(0, 0), window.inner_size().unwrap())?;
673      Ok(())
674    });
675}
676```
677  "####
678  )]
679  ///
680  /// [addDocumentStartJavaScript]: https://developer.android.com/reference/androidx/webkit/WebViewCompat#addDocumentStartJavaScript(android.webkit.WebView,java.lang.String,java.util.Set%3Cjava.lang.String%3E)
681  /// [onPageStarted]: https://developer.android.com/reference/android/webkit/WebViewClient#onPageStarted(android.webkit.WebView,%20java.lang.String,%20android.graphics.Bitmap)
682  #[must_use]
683  pub fn initialization_script(mut self, script: impl Into<String>) -> Self {
684    self
685      .webview_attributes
686      .initialization_scripts
687      .push(InitializationScript {
688        script: script.into(),
689        for_main_frame_only: true,
690      });
691    self
692  }
693
694  /// Adds the provided JavaScript to a list of scripts that should be run after the global object has been created,
695  /// but before the HTML document has been parsed and before any other script included by the HTML document is run.
696  ///
697  /// Since it runs on all top-level document navigations and also child frame page navigations,
698  /// it's recommended to check the `window.location` to guard your script from running on unexpected origins.
699  ///
700  /// This is executed on all frames (main frame and also sub frames).
701  /// If you only want to run the script in the main frame, use [Self::initialization_script] instead.
702  ///
703  /// ## Platform-specific
704  ///
705  /// - **Android:** When [addDocumentStartJavaScript] is not supported,
706  ///   we prepend initialization scripts to each HTML head (implementation only supported on custom protocol URLs).
707  ///   For remote URLs, we use [onPageStarted] which is not guaranteed to run before other scripts.
708  ///
709  /// # Examples
710  ///
711  #[cfg_attr(
712    feature = "unstable",
713    doc = r####"
714```rust
715use tauri::{WindowBuilder, Runtime};
716
717const INIT_SCRIPT: &str = r#"
718  if (window.location.origin === 'https://tauri.app') {
719    console.log("hello world from js init script");
720
721    window.__MY_CUSTOM_PROPERTY__ = { foo: 'bar' };
722  }
723"#;
724
725fn main() {
726  tauri::Builder::default()
727    .setup(|app| {
728      let window = tauri::window::WindowBuilder::new(app, "label").build()?;
729      let webview_builder = tauri::webview::WebviewBuilder::new("label", tauri::WebviewUrl::App("index.html".into()))
730        .initialization_script_for_all_frames(INIT_SCRIPT);
731      let webview = window.add_child(webview_builder, tauri::LogicalPosition::new(0, 0), window.inner_size().unwrap())?;
732      Ok(())
733    });
734}
735```
736  "####
737  )]
738  ///
739  /// [addDocumentStartJavaScript]: https://developer.android.com/reference/androidx/webkit/WebViewCompat#addDocumentStartJavaScript(android.webkit.WebView,java.lang.String,java.util.Set%3Cjava.lang.String%3E)
740  /// [onPageStarted]: https://developer.android.com/reference/android/webkit/WebViewClient#onPageStarted(android.webkit.WebView,%20java.lang.String,%20android.graphics.Bitmap)
741  #[must_use]
742  pub fn initialization_script_for_all_frames(mut self, script: impl Into<String>) -> Self {
743    self
744      .webview_attributes
745      .initialization_scripts
746      .push(InitializationScript {
747        script: script.into(),
748        for_main_frame_only: false,
749      });
750    self
751  }
752
753  /// Set the user agent for the webview
754  #[must_use]
755  pub fn user_agent(mut self, user_agent: &str) -> Self {
756    self.webview_attributes.user_agent = Some(user_agent.to_string());
757    self
758  }
759
760  /// Set additional arguments for the webview.
761  ///
762  /// ## Platform-specific
763  ///
764  /// - **macOS / Linux / Android / iOS**: Unsupported.
765  ///
766  /// ## Warning
767  ///
768  /// By default wry passes `--disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection`
769  /// so if you use this method, you also need to disable these components by yourself if you want.
770  #[must_use]
771  pub fn additional_browser_args(mut self, additional_args: &str) -> Self {
772    self.webview_attributes.additional_browser_args = Some(additional_args.to_string());
773    self
774  }
775
776  /// Data directory for the webview.
777  #[must_use]
778  pub fn data_directory(mut self, data_directory: PathBuf) -> Self {
779    self
780      .webview_attributes
781      .data_directory
782      .replace(data_directory);
783    self
784  }
785
786  /// Disables the drag and drop handler. This is required to use HTML5 drag and drop APIs on the frontend on Windows.
787  #[must_use]
788  pub fn disable_drag_drop_handler(mut self) -> Self {
789    self.webview_attributes.drag_drop_handler_enabled = false;
790    self
791  }
792
793  /// Enables clipboard access for the page rendered on **Linux** and **Windows**.
794  ///
795  /// **macOS** doesn't provide such method and is always enabled by default,
796  /// but you still need to add menu item accelerators to use shortcuts.
797  #[must_use]
798  pub fn enable_clipboard_access(mut self) -> Self {
799    self.webview_attributes.clipboard = true;
800    self
801  }
802
803  /// Enable or disable incognito mode for the WebView.
804  ///
805  ///  ## Platform-specific:
806  ///
807  ///  - **Windows**: Requires WebView2 Runtime version 101.0.1210.39 or higher, does nothing on older versions,
808  ///    see https://learn.microsoft.com/en-us/microsoft-edge/webview2/release-notes/archive?tabs=dotnetcsharp#10121039
809  ///  - **Android**: Unsupported.
810  ///  - **macOS / iOS**: Uses the nonPersistent DataStore
811  #[must_use]
812  pub fn incognito(mut self, incognito: bool) -> Self {
813    self.webview_attributes.incognito = incognito;
814    self
815  }
816
817  /// Set a proxy URL for the WebView for all network requests.
818  ///
819  /// Must be either a `http://` or a `socks5://` URL.
820  ///
821  /// ## Platform-specific
822  ///
823  /// - **macOS**: Requires the `macos-proxy` feature flag and only compiles for macOS 14+.
824  #[must_use]
825  pub fn proxy_url(mut self, url: Url) -> Self {
826    self.webview_attributes.proxy_url = Some(url);
827    self
828  }
829
830  /// Enable or disable transparency for the WebView.
831  #[cfg(any(not(target_os = "macos"), feature = "macos-private-api"))]
832  #[cfg_attr(
833    docsrs,
834    doc(cfg(any(not(target_os = "macos"), feature = "macos-private-api")))
835  )]
836  #[must_use]
837  pub fn transparent(mut self, transparent: bool) -> Self {
838    self.webview_attributes.transparent = transparent;
839    self
840  }
841
842  /// Whether the webview should be focused or not.
843  #[must_use]
844  pub fn focused(mut self, focus: bool) -> Self {
845    self.webview_attributes.focus = focus;
846    self
847  }
848
849  /// Sets the webview to automatically grow and shrink its size and position when the parent window resizes.
850  #[must_use]
851  pub fn auto_resize(mut self) -> Self {
852    self.webview_attributes.auto_resize = true;
853    self
854  }
855
856  /// Whether page zooming by hotkeys and mousewheel should be enabled or not.
857  ///
858  /// ## Platform-specific:
859  ///
860  /// - **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.
861  /// - **MacOS / Linux**: Injects a polyfill that zooms in and out with `Ctrl/Cmd + [- = +]` hotkeys or mousewheel events,
862  ///   20% in each step, ranging from 20% to 1000%. Requires `core:webview:allow-set-webview-zoom` permission
863  ///
864  /// - **Android / iOS**: Unsupported.
865  #[must_use]
866  pub fn zoom_hotkeys_enabled(mut self, enabled: bool) -> Self {
867    self.webview_attributes.zoom_hotkeys_enabled = enabled;
868    self
869  }
870
871  /// Whether browser extensions can be installed for the webview process
872  ///
873  /// ## Platform-specific:
874  ///
875  /// - **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)
876  /// - **MacOS / Linux / iOS / Android** - Unsupported.
877  #[must_use]
878  pub fn browser_extensions_enabled(mut self, enabled: bool) -> Self {
879    self.webview_attributes.browser_extensions_enabled = enabled;
880    self
881  }
882
883  /// Set the path from which to load extensions from. Extensions stored in this path should be unpacked Chrome extensions on Windows, and compiled `.so` extensions on Linux.
884  ///
885  /// ## Platform-specific:
886  ///
887  /// - **Windows**: Browser extensions must first be enabled. See [`browser_extensions_enabled`](Self::browser_extensions_enabled)
888  /// - **MacOS / iOS / Android** - Unsupported.
889  #[must_use]
890  pub fn extensions_path(mut self, path: impl AsRef<Path>) -> Self {
891    self.webview_attributes.extensions_path = Some(path.as_ref().to_path_buf());
892    self
893  }
894
895  /// Initialize the WebView with a custom data store identifier.
896  /// Can be used as a replacement for data_directory not being available in WKWebView.
897  ///
898  /// - **macOS / iOS**: Available on macOS >= 14 and iOS >= 17
899  /// - **Windows / Linux / Android**: Unsupported.
900  ///
901  /// Note: Enable incognito mode to use the `nonPersistent` DataStore.
902  #[must_use]
903  pub fn data_store_identifier(mut self, data_store_identifier: [u8; 16]) -> Self {
904    self.webview_attributes.data_store_identifier = Some(data_store_identifier);
905    self
906  }
907
908  /// Sets whether the custom protocols should use `https://<scheme>.localhost` instead of the default `http://<scheme>.localhost` on Windows and Android. Defaults to `false`.
909  ///
910  /// ## Note
911  ///
912  /// 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.
913  ///
914  /// ## Warning
915  ///
916  /// 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.
917  #[must_use]
918  pub fn use_https_scheme(mut self, enabled: bool) -> Self {
919    self.webview_attributes.use_https_scheme = enabled;
920    self
921  }
922
923  /// Whether web inspector, which is usually called browser devtools, is enabled or not. Enabled by default.
924  ///
925  /// This API works in **debug** builds, but requires `devtools` feature flag to enable it in **release** builds.
926  ///
927  /// ## Platform-specific
928  ///
929  /// - macOS: This will call private functions on **macOS**
930  /// - Android: Open `chrome://inspect/#devices` in Chrome to get the devtools window. Wry's `WebView` devtools API isn't supported on Android.
931  /// - iOS: Open Safari > Develop > [Your Device Name] > [Your WebView] to get the devtools window.
932  #[must_use]
933  pub fn devtools(mut self, enabled: bool) -> Self {
934    self.webview_attributes.devtools.replace(enabled);
935    self
936  }
937
938  /// Set the webview background color.
939  ///
940  /// ## Platform-specific:
941  ///
942  /// - **macOS / iOS**: Not implemented.
943  /// - **Windows**: On Windows 7, alpha channel is ignored.
944  /// - **Windows**: On Windows 8 and newer, if alpha channel is not `0`, it will be ignored.
945  #[must_use]
946  pub fn background_color(mut self, color: Color) -> Self {
947    self.webview_attributes.background_color = Some(color);
948    self
949  }
950
951  /// Change the default background throttling behaviour.
952  ///
953  /// By default, browsers use a suspend policy that will throttle timers and even unload
954  /// the whole tab (view) to free resources after roughly 5 minutes when a view became
955  /// minimized or hidden. This will pause all tasks until the documents visibility state
956  /// changes back from hidden to visible by bringing the view back to the foreground.
957  ///
958  /// ## Platform-specific
959  ///
960  /// - **Linux / Windows / Android**: Unsupported. Workarounds like a pending WebLock transaction might suffice.
961  /// - **iOS**: Supported since version 17.0+.
962  /// - **macOS**: Supported since version 14.0+.
963  ///
964  /// see https://github.com/tauri-apps/tauri/issues/5250#issuecomment-2569380578
965  #[must_use]
966  pub fn background_throttling(mut self, policy: BackgroundThrottlingPolicy) -> Self {
967    self.webview_attributes.background_throttling = Some(policy);
968    self
969  }
970
971  /// Whether JavaScript should be disabled.
972  #[must_use]
973  pub fn disable_javascript(mut self) -> Self {
974    self.webview_attributes.javascript_disabled = true;
975    self
976  }
977
978  /// Whether to show a link preview when long pressing on links. Available on macOS and iOS only.
979  ///
980  /// Default is true.
981  ///
982  /// See https://docs.rs/objc2-web-kit/latest/objc2_web_kit/struct.WKWebView.html#method.allowsLinkPreview
983  ///
984  /// ## Platform-specific
985  ///
986  /// - **Linux / Windows / Android:** Unsupported.
987  #[cfg(target_os = "macos")]
988  #[must_use]
989  pub fn allow_link_preview(mut self, allow_link_preview: bool) -> Self {
990    self.webview_attributes = self
991      .webview_attributes
992      .allow_link_preview(allow_link_preview);
993    self
994  }
995
996  /// Allows overriding the the keyboard accessory view on iOS.
997  /// Returning `None` effectively removes the view.
998  ///
999  /// The closure parameter is the webview instance.
1000  ///
1001  /// The accessory view is the view that appears above the keyboard when a text input element is focused.
1002  /// It usually displays a view with "Done", "Next" buttons.
1003  ///
1004  /// # Stability
1005  ///
1006  /// This relies on [`objc2_ui_kit`] which does not provide a stable API yet, so it can receive breaking changes in minor releases.
1007  #[cfg(target_os = "ios")]
1008  pub fn with_input_accessory_view_builder<
1009    F: Fn(&objc2_ui_kit::UIView) -> Option<objc2::rc::Retained<objc2_ui_kit::UIView>>
1010      + Send
1011      + Sync
1012      + 'static,
1013  >(
1014    mut self,
1015    builder: F,
1016  ) -> Self {
1017    self
1018      .webview_attributes
1019      .input_accessory_view_builder
1020      .replace(tauri_runtime::webview::InputAccessoryViewBuilder::new(
1021        Box::new(builder),
1022      ));
1023    self
1024  }
1025}
1026
1027/// Webview.
1028#[default_runtime(crate::Wry, wry)]
1029pub struct Webview<R: Runtime> {
1030  pub(crate) window: Arc<Mutex<Window<R>>>,
1031  /// The webview created by the runtime.
1032  pub(crate) webview: DetachedWebview<EventLoopMessage, R>,
1033  /// The manager to associate this webview with.
1034  pub(crate) manager: Arc<AppManager<R>>,
1035  pub(crate) app_handle: AppHandle<R>,
1036  pub(crate) resources_table: Arc<Mutex<ResourceTable>>,
1037  use_https_scheme: bool,
1038}
1039
1040impl<R: Runtime> std::fmt::Debug for Webview<R> {
1041  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1042    f.debug_struct("Window")
1043      .field("window", &self.window.lock().unwrap())
1044      .field("webview", &self.webview)
1045      .field("use_https_scheme", &self.use_https_scheme)
1046      .finish()
1047  }
1048}
1049
1050impl<R: Runtime> Clone for Webview<R> {
1051  fn clone(&self) -> Self {
1052    Self {
1053      window: self.window.clone(),
1054      webview: self.webview.clone(),
1055      manager: self.manager.clone(),
1056      app_handle: self.app_handle.clone(),
1057      resources_table: self.resources_table.clone(),
1058      use_https_scheme: self.use_https_scheme,
1059    }
1060  }
1061}
1062
1063impl<R: Runtime> Hash for Webview<R> {
1064  /// Only use the [`Webview`]'s label to represent its hash.
1065  fn hash<H: Hasher>(&self, state: &mut H) {
1066    self.webview.label.hash(state)
1067  }
1068}
1069
1070impl<R: Runtime> Eq for Webview<R> {}
1071impl<R: Runtime> PartialEq for Webview<R> {
1072  /// Only use the [`Webview`]'s label to compare equality.
1073  fn eq(&self, other: &Self) -> bool {
1074    self.webview.label.eq(&other.webview.label)
1075  }
1076}
1077
1078/// Base webview functions.
1079impl<R: Runtime> Webview<R> {
1080  /// Create a new webview that is attached to the window.
1081  pub(crate) fn new(
1082    window: Window<R>,
1083    webview: DetachedWebview<EventLoopMessage, R>,
1084    use_https_scheme: bool,
1085  ) -> Self {
1086    Self {
1087      manager: window.manager.clone(),
1088      app_handle: window.app_handle.clone(),
1089      window: Arc::new(Mutex::new(window)),
1090      webview,
1091      resources_table: Default::default(),
1092      use_https_scheme,
1093    }
1094  }
1095
1096  /// Initializes a webview builder with the given window label and URL to load on the webview.
1097  ///
1098  /// Data URLs are only supported with the `webview-data-url` feature flag.
1099  #[cfg(feature = "unstable")]
1100  #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))]
1101  pub fn builder<L: Into<String>>(label: L, url: WebviewUrl) -> WebviewBuilder<R> {
1102    WebviewBuilder::new(label.into(), url)
1103  }
1104
1105  /// Runs the given closure on the main thread.
1106  pub fn run_on_main_thread<F: FnOnce() + Send + 'static>(&self, f: F) -> crate::Result<()> {
1107    self
1108      .webview
1109      .dispatcher
1110      .run_on_main_thread(f)
1111      .map_err(Into::into)
1112  }
1113
1114  /// The webview label.
1115  pub fn label(&self) -> &str {
1116    &self.webview.label
1117  }
1118
1119  /// Whether the webview was configured to use the HTTPS scheme or not.
1120  pub(crate) fn use_https_scheme(&self) -> bool {
1121    self.use_https_scheme
1122  }
1123
1124  /// Registers a window event listener.
1125  pub fn on_webview_event<F: Fn(&WebviewEvent) + Send + 'static>(&self, f: F) {
1126    self
1127      .webview
1128      .dispatcher
1129      .on_webview_event(move |event| f(&event.clone().into()));
1130  }
1131
1132  /// Resolves the given command scope for this webview on the currently loaded URL.
1133  ///
1134  /// If the command is not allowed, returns None.
1135  ///
1136  /// If the scope cannot be deserialized to the given type, an error is returned.
1137  ///
1138  /// In a command context this can be directly resolved from the command arguments via [CommandScope]:
1139  ///
1140  /// ```
1141  /// use tauri::ipc::CommandScope;
1142  ///
1143  /// #[derive(Debug, serde::Deserialize)]
1144  /// struct ScopeType {
1145  ///   some_value: String,
1146  /// }
1147  /// #[tauri::command]
1148  /// fn my_command(scope: CommandScope<ScopeType>) {
1149  ///   // check scope
1150  /// }
1151  /// ```
1152  ///
1153  /// # Examples
1154  ///
1155  /// ```
1156  /// use tauri::Manager;
1157  ///
1158  /// #[derive(Debug, serde::Deserialize)]
1159  /// struct ScopeType {
1160  ///   some_value: String,
1161  /// }
1162  ///
1163  /// tauri::Builder::default()
1164  ///   .setup(|app| {
1165  ///     let webview = app.get_webview_window("main").unwrap();
1166  ///     let scope = webview.resolve_command_scope::<ScopeType>("my-plugin", "read");
1167  ///     Ok(())
1168  ///   });
1169  /// ```
1170  pub fn resolve_command_scope<T: ScopeObject>(
1171    &self,
1172    plugin: &str,
1173    command: &str,
1174  ) -> crate::Result<Option<ResolvedScope<T>>> {
1175    let current_url = self.url()?;
1176    let is_local = self.is_local_url(&current_url);
1177    let origin = if is_local {
1178      Origin::Local
1179    } else {
1180      Origin::Remote { url: current_url }
1181    };
1182
1183    let cmd_name = format!("plugin:{plugin}|{command}");
1184    let resolved_access = self
1185      .manager()
1186      .runtime_authority
1187      .lock()
1188      .unwrap()
1189      .resolve_access(&cmd_name, self.window().label(), self.label(), &origin);
1190
1191    if let Some(access) = resolved_access {
1192      let scope_ids = access
1193        .iter()
1194        .filter_map(|cmd| cmd.scope_id)
1195        .collect::<Vec<_>>();
1196
1197      let command_scope = CommandScope::resolve(self, scope_ids)?;
1198      let global_scope = GlobalScope::resolve(self, plugin)?;
1199
1200      Ok(Some(ResolvedScope {
1201        global_scope,
1202        command_scope,
1203      }))
1204    } else {
1205      Ok(None)
1206    }
1207  }
1208}
1209
1210/// Desktop webview setters and actions.
1211#[cfg(desktop)]
1212impl<R: Runtime> Webview<R> {
1213  /// Opens the dialog to prints the contents of the webview.
1214  /// Currently only supported on macOS on `wry`.
1215  /// `window.print()` works on all platforms.
1216  pub fn print(&self) -> crate::Result<()> {
1217    self.webview.dispatcher.print().map_err(Into::into)
1218  }
1219
1220  /// Get the cursor position relative to the top-left hand corner of the desktop.
1221  ///
1222  /// Note that the top-left hand corner of the desktop is not necessarily the same as the screen.
1223  /// If the user uses a desktop with multiple monitors,
1224  /// the top-left hand corner of the desktop is the top-left hand corner of the main monitor on Windows and macOS
1225  /// or the top-left of the leftmost monitor on X11.
1226  ///
1227  /// The coordinates can be negative if the top-left hand corner of the window is outside of the visible screen region.
1228  pub fn cursor_position(&self) -> crate::Result<PhysicalPosition<f64>> {
1229    self.app_handle.cursor_position()
1230  }
1231
1232  /// Closes this webview.
1233  pub fn close(&self) -> crate::Result<()> {
1234    self.webview.dispatcher.close()?;
1235    self.manager().on_webview_close(self.label());
1236    Ok(())
1237  }
1238
1239  /// Resizes this webview.
1240  pub fn set_bounds(&self, bounds: tauri_runtime::Rect) -> crate::Result<()> {
1241    self
1242      .webview
1243      .dispatcher
1244      .set_bounds(bounds)
1245      .map_err(Into::into)
1246  }
1247
1248  /// Resizes this webview.
1249  pub fn set_size<S: Into<Size>>(&self, size: S) -> crate::Result<()> {
1250    self
1251      .webview
1252      .dispatcher
1253      .set_size(size.into())
1254      .map_err(Into::into)
1255  }
1256
1257  /// Sets this webviews's position.
1258  pub fn set_position<Pos: Into<Position>>(&self, position: Pos) -> crate::Result<()> {
1259    self
1260      .webview
1261      .dispatcher
1262      .set_position(position.into())
1263      .map_err(Into::into)
1264  }
1265
1266  /// Focus the webview.
1267  pub fn set_focus(&self) -> crate::Result<()> {
1268    self.webview.dispatcher.set_focus().map_err(Into::into)
1269  }
1270
1271  /// Hide the webview.
1272  pub fn hide(&self) -> crate::Result<()> {
1273    self.webview.dispatcher.hide().map_err(Into::into)
1274  }
1275
1276  /// Show the webview.
1277  pub fn show(&self) -> crate::Result<()> {
1278    self.webview.dispatcher.show().map_err(Into::into)
1279  }
1280
1281  /// Move the webview to the given window.
1282  pub fn reparent(&self, window: &Window<R>) -> crate::Result<()> {
1283    #[cfg(not(feature = "unstable"))]
1284    {
1285      if self.window_ref().is_webview_window() || window.is_webview_window() {
1286        return Err(crate::Error::CannotReparentWebviewWindow);
1287      }
1288    }
1289
1290    *self.window.lock().unwrap() = window.clone();
1291    self.webview.dispatcher.reparent(window.window.id)?;
1292    Ok(())
1293  }
1294
1295  /// Sets whether the webview should automatically grow and shrink its size and position when the parent window resizes.
1296  pub fn set_auto_resize(&self, auto_resize: bool) -> crate::Result<()> {
1297    self
1298      .webview
1299      .dispatcher
1300      .set_auto_resize(auto_resize)
1301      .map_err(Into::into)
1302  }
1303
1304  /// Returns the bounds of the webviews's client area.
1305  pub fn bounds(&self) -> crate::Result<tauri_runtime::Rect> {
1306    self.webview.dispatcher.bounds().map_err(Into::into)
1307  }
1308
1309  /// Returns the webview position.
1310  ///
1311  /// - For child webviews, returns the position of the top-left hand corner of the webviews's client area relative to the top-left hand corner of the parent window.
1312  /// - For webview window, returns the inner position of the window.
1313  pub fn position(&self) -> crate::Result<PhysicalPosition<i32>> {
1314    self.webview.dispatcher.position().map_err(Into::into)
1315  }
1316
1317  /// Returns the physical size of the webviews's client area.
1318  pub fn size(&self) -> crate::Result<PhysicalSize<u32>> {
1319    self.webview.dispatcher.size().map_err(Into::into)
1320  }
1321}
1322
1323/// Webview APIs.
1324impl<R: Runtime> Webview<R> {
1325  /// The window that is hosting this webview.
1326  pub fn window(&self) -> Window<R> {
1327    self.window.lock().unwrap().clone()
1328  }
1329
1330  /// A reference to the window that is hosting this webview.
1331  pub fn window_ref(&self) -> MutexGuard<'_, Window<R>> {
1332    self.window.lock().unwrap()
1333  }
1334
1335  pub(crate) fn window_label(&self) -> String {
1336    self.window_ref().label().to_string()
1337  }
1338
1339  /// Executes a closure, providing it with the webview handle that is specific to the current platform.
1340  ///
1341  /// The closure is executed on the main thread.
1342  ///
1343  /// Note that `webview2-com`, `webkit2gtk`, `objc2_web_kit` and similar crates may be updated in minor releases of Tauri.
1344  /// Therefore it's recommended to pin Tauri to at least a minor version when you're using `with_webview`.
1345  ///
1346  /// # Examples
1347  ///
1348  #[cfg_attr(
1349    feature = "unstable",
1350    doc = r####"
1351```rust,no_run
1352use tauri::Manager;
1353
1354fn main() {
1355  tauri::Builder::default()
1356    .setup(|app| {
1357      let main_webview = app.get_webview("main").unwrap();
1358      main_webview.with_webview(|webview| {
1359        #[cfg(target_os = "linux")]
1360        {
1361          // see <https://docs.rs/webkit2gtk/2.0.0/webkit2gtk/struct.WebView.html>
1362          // and <https://docs.rs/webkit2gtk/2.0.0/webkit2gtk/trait.WebViewExt.html>
1363          use webkit2gtk::WebViewExt;
1364          webview.inner().set_zoom_level(4.);
1365        }
1366
1367        #[cfg(windows)]
1368        unsafe {
1369          // see https://docs.rs/webview2-com/0.19.1/webview2_com/Microsoft/Web/WebView2/Win32/struct.ICoreWebView2Controller.html
1370          webview.controller().SetZoomFactor(4.).unwrap();
1371        }
1372
1373        #[cfg(target_os = "macos")]
1374        unsafe {
1375          let view: &objc2_web_kit::WKWebView = &*webview.inner().cast();
1376          let controller: &objc2_web_kit::WKUserContentController = &*webview.controller().cast();
1377          let window: &objc2_app_kit::NSWindow = &*webview.ns_window().cast();
1378
1379          view.setPageZoom(4.);
1380          controller.removeAllUserScripts();
1381          let bg_color = objc2_app_kit::NSColor::colorWithDeviceRed_green_blue_alpha(0.5, 0.2, 0.4, 1.);
1382          window.setBackgroundColor(Some(&bg_color));
1383        }
1384
1385        #[cfg(target_os = "android")]
1386        {
1387          use jni::objects::JValue;
1388          webview.jni_handle().exec(|env, _, webview| {
1389            env.call_method(webview, "zoomBy", "(F)V", &[JValue::Float(4.)]).unwrap();
1390          })
1391        }
1392      });
1393      Ok(())
1394  });
1395}
1396```
1397  "####
1398  )]
1399  #[cfg(feature = "wry")]
1400  #[cfg_attr(docsrs, doc(feature = "wry"))]
1401  pub fn with_webview<F: FnOnce(PlatformWebview) + Send + 'static>(
1402    &self,
1403    f: F,
1404  ) -> crate::Result<()> {
1405    self
1406      .webview
1407      .dispatcher
1408      .with_webview(|w| f(PlatformWebview(*w.downcast().unwrap())))
1409      .map_err(Into::into)
1410  }
1411
1412  /// Returns the current url of the webview.
1413  pub fn url(&self) -> crate::Result<Url> {
1414    self
1415      .webview
1416      .dispatcher
1417      .url()
1418      .map(|url| url.parse().map_err(crate::Error::InvalidUrl))?
1419  }
1420
1421  /// Navigates the webview to the defined url.
1422  pub fn navigate(&self, url: Url) -> crate::Result<()> {
1423    self.webview.dispatcher.navigate(url).map_err(Into::into)
1424  }
1425
1426  /// Reloads the current page.
1427  pub fn reload(&self) -> crate::Result<()> {
1428    self.webview.dispatcher.reload().map_err(Into::into)
1429  }
1430
1431  fn is_local_url(&self, current_url: &Url) -> bool {
1432    let uses_https = current_url.scheme() == "https";
1433
1434    // if from `tauri://` custom protocol
1435    ({
1436      let protocol_url = self.manager().protocol_url(uses_https);
1437      current_url.scheme() == protocol_url.scheme()
1438      && current_url.domain() == protocol_url.domain()
1439    }) ||
1440
1441    // or if relative to `devUrl` or `frontendDist`
1442      self
1443          .manager()
1444          .get_url(uses_https)
1445          .make_relative(current_url)
1446          .is_some()
1447
1448      // or from a custom protocol registered by the user
1449      || ({
1450        let scheme = current_url.scheme();
1451        let protocols = self.manager().webview.uri_scheme_protocols.lock().unwrap();
1452
1453        #[cfg(all(not(windows), not(target_os = "android")))]
1454        let local = protocols.contains_key(scheme);
1455
1456        // on window and android, custom protocols are `http://<protocol-name>.path/to/route`
1457        // so we check using the first part of the domain
1458        #[cfg(any(windows, target_os = "android"))]
1459        let local = {
1460          let protocol_url = self.manager().protocol_url(uses_https);
1461          let maybe_protocol = current_url
1462            .domain()
1463            .and_then(|d| d .split_once('.'))
1464            .unwrap_or_default()
1465            .0;
1466
1467          protocols.contains_key(maybe_protocol) && scheme == protocol_url.scheme()
1468        };
1469
1470        local
1471      })
1472  }
1473
1474  /// Handles this window receiving an [`InvokeRequest`].
1475  pub fn on_message(self, request: InvokeRequest, responder: Box<OwnedInvokeResponder<R>>) {
1476    let manager = self.manager_owned();
1477    let is_local = self.is_local_url(&request.url);
1478
1479    // ensure the passed key matches what our manager should have injected
1480    let expected = manager.invoke_key();
1481    if request.invoke_key != expected {
1482      #[cfg(feature = "tracing")]
1483      tracing::error!(
1484        "__TAURI_INVOKE_KEY__ expected {expected} but received {}",
1485        request.invoke_key
1486      );
1487
1488      #[cfg(not(feature = "tracing"))]
1489      eprintln!(
1490        "__TAURI_INVOKE_KEY__ expected {expected} but received {}",
1491        request.invoke_key
1492      );
1493
1494      return;
1495    }
1496
1497    let resolver = InvokeResolver::new(
1498      self.clone(),
1499      Arc::new(Mutex::new(Some(Box::new(
1500        move |webview: Webview<R>, cmd, response, callback, error| {
1501          responder(webview, cmd, response, callback, error);
1502        },
1503      )))),
1504      request.cmd.clone(),
1505      request.callback,
1506      request.error,
1507    );
1508
1509    #[cfg(mobile)]
1510    let app_handle = self.app_handle.clone();
1511
1512    let message = InvokeMessage::new(
1513      self,
1514      manager.state(),
1515      request.cmd.to_string(),
1516      request.body,
1517      request.headers,
1518    );
1519
1520    let acl_origin = if is_local {
1521      Origin::Local
1522    } else {
1523      Origin::Remote {
1524        url: request.url.clone(),
1525      }
1526    };
1527    let (resolved_acl, has_app_acl_manifest) = {
1528      let runtime_authority = manager.runtime_authority.lock().unwrap();
1529      let acl = runtime_authority.resolve_access(
1530        &request.cmd,
1531        message.webview.window_ref().label(),
1532        message.webview.label(),
1533        &acl_origin,
1534      );
1535      (acl, runtime_authority.has_app_manifest())
1536    };
1537
1538    let mut invoke = Invoke {
1539      message,
1540      resolver: resolver.clone(),
1541      acl: resolved_acl,
1542    };
1543
1544    let plugin_command = request.cmd.strip_prefix("plugin:").map(|raw_command| {
1545      let mut tokens = raw_command.split('|');
1546      // safe to unwrap: split always has a least one item
1547      let plugin = tokens.next().unwrap();
1548      let command = tokens.next().map(|c| c.to_string()).unwrap_or_default();
1549      (plugin, command)
1550    });
1551
1552    // we only check ACL on plugin commands or if the app defined its ACL manifest
1553    if (plugin_command.is_some() || has_app_acl_manifest)
1554      // TODO: Remove this special check in v3
1555      && request.cmd != crate::ipc::channel::FETCH_CHANNEL_DATA_COMMAND
1556      && invoke.acl.is_none()
1557    {
1558      #[cfg(debug_assertions)]
1559      {
1560        let (key, command_name) = plugin_command
1561          .clone()
1562          .unwrap_or_else(|| (tauri_utils::acl::APP_ACL_KEY, request.cmd.clone()));
1563        invoke.resolver.reject(
1564          manager
1565            .runtime_authority
1566            .lock()
1567            .unwrap()
1568            .resolve_access_message(
1569              key,
1570              &command_name,
1571              invoke.message.webview.window().label(),
1572              invoke.message.webview.label(),
1573              &acl_origin,
1574            ),
1575        );
1576      }
1577      #[cfg(not(debug_assertions))]
1578      invoke
1579        .resolver
1580        .reject(format!("Command {} not allowed by ACL", request.cmd));
1581      return;
1582    }
1583
1584    if let Some((plugin, command_name)) = plugin_command {
1585      invoke.message.command = command_name;
1586
1587      let command = invoke.message.command.clone();
1588
1589      #[cfg(mobile)]
1590      let message = invoke.message.clone();
1591
1592      #[allow(unused_mut)]
1593      let mut handled = manager.extend_api(plugin, invoke);
1594
1595      #[cfg(mobile)]
1596      {
1597        if !handled {
1598          handled = true;
1599
1600          fn load_channels<R: Runtime>(payload: &serde_json::Value, webview: &Webview<R>) {
1601            use std::str::FromStr;
1602
1603            if let serde_json::Value::Object(map) = payload {
1604              for v in map.values() {
1605                if let serde_json::Value::String(s) = v {
1606                  let _ = crate::ipc::JavaScriptChannelId::from_str(s)
1607                    .map(|id| id.channel_on::<R, ()>(webview.clone()));
1608                }
1609              }
1610            }
1611          }
1612
1613          let payload = message.payload.into_json();
1614          // initialize channels
1615          load_channels(&payload, &message.webview);
1616
1617          let resolver_ = resolver.clone();
1618          if let Err(e) = crate::plugin::mobile::run_command(
1619            plugin,
1620            &app_handle,
1621            heck::AsLowerCamelCase(message.command).to_string(),
1622            payload,
1623            move |response| match response {
1624              Ok(r) => resolver_.resolve(r),
1625              Err(e) => resolver_.reject(e),
1626            },
1627          ) {
1628            resolver.reject(e.to_string());
1629            return;
1630          }
1631        }
1632      }
1633
1634      if !handled {
1635        resolver.reject(format!("Command {command} not found"));
1636      }
1637    } else {
1638      let command = invoke.message.command.clone();
1639      let handled = manager.run_invoke_handler(invoke);
1640      if !handled {
1641        resolver.reject(format!("Command {command} not found"));
1642      }
1643    }
1644  }
1645
1646  /// Evaluates JavaScript on this window.
1647  pub fn eval(&self, js: impl Into<String>) -> crate::Result<()> {
1648    self
1649      .webview
1650      .dispatcher
1651      .eval_script(js.into())
1652      .map_err(Into::into)
1653  }
1654
1655  /// Register a JS event listener and return its identifier.
1656  pub(crate) fn listen_js(
1657    &self,
1658    event: EventName<&str>,
1659    target: EventTarget,
1660    handler: CallbackFn,
1661  ) -> crate::Result<EventId> {
1662    let listeners = self.manager().listeners();
1663
1664    let id = listeners.next_event_id();
1665
1666    self.eval(crate::event::listen_js_script(
1667      listeners.listeners_object_name(),
1668      &serde_json::to_string(&target)?,
1669      event,
1670      id,
1671      &format!("window['_{}']", handler.0),
1672    ))?;
1673
1674    listeners.listen_js(event, self.label(), target, id);
1675
1676    Ok(id)
1677  }
1678
1679  /// Unregister a JS event listener.
1680  pub(crate) fn unlisten_js(&self, event: EventName<&str>, id: EventId) -> crate::Result<()> {
1681    let listeners = self.manager().listeners();
1682
1683    self.eval(crate::event::unlisten_js_script(
1684      listeners.listeners_object_name(),
1685      event,
1686      id,
1687    ))?;
1688
1689    listeners.unlisten_js(event, id);
1690
1691    Ok(())
1692  }
1693
1694  pub(crate) fn emit_js(&self, emit_args: &EmitArgs, ids: &[u32]) -> crate::Result<()> {
1695    self.eval(crate::event::emit_js_script(
1696      self.manager().listeners().function_name(),
1697      emit_args,
1698      &serde_json::to_string(ids)?,
1699    )?)?;
1700    Ok(())
1701  }
1702
1703  /// Opens the developer tools window (Web Inspector).
1704  /// The devtools is only enabled on debug builds or with the `devtools` feature flag.
1705  ///
1706  /// ## Platform-specific
1707  ///
1708  /// - **macOS:** Only supported on macOS 10.15+.
1709  ///   This is a private API on macOS, so you cannot use this if your application will be published on the App Store.
1710  ///
1711  /// # Examples
1712  ///
1713  #[cfg_attr(
1714    feature = "unstable",
1715    doc = r####"
1716```rust,no_run
1717use tauri::Manager;
1718tauri::Builder::default()
1719  .setup(|app| {
1720    #[cfg(debug_assertions)]
1721    app.get_webview("main").unwrap().open_devtools();
1722    Ok(())
1723  });
1724```
1725  "####
1726  )]
1727  #[cfg(any(debug_assertions, feature = "devtools"))]
1728  #[cfg_attr(docsrs, doc(cfg(any(debug_assertions, feature = "devtools"))))]
1729  pub fn open_devtools(&self) {
1730    self.webview.dispatcher.open_devtools();
1731  }
1732
1733  /// Closes the developer tools window (Web Inspector).
1734  /// The devtools is only enabled on debug builds or with the `devtools` feature flag.
1735  ///
1736  /// ## Platform-specific
1737  ///
1738  /// - **macOS:** Only supported on macOS 10.15+.
1739  ///   This is a private API on macOS, so you cannot use this if your application will be published on the App Store.
1740  /// - **Windows:** Unsupported.
1741  ///
1742  /// # Examples
1743  ///
1744  #[cfg_attr(
1745    feature = "unstable",
1746    doc = r####"
1747```rust,no_run
1748use tauri::Manager;
1749tauri::Builder::default()
1750  .setup(|app| {
1751    #[cfg(debug_assertions)]
1752    {
1753      let webview = app.get_webview("main").unwrap();
1754      webview.open_devtools();
1755      std::thread::spawn(move || {
1756        std::thread::sleep(std::time::Duration::from_secs(10));
1757        webview.close_devtools();
1758      });
1759    }
1760    Ok(())
1761  });
1762```
1763  "####
1764  )]
1765  #[cfg(any(debug_assertions, feature = "devtools"))]
1766  #[cfg_attr(docsrs, doc(cfg(any(debug_assertions, feature = "devtools"))))]
1767  pub fn close_devtools(&self) {
1768    self.webview.dispatcher.close_devtools();
1769  }
1770
1771  /// Checks if the developer tools window (Web Inspector) is opened.
1772  /// The devtools is only enabled on debug builds or with the `devtools` feature flag.
1773  ///
1774  /// ## Platform-specific
1775  ///
1776  /// - **macOS:** Only supported on macOS 10.15+.
1777  ///   This is a private API on macOS, so you cannot use this if your application will be published on the App Store.
1778  /// - **Windows:** Unsupported.
1779  ///
1780  /// # Examples
1781  ///
1782  #[cfg_attr(
1783    feature = "unstable",
1784    doc = r####"
1785```rust,no_run
1786use tauri::Manager;
1787tauri::Builder::default()
1788  .setup(|app| {
1789    #[cfg(debug_assertions)]
1790    {
1791      let webview = app.get_webview("main").unwrap();
1792      if !webview.is_devtools_open() {
1793        webview.open_devtools();
1794      }
1795    }
1796    Ok(())
1797  });
1798```
1799  "####
1800  )]
1801  #[cfg(any(debug_assertions, feature = "devtools"))]
1802  #[cfg_attr(docsrs, doc(cfg(any(debug_assertions, feature = "devtools"))))]
1803  pub fn is_devtools_open(&self) -> bool {
1804    self
1805      .webview
1806      .dispatcher
1807      .is_devtools_open()
1808      .unwrap_or_default()
1809  }
1810
1811  /// Set the webview zoom level
1812  ///
1813  /// ## Platform-specific:
1814  ///
1815  /// - **Android**: Not supported.
1816  /// - **macOS**: available on macOS 11+ only.
1817  /// - **iOS**: available on iOS 14+ only.
1818  pub fn set_zoom(&self, scale_factor: f64) -> crate::Result<()> {
1819    self
1820      .webview
1821      .dispatcher
1822      .set_zoom(scale_factor)
1823      .map_err(Into::into)
1824  }
1825
1826  /// Specify the webview background color.
1827  ///
1828  /// ## Platfrom-specific:
1829  ///
1830  /// - **macOS / iOS**: Not implemented.
1831  /// - **Windows**:
1832  ///   - On Windows 7, transparency is not supported and the alpha value will be ignored.
1833  ///   - On Windows higher than 7: translucent colors are not supported so any alpha value other than `0` will be replaced by `255`
1834  pub fn set_background_color(&self, color: Option<Color>) -> crate::Result<()> {
1835    self
1836      .webview
1837      .dispatcher
1838      .set_background_color(color)
1839      .map_err(Into::into)
1840  }
1841
1842  /// Clear all browsing data for this webview.
1843  pub fn clear_all_browsing_data(&self) -> crate::Result<()> {
1844    self
1845      .webview
1846      .dispatcher
1847      .clear_all_browsing_data()
1848      .map_err(Into::into)
1849  }
1850
1851  /// Returns all cookies in the runtime's cookie store including HTTP-only and secure cookies.
1852  ///
1853  /// Note that cookies will only be returned for URLs with an http or https scheme.
1854  /// Cookies set through javascript for local files
1855  /// (such as those served from the tauri://) protocol are not currently supported.
1856  ///
1857  /// # Stability
1858  ///
1859  /// The return value of this function leverages [`tauri_runtime::Cookie`] which re-exports the cookie crate.
1860  /// This dependency might receive updates in minor Tauri releases.
1861  ///
1862  /// # Known issues
1863  ///
1864  /// On Windows, this function deadlocks when used in a synchronous command or event handlers, see [the Webview2 issue].
1865  /// You should use `async` commands and separate threads when reading cookies.
1866  ///
1867  /// [the Webview2 issue]: https://github.com/tauri-apps/wry/issues/583
1868  pub fn cookies_for_url(&self, url: Url) -> crate::Result<Vec<Cookie<'static>>> {
1869    self
1870      .webview
1871      .dispatcher
1872      .cookies_for_url(url)
1873      .map_err(Into::into)
1874  }
1875
1876  /// Returns all cookies in the runtime's cookie store for all URLs including HTTP-only and secure cookies.
1877  ///
1878  /// Note that cookies will only be returned for URLs with an http or https scheme.
1879  /// Cookies set through javascript for local files
1880  /// (such as those served from the tauri://) protocol are not currently supported.
1881  ///
1882  /// # Stability
1883  ///
1884  /// The return value of this function leverages [`tauri_runtime::Cookie`] which re-exports the cookie crate.
1885  /// This dependency might receive updates in minor Tauri releases.
1886  ///
1887  /// # Known issues
1888  ///
1889  /// On Windows, this function deadlocks when used in a synchronous command or event handlers, see [the Webview2 issue].
1890  /// You should use `async` commands and separate threads when reading cookies.
1891  ///
1892  /// ## Platform-specific
1893  ///
1894  /// - **Android**: Unsupported, always returns an empty [`Vec`].
1895  ///
1896  /// [the Webview2 issue]: https://github.com/tauri-apps/wry/issues/583
1897  pub fn cookies(&self) -> crate::Result<Vec<Cookie<'static>>> {
1898    self.webview.dispatcher.cookies().map_err(Into::into)
1899  }
1900}
1901
1902impl<R: Runtime> Listener<R> for Webview<R> {
1903  /// Listen to an event on this webview.
1904  ///
1905  /// # Examples
1906  #[cfg_attr(
1907    feature = "unstable",
1908    doc = r####"
1909```
1910use tauri::{Manager, Listener};
1911
1912tauri::Builder::default()
1913  .setup(|app| {
1914    let webview = app.get_webview("main").unwrap();
1915    webview.listen("component-loaded", move |event| {
1916      println!("webview just loaded a component");
1917    });
1918
1919    Ok(())
1920  });
1921```
1922  "####
1923  )]
1924  fn listen<F>(&self, event: impl Into<String>, handler: F) -> EventId
1925  where
1926    F: Fn(Event) + Send + 'static,
1927  {
1928    let event = EventName::new(event.into()).unwrap();
1929    self.manager.listen(
1930      event,
1931      EventTarget::Webview {
1932        label: self.label().to_string(),
1933      },
1934      handler,
1935    )
1936  }
1937
1938  /// Listen to an event on this webview only once.
1939  ///
1940  /// See [`Self::listen`] for more information.
1941  fn once<F>(&self, event: impl Into<String>, handler: F) -> EventId
1942  where
1943    F: FnOnce(Event) + Send + 'static,
1944  {
1945    let event = EventName::new(event.into()).unwrap();
1946    self.manager.once(
1947      event,
1948      EventTarget::Webview {
1949        label: self.label().to_string(),
1950      },
1951      handler,
1952    )
1953  }
1954
1955  /// Unlisten to an event on this webview.
1956  ///
1957  /// # Examples
1958  #[cfg_attr(
1959    feature = "unstable",
1960    doc = r####"
1961```
1962use tauri::{Manager, Listener};
1963
1964tauri::Builder::default()
1965  .setup(|app| {
1966    let webview = app.get_webview("main").unwrap();
1967    let webview_ = webview.clone();
1968    let handler = webview.listen("component-loaded", move |event| {
1969      println!("webview just loaded a component");
1970
1971      // we no longer need to listen to the event
1972      // we also could have used `webview.once` instead
1973      webview_.unlisten(event.id());
1974    });
1975
1976    // stop listening to the event when you do not need it anymore
1977    webview.unlisten(handler);
1978
1979    Ok(())
1980  });
1981```
1982  "####
1983  )]
1984  fn unlisten(&self, id: EventId) {
1985    self.manager.unlisten(id)
1986  }
1987}
1988
1989impl<R: Runtime> Emitter<R> for Webview<R> {}
1990
1991impl<R: Runtime> Manager<R> for Webview<R> {
1992  fn resources_table(&self) -> MutexGuard<'_, ResourceTable> {
1993    self
1994      .resources_table
1995      .lock()
1996      .expect("poisoned window resources table")
1997  }
1998}
1999
2000impl<R: Runtime> ManagerBase<R> for Webview<R> {
2001  fn manager(&self) -> &AppManager<R> {
2002    &self.manager
2003  }
2004
2005  fn manager_owned(&self) -> Arc<AppManager<R>> {
2006    self.manager.clone()
2007  }
2008
2009  fn runtime(&self) -> RuntimeOrDispatch<'_, R> {
2010    self.app_handle.runtime()
2011  }
2012
2013  fn managed_app_handle(&self) -> &AppHandle<R> {
2014    &self.app_handle
2015  }
2016}
2017
2018impl<'de, R: Runtime> CommandArg<'de, R> for Webview<R> {
2019  /// Grabs the [`Webview`] from the [`CommandItem`]. This will never fail.
2020  fn from_command(command: CommandItem<'de, R>) -> Result<Self, InvokeError> {
2021    Ok(command.message.webview())
2022  }
2023}
2024
2025/// Resolved scope that can be obtained via [`Webview::resolve_command_scope`].
2026pub struct ResolvedScope<T: ScopeObject> {
2027  command_scope: CommandScope<T>,
2028  global_scope: GlobalScope<T>,
2029}
2030
2031impl<T: ScopeObject> ResolvedScope<T> {
2032  /// The global plugin scope.
2033  pub fn global_scope(&self) -> &GlobalScope<T> {
2034    &self.global_scope
2035  }
2036
2037  /// The command-specific scope.
2038  pub fn command_scope(&self) -> &CommandScope<T> {
2039    &self.command_scope
2040  }
2041}
2042
2043#[cfg(test)]
2044mod tests {
2045  #[test]
2046  fn webview_is_send_sync() {
2047    crate::test_utils::assert_send::<super::Webview>();
2048    crate::test_utils::assert_sync::<super::Webview>();
2049  }
2050}