Bläddra i källkod

refactor(core): break manager struct into smaller ones (#8124)

Lucas Fernandes Nogueira 1 år sedan
förälder
incheckning
04a682beb0

+ 1 - 1
core/tauri-codegen/src/context.rs

@@ -377,7 +377,7 @@ pub fn context_codegen(data: ContextData) -> Result<TokenStream, EmbeddedAssetsE
     #[allow(unused_mut, clippy::let_and_return)]
     let mut context = #root::Context::new(
       #config,
-      ::std::sync::Arc::new(#assets),
+      ::std::boxed::Box::new(#assets),
       #default_window_icon,
       #app_icon,
       #package_info,

+ 65 - 84
core/tauri/src/app.rs

@@ -8,7 +8,7 @@ use crate::{
     channel::ChannelDataIpcQueue, CallbackFn, Invoke, InvokeError, InvokeHandler, InvokeResponder,
     InvokeResponse,
   },
-  manager::{Asset, UriSchemeProtocol, WindowManager},
+  manager::{window::UriSchemeProtocol, AppManager, Asset},
   plugin::{Plugin, PluginStore},
   runtime::{
     webview::WebviewAttributes,
@@ -242,7 +242,7 @@ impl<R: Runtime> GlobalWindowEvent<R> {
 /// The asset resolver is a helper to access the [`tauri_utils::assets::Assets`] interface.
 #[derive(Debug, Clone)]
 pub struct AssetResolver<R: Runtime> {
-  manager: WindowManager<R>,
+  manager: Arc<AppManager<R>>,
 }
 
 impl<R: Runtime> AssetResolver<R> {
@@ -259,7 +259,7 @@ impl<R: Runtime> AssetResolver<R> {
 #[derive(Debug)]
 pub struct AppHandle<R: Runtime> {
   pub(crate) runtime_handle: R::Handle,
-  pub(crate) manager: WindowManager<R>,
+  pub(crate) manager: Arc<AppManager<R>>,
 }
 
 /// APIs specific to the wry runtime.
@@ -354,13 +354,7 @@ impl<R: Runtime> AppHandle<R> {
           .unwrap_or_default(),
       )
       .map_err(|e| crate::Error::PluginInitialization(plugin.name().to_string(), e.to_string()))?;
-    self
-      .manager()
-      .inner
-      .plugins
-      .lock()
-      .unwrap()
-      .register(plugin);
+    self.manager().plugins.lock().unwrap().register(plugin);
     Ok(())
   }
 
@@ -390,13 +384,7 @@ impl<R: Runtime> AppHandle<R> {
   ///   });
   /// ```
   pub fn remove_plugin(&self, plugin: &'static str) -> bool {
-    self
-      .manager()
-      .inner
-      .plugins
-      .lock()
-      .unwrap()
-      .unregister(plugin)
+    self.manager().plugins.lock().unwrap().unregister(plugin)
   }
 
   /// Exits the app. This is the same as [`std::process::exit`], but it performs cleanup on this application.
@@ -414,10 +402,14 @@ impl<R: Runtime> AppHandle<R> {
 
 impl<R: Runtime> Manager<R> for AppHandle<R> {}
 impl<R: Runtime> ManagerBase<R> for AppHandle<R> {
-  fn manager(&self) -> &WindowManager<R> {
+  fn manager(&self) -> &AppManager<R> {
     &self.manager
   }
 
+  fn manager_owned(&self) -> Arc<AppManager<R>> {
+    self.manager.clone()
+  }
+
   fn runtime(&self) -> RuntimeOrDispatch<'_, R> {
     RuntimeOrDispatch::RuntimeHandle(self.runtime_handle.clone())
   }
@@ -435,7 +427,7 @@ pub struct App<R: Runtime> {
   runtime: Option<R>,
   pending_windows: Option<Vec<PendingWindow<EventLoopMessage, R>>>,
   setup: Option<SetupHook<R>>,
-  manager: WindowManager<R>,
+  manager: Arc<AppManager<R>>,
   handle: AppHandle<R>,
 }
 
@@ -451,10 +443,14 @@ impl<R: Runtime> fmt::Debug for App<R> {
 
 impl<R: Runtime> Manager<R> for App<R> {}
 impl<R: Runtime> ManagerBase<R> for App<R> {
-  fn manager(&self) -> &WindowManager<R> {
+  fn manager(&self) -> &AppManager<R> {
     &self.manager
   }
 
+  fn manager_owned(&self) -> Arc<AppManager<R>> {
+    self.manager.clone()
+  }
+
   fn runtime(&self) -> RuntimeOrDispatch<'_, R> {
     if let Some(runtime) = self.runtime.as_ref() {
       RuntimeOrDispatch::Runtime(runtime)
@@ -497,8 +493,8 @@ macro_rules! shared_app_impl {
       ) {
         self
           .manager
-          .inner
-          .menu_event_listeners
+          .menu
+          .global_event_listeners
           .lock()
           .unwrap()
           .push(Box::new(handler));
@@ -513,8 +509,8 @@ macro_rules! shared_app_impl {
       ) {
         self
           .manager
-          .inner
-          .global_tray_event_listeners
+          .tray
+          .global_event_listeners
           .lock()
           .unwrap()
           .push(Box::new(handler));
@@ -525,14 +521,7 @@ macro_rules! shared_app_impl {
       #[cfg(all(desktop, feature = "tray-icon"))]
       #[cfg_attr(docsrs, doc(cfg(all(desktop, feature = "tray-icon"))))]
       pub fn tray(&self) -> Option<TrayIcon<R>> {
-        self
-          .manager
-          .inner
-          .tray_icons
-          .lock()
-          .unwrap()
-          .first()
-          .cloned()
+        self.manager.tray.icons.lock().unwrap().first().cloned()
       }
 
       /// Removes the first tray icon registerd, usually the one configured in
@@ -542,9 +531,9 @@ macro_rules! shared_app_impl {
       #[cfg(all(desktop, feature = "tray-icon"))]
       #[cfg_attr(docsrs, doc(cfg(all(desktop, feature = "tray-icon"))))]
       pub fn remove_tray(&self) -> Option<TrayIcon<R>> {
-        let mut tray_icons = self.manager.inner.tray_icons.lock().unwrap();
-        if !tray_icons.is_empty() {
-          return Some(tray_icons.swap_remove(0));
+        let mut icons = self.manager.tray.icons.lock().unwrap();
+        if !icons.is_empty() {
+          return Some(icons.swap_remove(0));
         }
         None
       }
@@ -559,8 +548,8 @@ macro_rules! shared_app_impl {
       {
         self
           .manager
-          .inner
-          .tray_icons
+          .tray
+          .icons
           .lock()
           .unwrap()
           .iter()
@@ -578,16 +567,16 @@ macro_rules! shared_app_impl {
         I: ?Sized,
         TrayIconId: PartialEq<&'a I>,
       {
-        let mut tray_icons = self.manager.inner.tray_icons.lock().unwrap();
-        let idx = tray_icons.iter().position(|t| t.id() == &id);
+        let mut icons = self.manager.tray.icons.lock().unwrap();
+        let idx = icons.iter().position(|t| t.id() == &id);
         if let Some(idx) = idx {
-          return Some(tray_icons.swap_remove(idx));
+          return Some(icons.swap_remove(idx));
         }
         None
       }
 
       /// Gets the app's configuration, defined on the `tauri.conf.json` file.
-      pub fn config(&self) -> Arc<Config> {
+      pub fn config(&self) -> &Config {
         self.manager.config()
       }
 
@@ -628,13 +617,13 @@ macro_rules! shared_app_impl {
       }
       /// Returns the default window icon.
       pub fn default_window_icon(&self) -> Option<&Icon> {
-        self.manager.inner.default_window_icon.as_ref()
+        self.manager.window.default_icon.as_ref()
       }
 
       /// Returns the app-wide menu.
       #[cfg(desktop)]
       pub fn menu(&self) -> Option<Menu<R>> {
-        self.manager.menu_lock().clone()
+        self.manager.menu.menu_lock().clone()
       }
 
       /// Sets the app-wide menu and returns the previous one.
@@ -645,9 +634,9 @@ macro_rules! shared_app_impl {
       pub fn set_menu(&self, menu: Menu<R>) -> crate::Result<Option<Menu<R>>> {
         let prev_menu = self.remove_menu()?;
 
-        self.manager.insert_menu_into_stash(&menu);
+        self.manager.menu.insert_menu_into_stash(&menu);
 
-        self.manager.menu_lock().replace(menu.clone());
+        self.manager.menu.menu_lock().replace(menu.clone());
 
         // set it on all windows that don't have one or previously had the app-wide menu
         #[cfg(not(target_os = "macos"))]
@@ -682,7 +671,7 @@ macro_rules! shared_app_impl {
       /// this will remove the menu from it.
       #[cfg(desktop)]
       pub fn remove_menu(&self) -> crate::Result<Option<Menu<R>>> {
-        let menu = self.manager.menu_lock().as_ref().cloned();
+        let menu = self.manager.menu.menu_lock().as_ref().cloned();
         #[allow(unused_variables)]
         if let Some(menu) = menu {
           // remove from windows that have the app-wide menu
@@ -706,7 +695,7 @@ macro_rules! shared_app_impl {
           }
         }
 
-        let prev_menu = self.manager.menu_lock().take();
+        let prev_menu = self.manager.menu.menu_lock().take();
 
         self
           .manager
@@ -723,7 +712,7 @@ macro_rules! shared_app_impl {
       pub fn hide_menu(&self) -> crate::Result<()> {
         #[cfg(not(target_os = "macos"))]
         {
-          let is_app_menu_set = self.manager.menu_lock().is_some();
+          let is_app_menu_set = self.manager.menu.menu_lock().is_some();
           if is_app_menu_set {
             for window in self.manager.windows().values() {
               if window.has_app_wide_menu() {
@@ -744,7 +733,7 @@ macro_rules! shared_app_impl {
       pub fn show_menu(&self) -> crate::Result<()> {
         #[cfg(not(target_os = "macos"))]
         {
-          let is_app_menu_set = self.manager.menu_lock().is_some();
+          let is_app_menu_set = self.manager.menu.menu_lock().is_some();
           if is_app_menu_set {
             for window in self.manager.windows().values() {
               if window.has_app_wide_menu() {
@@ -783,7 +772,7 @@ macro_rules! shared_app_impl {
       /// **You should always exit the tauri app immediately after this function returns and not use any tauri-related APIs.**
       pub fn cleanup_before_exit(&self) {
         #[cfg(all(desktop, feature = "tray-icon"))]
-        self.manager.inner.tray_icons.lock().unwrap().clear()
+        self.manager.tray.icons.lock().unwrap().clear()
       }
     }
   };
@@ -1019,7 +1008,7 @@ impl<R: Runtime> Builder<R> {
       invoke_handler: Box::new(|_| false),
       invoke_responder: None,
       invoke_initialization_script: InvokeInitializationScript {
-        process_ipc_message_fn: crate::manager::PROCESS_IPC_MESSAGE_FN,
+        process_ipc_message_fn: crate::manager::window::PROCESS_IPC_MESSAGE_FN,
         os_name: std::env::consts::OS,
         fetch_channel_data_command: crate::ipc::channel::FETCH_CHANNEL_DATA_COMMAND,
         use_custom_protocol: cfg!(ipc_custom_protocol),
@@ -1460,7 +1449,7 @@ impl<R: Runtime> Builder<R> {
       }));
     }
 
-    let manager = WindowManager::with_handlers(
+    let manager = Arc::new(AppManager::with_handlers(
       context,
       self.plugins,
       self.invoke_handler,
@@ -1471,7 +1460,7 @@ impl<R: Runtime> Builder<R> {
       #[cfg(desktop)]
       HashMap::new(),
       (self.invoke_responder, self.invoke_initialization_script),
-    );
+    ));
 
     // set up all the windows defined in the config
     for config in manager.config().tauri.windows.clone() {
@@ -1488,7 +1477,7 @@ impl<R: Runtime> Builder<R> {
     let runtime_args = RuntimeInitArgs {
       #[cfg(windows)]
       msg_hook: {
-        let menus = manager.inner.menus.clone();
+        let menus = manager.menu.menus.clone();
         Some(Box::new(move |msg| {
           use windows::Win32::UI::WindowsAndMessaging::{TranslateAcceleratorW, HACCEL, MSG};
           unsafe {
@@ -1555,13 +1544,14 @@ impl<R: Runtime> Builder<R> {
       let menu = menu(&app.handle)?;
       app
         .manager
+        .menu
         .menus_stash_lock()
         .insert(menu.id().clone(), menu.clone());
 
       #[cfg(target_os = "macos")]
       init_app_menu(&menu)?;
 
-      app.manager.menu_lock().replace(menu);
+      app.manager.menu.menu_lock().replace(menu);
     }
 
     app.register_core_plugins()?;
@@ -1570,7 +1560,7 @@ impl<R: Runtime> Builder<R> {
     app.manage(env);
 
     app.manage(Scopes {
-      ipc: scope::ipc::Scope::new(&app.config()),
+      ipc: scope::ipc::Scope::new(app.config()),
       #[cfg(feature = "protocol-asset")]
       asset_protocol: scope::fs::Scope::for_fs_api(
         &app,
@@ -1616,7 +1606,7 @@ impl<R: Runtime> Builder<R> {
           TrayIconBuilder::with_id(tray_config.id.clone().unwrap_or_else(|| "main".into()))
             .icon_as_template(tray_config.icon_as_template)
             .menu_on_left_click(tray_config.menu_on_left_click);
-        if let Some(icon) = &app.manager.inner.tray_icon {
+        if let Some(icon) = &app.manager.tray.icon {
           tray = tray.icon(icon.clone());
         }
         if let Some(title) = &tray_config.title {
@@ -1626,7 +1616,7 @@ impl<R: Runtime> Builder<R> {
           tray = tray.tooltip(tooltip);
         }
         let tray = tray.build(handle)?;
-        app.manager.inner.tray_icons.lock().unwrap().push(tray);
+        app.manager.tray.icons.lock().unwrap().push(tray);
       }
     }
 
@@ -1695,16 +1685,20 @@ fn setup<R: Runtime>(app: &mut App<R>) -> crate::Result<()> {
     let manager = app.manager();
 
     for pending in pending_windows {
-      let pending = manager.prepare_window(app_handle.clone(), pending, &window_labels)?;
+      let pending = manager
+        .window
+        .prepare_window(app_handle.clone(), pending, &window_labels)?;
 
       #[cfg(desktop)]
-      let window_menu = app.manager.menu_lock().as_ref().map(|m| WindowMenu {
+      let window_menu = app.manager.menu.menu_lock().as_ref().map(|m| WindowMenu {
         is_app_wide: true,
         menu: m.clone(),
       });
 
       #[cfg(desktop)]
-      let handler = manager.prepare_window_menu_creation_handler(window_menu.as_ref());
+      let handler = manager
+        .menu
+        .prepare_window_menu_creation_handler(window_menu.as_ref());
       #[cfg(not(desktop))]
       #[allow(clippy::type_complexity)]
       let handler: Option<Box<dyn Fn(tauri_runtime::window::RawWindow<'_>) + Send>> = None;
@@ -1716,7 +1710,7 @@ fn setup<R: Runtime>(app: &mut App<R>) -> crate::Result<()> {
         // the AppHandle's runtime is always RuntimeOrDispatch::RuntimeHandle
         unreachable!()
       };
-      let window = manager.attach_window(
+      let window = manager.window.attach_window(
         app_handle.clone(),
         detached,
         #[cfg(desktop)]
@@ -1739,7 +1733,7 @@ fn setup<R: Runtime>(app: &mut App<R>) -> crate::Result<()> {
 fn on_event_loop_event<R: Runtime, F: FnMut(&AppHandle<R>, RunEvent) + 'static>(
   app_handle: &AppHandle<R>,
   event: RuntimeRunEvent<EventLoopMessage>,
-  manager: &WindowManager<R>,
+  manager: &AppManager<R>,
   callback: Option<&mut F>,
 ) {
   if let RuntimeRunEvent::WindowEvent {
@@ -1747,7 +1741,7 @@ fn on_event_loop_event<R: Runtime, F: FnMut(&AppHandle<R>, RunEvent) + 'static>(
     event: RuntimeWindowEvent::Destroyed,
   } = &event
   {
-    manager.on_window_close(label);
+    manager.window.on_window_close(label);
   }
 
   let event = match event {
@@ -1769,7 +1763,7 @@ fn on_event_loop_event<R: Runtime, F: FnMut(&AppHandle<R>, RunEvent) + 'static>(
           foundation::NSData,
         };
         use objc::*;
-        if let Some(icon) = app_handle.manager.inner.app_icon.clone() {
+        if let Some(icon) = app_handle.manager.app_icon.clone() {
           let ns_app: id = msg_send![class!(NSApplication), sharedApplication];
           let data = NSData::dataWithBytes_length_(
             nil,
@@ -1790,20 +1784,14 @@ fn on_event_loop_event<R: Runtime, F: FnMut(&AppHandle<R>, RunEvent) + 'static>(
         EventLoopMessage::MenuEvent(ref e) => {
           for listener in &*app_handle
             .manager
-            .inner
-            .menu_event_listeners
+            .menu
+            .global_event_listeners
             .lock()
             .unwrap()
           {
             listener(app_handle, e.clone());
           }
-          for (label, listener) in &*app_handle
-            .manager
-            .inner
-            .window_menu_event_listeners
-            .lock()
-            .unwrap()
-          {
+          for (label, listener) in &*app_handle.manager.menu.event_listeners.lock().unwrap() {
             if let Some(w) = app_handle.get_window(label) {
               listener(&w, e.clone());
             }
@@ -1813,21 +1801,15 @@ fn on_event_loop_event<R: Runtime, F: FnMut(&AppHandle<R>, RunEvent) + 'static>(
         EventLoopMessage::TrayIconEvent(ref e) => {
           for listener in &*app_handle
             .manager
-            .inner
-            .global_tray_event_listeners
+            .tray
+            .global_event_listeners
             .lock()
             .unwrap()
           {
             listener(app_handle, e.clone());
           }
 
-          for (id, listener) in &*app_handle
-            .manager
-            .inner
-            .tray_event_listeners
-            .lock()
-            .unwrap()
-          {
+          for (id, listener) in &*app_handle.manager.tray.event_listeners.lock().unwrap() {
             if e.id == id {
               if let Some(tray) = app_handle.tray_by_id(id) {
                 listener(&tray, e.clone());
@@ -1846,7 +1828,6 @@ fn on_event_loop_event<R: Runtime, F: FnMut(&AppHandle<R>, RunEvent) + 'static>(
   };
 
   manager
-    .inner
     .plugins
     .lock()
     .expect("poisoned plugin store")

+ 9 - 9
core/tauri/src/ipc/protocol.rs

@@ -2,7 +2,7 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
-use std::borrow::Cow;
+use std::{borrow::Cow, sync::Arc};
 
 use http::{
   header::{ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_ORIGIN, CONTENT_TYPE},
@@ -10,7 +10,7 @@ use http::{
 };
 
 use crate::{
-  manager::WindowManager,
+  manager::AppManager,
   window::{InvokeRequest, UriSchemeProtocolHandler},
   Runtime,
 };
@@ -22,12 +22,12 @@ const TAURI_ERROR_HEADER_NAME: &str = "Tauri-Error";
 
 #[cfg(any(target_os = "macos", target_os = "ios", not(ipc_custom_protocol)))]
 pub fn message_handler<R: Runtime>(
-  manager: WindowManager<R>,
+  manager: Arc<AppManager<R>>,
 ) -> crate::runtime::webview::WebviewIpcHandler<crate::EventLoopMessage, R> {
   Box::new(move |window, request| handle_ipc_message(request, &manager, &window.label))
 }
 
-pub fn get<R: Runtime>(manager: WindowManager<R>, label: String) -> UriSchemeProtocolHandler {
+pub fn get<R: Runtime>(manager: Arc<AppManager<R>>, label: String) -> UriSchemeProtocolHandler {
   Box::new(move |request, responder| {
     let manager = manager.clone();
     let label = label.clone();
@@ -127,7 +127,7 @@ pub fn get<R: Runtime>(manager: WindowManager<R>, label: String) -> UriSchemePro
 }
 
 #[cfg(any(target_os = "macos", target_os = "ios", not(ipc_custom_protocol)))]
-fn handle_ipc_message<R: Runtime>(message: String, manager: &WindowManager<R>, label: &str) {
+fn handle_ipc_message<R: Runtime>(message: String, manager: &AppManager<R>, label: &str) {
   if let Some(window) = manager.get_window(label) {
     use serde::{Deserialize, Deserializer};
 
@@ -184,7 +184,7 @@ fn handle_ipc_message<R: Runtime>(message: String, manager: &WindowManager<R>, l
         options: Option<RequestOptions>,
       }
 
-      if let crate::Pattern::Isolation { crypto_keys, .. } = manager.pattern() {
+      if let crate::Pattern::Isolation { crypto_keys, .. } = &*manager.pattern {
         invoke_message.replace(
           serde_json::from_str::<IsolationMessage<'_>>(&message)
             .map_err(Into::into)
@@ -223,7 +223,7 @@ fn handle_ipc_message<R: Runtime>(message: String, manager: &WindowManager<R>, l
             use serde_json::Value as JsonValue;
 
             // the channel data command is the only command that uses a custom protocol on Linux
-            if window.manager.invoke_responder().is_none()
+            if window.manager.window.invoke_responder.is_none()
               && cmd != crate::ipc::channel::FETCH_CHANNEL_DATA_COMMAND
             {
               fn responder_eval<R: Runtime>(
@@ -286,7 +286,7 @@ fn handle_ipc_message<R: Runtime>(message: String, manager: &WindowManager<R>, l
 }
 
 fn parse_invoke_request<R: Runtime>(
-  #[allow(unused_variables)] manager: &WindowManager<R>,
+  #[allow(unused_variables)] manager: &AppManager<R>,
   request: http::Request<Vec<u8>>,
 ) -> std::result::Result<InvokeRequest, String> {
   #[allow(unused_mut)]
@@ -299,7 +299,7 @@ fn parse_invoke_request<R: Runtime>(
 
   // the body is not set if ipc_custom_protocol is not enabled so we'll just ignore it
   #[cfg(all(feature = "isolation", ipc_custom_protocol))]
-  if let crate::Pattern::Isolation { crypto_keys, .. } = manager.pattern() {
+  if let crate::Pattern::Isolation { crypto_keys, .. } = &*manager.pattern {
     body = crate::utils::pattern::isolation::RawIsolationPayload::try_from(&body)
       .and_then(|raw| crypto_keys.decrypt(raw))
       .map_err(|e| e.to_string())?;

+ 11 - 12
core/tauri/src/lib.rs

@@ -164,7 +164,6 @@ use serde::Serialize;
 use std::{
   collections::HashMap,
   fmt::{self, Debug},
-  sync::Arc,
 };
 
 #[cfg(feature = "wry")]
@@ -389,7 +388,7 @@ impl TryFrom<Icon> for runtime::Icon {
 /// Unless you know what you are doing and are prepared for this type to have breaking changes, do not create it yourself.
 pub struct Context<A: Assets> {
   pub(crate) config: Config,
-  pub(crate) assets: Arc<A>,
+  pub(crate) assets: Box<A>,
   pub(crate) default_window_icon: Option<Icon>,
   pub(crate) app_icon: Option<Vec<u8>>,
   #[cfg(all(desktop, feature = "tray-icon"))]
@@ -430,13 +429,13 @@ impl<A: Assets> Context<A> {
 
   /// The assets to be served directly by Tauri.
   #[inline(always)]
-  pub fn assets(&self) -> Arc<A> {
-    self.assets.clone()
+  pub fn assets(&self) -> &A {
+    &self.assets
   }
 
   /// A mutable reference to the assets to be served directly by Tauri.
   #[inline(always)]
-  pub fn assets_mut(&mut self) -> &mut Arc<A> {
+  pub fn assets_mut(&mut self) -> &mut A {
     &mut self.assets
   }
 
@@ -491,7 +490,7 @@ impl<A: Assets> Context<A> {
   #[allow(clippy::too_many_arguments)]
   pub fn new(
     config: Config,
-    assets: Arc<A>,
+    assets: Box<A>,
     default_window_icon: Option<Icon>,
     app_icon: Option<Vec<u8>>,
     package_info: PackageInfo,
@@ -536,7 +535,7 @@ pub trait Manager<R: Runtime>: sealed::ManagerBase<R> {
   }
 
   /// The [`Config`] the manager was created with.
-  fn config(&self) -> Arc<Config> {
+  fn config(&self) -> &Config {
     self.manager().config()
   }
 
@@ -794,7 +793,6 @@ pub trait Manager<R: Runtime>: sealed::ManagerBase<R> {
   {
     self
       .manager()
-      .inner
       .state
       .try_get()
       .expect("state() called before manage() for given type")
@@ -807,7 +805,7 @@ pub trait Manager<R: Runtime>: sealed::ManagerBase<R> {
   where
     T: Send + Sync + 'static,
   {
-    self.manager().inner.state.try_get()
+    self.manager().state.try_get()
   }
 
   /// Gets the managed [`Env`].
@@ -835,7 +833,8 @@ pub trait Manager<R: Runtime>: sealed::ManagerBase<R> {
 /// Prevent implementation details from leaking out of the [`Manager`] trait.
 pub(crate) mod sealed {
   use super::Runtime;
-  use crate::{app::AppHandle, manager::WindowManager};
+  use crate::{app::AppHandle, manager::AppManager};
+  use std::sync::Arc;
 
   /// A running [`Runtime`] or a dispatcher to it.
   pub enum RuntimeOrDispatch<'r, R: Runtime> {
@@ -851,8 +850,8 @@ pub(crate) mod sealed {
 
   /// Managed handle to the application runtime.
   pub trait ManagerBase<R: Runtime> {
-    /// The manager behind the [`Managed`] item.
-    fn manager(&self) -> &WindowManager<R>;
+    fn manager(&self) -> &AppManager<R>;
+    fn manager_owned(&self) -> Arc<AppManager<R>>;
     fn runtime(&self) -> RuntimeOrDispatch<'_, R>;
     fn managed_app_handle(&self) -> &AppHandle<R>;
   }

+ 0 - 1575
core/tauri/src/manager.rs

@@ -1,1575 +0,0 @@
-// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
-// SPDX-License-Identifier: Apache-2.0
-// SPDX-License-Identifier: MIT
-
-use std::{
-  borrow::Cow,
-  collections::{HashMap, HashSet},
-  fmt,
-  fs::create_dir_all,
-  path::PathBuf,
-  sync::{Arc, Mutex, MutexGuard},
-};
-
-#[cfg(desktop)]
-use crate::menu::{Menu, MenuId};
-#[cfg(all(desktop, feature = "tray-icon"))]
-use crate::tray::{TrayIcon, TrayIconId};
-use serde::Serialize;
-use serialize_to_javascript::{default_template, DefaultTemplate, Template};
-use url::Url;
-
-use tauri_macros::default_runtime;
-use tauri_utils::debug_eprintln;
-use tauri_utils::{
-  assets::{AssetKey, CspHash},
-  config::{Csp, CspDirectiveSources},
-  html::{SCRIPT_NONCE_TOKEN, STYLE_NONCE_TOKEN},
-};
-
-use crate::{
-  app::{AppHandle, GlobalWindowEvent, GlobalWindowEventListener, OnPageLoad, UriSchemeResponder},
-  event::{assert_event_name_is_valid, Event, EventId, Listeners},
-  ipc::{Invoke, InvokeHandler, InvokeResponder},
-  pattern::PatternJavascript,
-  plugin::PluginStore,
-  runtime::{
-    webview::WindowBuilder,
-    window::{
-      dpi::{PhysicalPosition, PhysicalSize},
-      DetachedWindow, FileDropEvent, PendingWindow,
-    },
-  },
-  utils::{
-    assets::Assets,
-    config::{AppUrl, Config, WindowUrl},
-    PackageInfo,
-  },
-  Context, EventLoopMessage, Icon, Manager, Pattern, Runtime, Scopes, StateManager, Window,
-  WindowEvent,
-};
-use crate::{event::EmitArgs, window::PageLoadPayload};
-
-#[cfg(desktop)]
-use crate::app::GlobalMenuEventListener;
-#[cfg(all(desktop, feature = "tray-icon"))]
-use crate::app::GlobalTrayIconEventListener;
-
-#[cfg(any(target_os = "linux", target_os = "windows"))]
-use crate::path::BaseDirectory;
-
-const WINDOW_RESIZED_EVENT: &str = "tauri://resize";
-const WINDOW_MOVED_EVENT: &str = "tauri://move";
-const WINDOW_CLOSE_REQUESTED_EVENT: &str = "tauri://close-requested";
-const WINDOW_DESTROYED_EVENT: &str = "tauri://destroyed";
-const WINDOW_FOCUS_EVENT: &str = "tauri://focus";
-const WINDOW_BLUR_EVENT: &str = "tauri://blur";
-const WINDOW_SCALE_FACTOR_CHANGED_EVENT: &str = "tauri://scale-change";
-const WINDOW_THEME_CHANGED: &str = "tauri://theme-changed";
-const WINDOW_FILE_DROP_EVENT: &str = "tauri://file-drop";
-const WINDOW_FILE_DROP_HOVER_EVENT: &str = "tauri://file-drop-hover";
-const WINDOW_FILE_DROP_CANCELLED_EVENT: &str = "tauri://file-drop-cancelled";
-
-pub(crate) const PROCESS_IPC_MESSAGE_FN: &str =
-  include_str!("../scripts/process-ipc-message-fn.js");
-
-// we need to proxy the dev server on mobile because we can't use `localhost`, so we use the local IP address
-// and we do not get a secure context without the custom protocol that proxies to the dev server
-// additionally, we need the custom protocol to inject the initialization scripts on Android
-// must also keep in sync with the `let mut response` assignment in prepare_uri_scheme_protocol
-pub(crate) const PROXY_DEV_SERVER: bool = cfg!(all(dev, mobile));
-
-#[cfg(feature = "isolation")]
-#[derive(Template)]
-#[default_template("../scripts/isolation.js")]
-pub(crate) struct IsolationJavascript<'a> {
-  pub(crate) isolation_src: &'a str,
-  pub(crate) style: &'a str,
-}
-
-#[derive(Template)]
-#[default_template("../scripts/ipc.js")]
-pub(crate) struct IpcJavascript<'a> {
-  pub(crate) isolation_origin: &'a str,
-}
-
-#[derive(Default)]
-/// Spaced and quoted Content-Security-Policy hash values.
-struct CspHashStrings {
-  script: Vec<String>,
-  style: Vec<String>,
-}
-
-/// Sets the CSP value to the asset HTML if needed (on Linux).
-/// Returns the CSP string for access on the response header (on Windows and macOS).
-fn set_csp<R: Runtime>(
-  asset: &mut String,
-  assets: Arc<dyn Assets>,
-  asset_path: &AssetKey,
-  manager: &WindowManager<R>,
-  csp: Csp,
-) -> String {
-  let mut csp = csp.into();
-  let hash_strings =
-    assets
-      .csp_hashes(asset_path)
-      .fold(CspHashStrings::default(), |mut acc, hash| {
-        match hash {
-          CspHash::Script(hash) => {
-            acc.script.push(hash.into());
-          }
-          CspHash::Style(hash) => {
-            acc.style.push(hash.into());
-          }
-          _csp_hash => {
-            debug_eprintln!("Unknown CspHash variant encountered: {:?}", _csp_hash);
-          }
-        }
-
-        acc
-      });
-
-  let dangerous_disable_asset_csp_modification = &manager
-    .config()
-    .tauri
-    .security
-    .dangerous_disable_asset_csp_modification;
-  if dangerous_disable_asset_csp_modification.can_modify("script-src") {
-    replace_csp_nonce(
-      asset,
-      SCRIPT_NONCE_TOKEN,
-      &mut csp,
-      "script-src",
-      hash_strings.script,
-    );
-  }
-
-  if dangerous_disable_asset_csp_modification.can_modify("style-src") {
-    replace_csp_nonce(
-      asset,
-      STYLE_NONCE_TOKEN,
-      &mut csp,
-      "style-src",
-      hash_strings.style,
-    );
-  }
-
-  #[cfg(feature = "isolation")]
-  if let Pattern::Isolation { schema, .. } = &manager.inner.pattern {
-    let default_src = csp
-      .entry("default-src".into())
-      .or_insert_with(Default::default);
-    default_src.push(crate::pattern::format_real_schema(schema));
-  }
-
-  Csp::DirectiveMap(csp).to_string()
-}
-
-// inspired by https://github.com/rust-lang/rust/blob/1be5c8f90912c446ecbdc405cbc4a89f9acd20fd/library/alloc/src/str.rs#L260-L297
-fn replace_with_callback<F: FnMut() -> String>(
-  original: &str,
-  pattern: &str,
-  mut replacement: F,
-) -> String {
-  let mut result = String::new();
-  let mut last_end = 0;
-  for (start, part) in original.match_indices(pattern) {
-    result.push_str(unsafe { original.get_unchecked(last_end..start) });
-    result.push_str(&replacement());
-    last_end = start + part.len();
-  }
-  result.push_str(unsafe { original.get_unchecked(last_end..original.len()) });
-  result
-}
-
-fn replace_csp_nonce(
-  asset: &mut String,
-  token: &str,
-  csp: &mut HashMap<String, CspDirectiveSources>,
-  directive: &str,
-  hashes: Vec<String>,
-) {
-  let mut nonces = Vec::new();
-  *asset = replace_with_callback(asset, token, || {
-    #[cfg(target_pointer_width = "64")]
-    let mut raw = [0u8; 8];
-    #[cfg(target_pointer_width = "32")]
-    let mut raw = [0u8; 4];
-    #[cfg(target_pointer_width = "16")]
-    let mut raw = [0u8; 2];
-    getrandom::getrandom(&mut raw).expect("failed to get random bytes");
-    let nonce = usize::from_ne_bytes(raw);
-    nonces.push(nonce);
-    nonce.to_string()
-  });
-
-  if !(nonces.is_empty() && hashes.is_empty()) {
-    let nonce_sources = nonces
-      .into_iter()
-      .map(|n| format!("'nonce-{n}'"))
-      .collect::<Vec<String>>();
-    let sources = csp.entry(directive.into()).or_default();
-    let self_source = "'self'".to_string();
-    if !sources.contains(&self_source) {
-      sources.push(self_source);
-    }
-    sources.extend(nonce_sources);
-    sources.extend(hashes);
-  }
-}
-
-#[default_runtime(crate::Wry, wry)]
-pub struct InnerWindowManager<R: Runtime> {
-  pub(crate) windows: Mutex<HashMap<String, Window<R>>>,
-  pub(crate) plugins: Mutex<PluginStore<R>>,
-  listeners: Listeners<R>,
-  pub(crate) state: Arc<StateManager>,
-
-  /// The JS message handler.
-  invoke_handler: Box<InvokeHandler<R>>,
-
-  /// The page load hook, invoked when the webview performs a navigation.
-  on_page_load: Option<Arc<OnPageLoad<R>>>,
-
-  config: Arc<Config>,
-  assets: Arc<dyn Assets>,
-  pub(crate) default_window_icon: Option<Icon>,
-  pub(crate) app_icon: Option<Vec<u8>>,
-  #[cfg(all(desktop, feature = "tray-icon"))]
-  pub(crate) tray_icon: Option<Icon>,
-
-  package_info: PackageInfo,
-  /// The webview protocols available to all windows.
-  uri_scheme_protocols: Mutex<HashMap<String, Arc<UriSchemeProtocol<R>>>>,
-  /// A set containing a reference to the active menus, including
-  /// the app-wide menu and the window-specific menus
-  ///
-  /// This should be mainly used to acceess [`Menu::haccel`]
-  /// to setup the accelerator handling in the event loop
-  #[cfg(desktop)]
-  pub menus: Arc<Mutex<HashMap<MenuId, Menu<R>>>>,
-  /// The menu set to all windows.
-  #[cfg(desktop)]
-  pub(crate) menu: Arc<Mutex<Option<Menu<R>>>>,
-  /// Menu event listeners to all windows.
-  #[cfg(desktop)]
-  pub(crate) menu_event_listeners: Arc<Mutex<Vec<GlobalMenuEventListener<AppHandle<R>>>>>,
-  /// Menu event listeners to specific windows.
-  #[cfg(desktop)]
-  pub(crate) window_menu_event_listeners:
-    Arc<Mutex<HashMap<String, GlobalMenuEventListener<Window<R>>>>>,
-  /// Window event listeners to all windows.
-  window_event_listeners: Arc<Vec<GlobalWindowEventListener<R>>>,
-  /// Tray icons
-  #[cfg(all(desktop, feature = "tray-icon"))]
-  pub(crate) tray_icons: Arc<Mutex<Vec<TrayIcon<R>>>>,
-  /// Global Tray icon event listeners.
-  #[cfg(all(desktop, feature = "tray-icon"))]
-  pub(crate) global_tray_event_listeners:
-    Arc<Mutex<Vec<GlobalTrayIconEventListener<AppHandle<R>>>>>,
-  /// Tray icon event listeners.
-  #[cfg(all(desktop, feature = "tray-icon"))]
-  pub(crate) tray_event_listeners:
-    Arc<Mutex<HashMap<TrayIconId, GlobalTrayIconEventListener<TrayIcon<R>>>>>,
-  /// Responder for invoke calls.
-  invoke_responder: Option<Arc<InvokeResponder<R>>>,
-  /// The script that initializes the invoke system.
-  invoke_initialization_script: String,
-  /// Application pattern.
-  pub(crate) pattern: Pattern,
-}
-
-impl<R: Runtime> fmt::Debug for InnerWindowManager<R> {
-  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-    let mut d = f.debug_struct("InnerWindowManager");
-
-    d.field("plugins", &self.plugins)
-      .field("state", &self.state)
-      .field("config", &self.config)
-      .field("default_window_icon", &self.default_window_icon)
-      .field("app_icon", &self.app_icon)
-      .field("package_info", &self.package_info)
-      .field("pattern", &self.pattern);
-
-    #[cfg(all(desktop, feature = "tray-icon"))]
-    d.field("tray_icon", &self.tray_icon);
-
-    d.finish()
-  }
-}
-
-/// A resolved asset.
-pub struct Asset {
-  /// The asset bytes.
-  pub bytes: Vec<u8>,
-  /// The asset's mime type.
-  pub mime_type: String,
-  /// The `Content-Security-Policy` header value.
-  pub csp_header: Option<String>,
-}
-
-/// Uses a custom URI scheme handler to resolve file requests
-pub struct UriSchemeProtocol<R: Runtime> {
-  /// Handler for protocol
-  #[allow(clippy::type_complexity)]
-  pub protocol:
-    Box<dyn Fn(&AppHandle<R>, http::Request<Vec<u8>>, UriSchemeResponder) + Send + Sync>,
-}
-
-#[default_runtime(crate::Wry, wry)]
-#[derive(Debug)]
-pub struct WindowManager<R: Runtime> {
-  pub inner: Arc<InnerWindowManager<R>>,
-}
-
-impl<R: Runtime> Clone for WindowManager<R> {
-  fn clone(&self) -> Self {
-    Self {
-      inner: self.inner.clone(),
-    }
-  }
-}
-
-impl<R: Runtime> WindowManager<R> {
-  #[allow(clippy::too_many_arguments, clippy::type_complexity)]
-  pub(crate) fn with_handlers(
-    #[allow(unused_mut)] mut context: Context<impl Assets>,
-    plugins: PluginStore<R>,
-    invoke_handler: Box<InvokeHandler<R>>,
-    on_page_load: Option<Arc<OnPageLoad<R>>>,
-    uri_scheme_protocols: HashMap<String, Arc<UriSchemeProtocol<R>>>,
-    state: StateManager,
-    window_event_listeners: Vec<GlobalWindowEventListener<R>>,
-    #[cfg(desktop)] window_menu_event_listeners: HashMap<
-      String,
-      GlobalMenuEventListener<Window<R>>,
-    >,
-    (invoke_responder, invoke_initialization_script): (Option<Arc<InvokeResponder<R>>>, String),
-  ) -> Self {
-    // generate a random isolation key at runtime
-    #[cfg(feature = "isolation")]
-    if let Pattern::Isolation { ref mut key, .. } = &mut context.pattern {
-      *key = uuid::Uuid::new_v4().to_string();
-    }
-
-    Self {
-      inner: Arc::new(InnerWindowManager {
-        windows: Mutex::default(),
-        plugins: Mutex::new(plugins),
-        listeners: Listeners::default(),
-        state: Arc::new(state),
-        invoke_handler,
-        on_page_load,
-        config: Arc::new(context.config),
-        assets: context.assets,
-        default_window_icon: context.default_window_icon,
-        app_icon: context.app_icon,
-        #[cfg(all(desktop, feature = "tray-icon"))]
-        tray_icon: context.tray_icon,
-        package_info: context.package_info,
-        pattern: context.pattern,
-        uri_scheme_protocols: Mutex::new(uri_scheme_protocols),
-        #[cfg(desktop)]
-        menus: Default::default(),
-        #[cfg(desktop)]
-        menu: Default::default(),
-        #[cfg(desktop)]
-        menu_event_listeners: Default::default(),
-        #[cfg(desktop)]
-        window_menu_event_listeners: Arc::new(Mutex::new(window_menu_event_listeners)),
-        window_event_listeners: Arc::new(window_event_listeners),
-        #[cfg(all(desktop, feature = "tray-icon"))]
-        tray_icons: Default::default(),
-        #[cfg(all(desktop, feature = "tray-icon"))]
-        global_tray_event_listeners: Default::default(),
-        #[cfg(all(desktop, feature = "tray-icon"))]
-        tray_event_listeners: Default::default(),
-        invoke_responder,
-        invoke_initialization_script,
-      }),
-    }
-  }
-
-  pub(crate) fn pattern(&self) -> &Pattern {
-    &self.inner.pattern
-  }
-
-  /// Get a locked handle to the windows.
-  pub(crate) fn windows_lock(&self) -> MutexGuard<'_, HashMap<String, Window<R>>> {
-    self.inner.windows.lock().expect("poisoned window manager")
-  }
-
-  /// State managed by the application.
-  pub(crate) fn state(&self) -> Arc<StateManager> {
-    self.inner.state.clone()
-  }
-
-  #[cfg(desktop)]
-  pub(crate) fn prepare_window_menu_creation_handler(
-    &self,
-    window_menu: Option<&crate::window::WindowMenu<R>>,
-  ) -> Option<impl Fn(tauri_runtime::window::RawWindow<'_>)> {
-    {
-      if let Some(menu) = window_menu {
-        self
-          .menus_stash_lock()
-          .insert(menu.menu.id().clone(), menu.menu.clone());
-      }
-    }
-
-    #[cfg(target_os = "macos")]
-    return None;
-
-    #[cfg_attr(target_os = "macos", allow(unused_variables, unreachable_code))]
-    if let Some(menu) = &window_menu {
-      let menu = menu.menu.clone();
-      Some(move |raw: tauri_runtime::window::RawWindow<'_>| {
-        #[cfg(target_os = "windows")]
-        let _ = menu.inner().init_for_hwnd(raw.hwnd as _);
-        #[cfg(any(
-          target_os = "linux",
-          target_os = "dragonfly",
-          target_os = "freebsd",
-          target_os = "netbsd",
-          target_os = "openbsd"
-        ))]
-        let _ = menu
-          .inner()
-          .init_for_gtk_window(raw.gtk_window, raw.default_vbox);
-      })
-    } else {
-      None
-    }
-  }
-
-  /// App-wide menu.
-  #[cfg(desktop)]
-  pub(crate) fn menu_lock(&self) -> MutexGuard<'_, Option<Menu<R>>> {
-    self.inner.menu.lock().expect("poisoned window manager")
-  }
-
-  /// Menus stash.
-  #[cfg(desktop)]
-  pub(crate) fn menus_stash_lock(&self) -> MutexGuard<'_, HashMap<MenuId, Menu<R>>> {
-    self.inner.menus.lock().expect("poisoned window manager")
-  }
-
-  #[cfg(desktop)]
-  pub(crate) fn is_menu_in_use<I: PartialEq<MenuId>>(&self, id: &I) -> bool {
-    self
-      .menu_lock()
-      .as_ref()
-      .map(|m| id.eq(m.id()))
-      .unwrap_or(false)
-  }
-
-  /// Menus stash.
-  #[cfg(desktop)]
-  pub(crate) fn insert_menu_into_stash(&self, menu: &Menu<R>) {
-    self
-      .menus_stash_lock()
-      .insert(menu.id().clone(), menu.clone());
-  }
-
-  #[cfg(desktop)]
-  pub(crate) fn remove_menu_from_stash_by_id(&self, id: Option<&MenuId>) {
-    if let Some(id) = id {
-      let is_used_by_a_window = self.windows_lock().values().any(|w| w.is_menu_in_use(id));
-      if !(self.is_menu_in_use(id) || is_used_by_a_window) {
-        self.menus_stash_lock().remove(id);
-      }
-    }
-  }
-
-  /// The invoke responder.
-  pub(crate) fn invoke_responder(&self) -> Option<Arc<InvokeResponder<R>>> {
-    self.inner.invoke_responder.clone()
-  }
-
-  pub(crate) fn register_uri_scheme_protocol<N: Into<String>>(
-    &self,
-    uri_scheme: N,
-    protocol: Arc<UriSchemeProtocol<R>>,
-  ) {
-    let uri_scheme = uri_scheme.into();
-    self
-      .inner
-      .uri_scheme_protocols
-      .lock()
-      .unwrap()
-      .insert(uri_scheme, protocol);
-  }
-
-  /// Get the base path to serve data from.
-  ///
-  /// * In dev mode, this will be based on the `devPath` configuration value.
-  /// * Otherwise, this will be based on the `distDir` configuration value.
-  #[cfg(not(dev))]
-  fn base_path(&self) -> &AppUrl {
-    &self.inner.config.build.dist_dir
-  }
-
-  #[cfg(dev)]
-  fn base_path(&self) -> &AppUrl {
-    &self.inner.config.build.dev_path
-  }
-
-  /// Get the base URL to use for webview requests.
-  ///
-  /// In dev mode, this will be based on the `devPath` configuration value.
-  pub(crate) fn get_url(&self) -> Cow<'_, Url> {
-    match self.base_path() {
-      AppUrl::Url(WindowUrl::External(url)) => Cow::Borrowed(url),
-      _ => self.protocol_url(),
-    }
-  }
-
-  pub(crate) fn protocol_url(&self) -> Cow<'_, Url> {
-    if cfg!(windows) || cfg!(target_os = "android") {
-      Cow::Owned(Url::parse("http://tauri.localhost").unwrap())
-    } else {
-      Cow::Owned(Url::parse("tauri://localhost").unwrap())
-    }
-  }
-
-  fn csp(&self) -> Option<Csp> {
-    if cfg!(feature = "custom-protocol") {
-      self.inner.config.tauri.security.csp.clone()
-    } else {
-      self
-        .inner
-        .config
-        .tauri
-        .security
-        .dev_csp
-        .clone()
-        .or_else(|| self.inner.config.tauri.security.csp.clone())
-    }
-  }
-
-  fn prepare_pending_window(
-    &self,
-    mut pending: PendingWindow<EventLoopMessage, R>,
-    label: &str,
-    window_labels: &[String],
-    app_handle: AppHandle<R>,
-  ) -> crate::Result<PendingWindow<EventLoopMessage, R>> {
-    let is_init_global = self.inner.config.build.with_global_tauri;
-    let plugin_init = self
-      .inner
-      .plugins
-      .lock()
-      .expect("poisoned plugin store")
-      .initialization_script();
-
-    let pattern_init = PatternJavascript {
-      pattern: self.pattern().into(),
-    }
-    .render_default(&Default::default())?;
-
-    let mut webview_attributes = pending.webview_attributes;
-
-    let ipc_init = IpcJavascript {
-      isolation_origin: &match self.pattern() {
-        #[cfg(feature = "isolation")]
-        Pattern::Isolation { schema, .. } => crate::pattern::format_real_schema(schema),
-        _ => "".to_string(),
-      },
-    }
-    .render_default(&Default::default())?;
-
-    let mut window_labels = window_labels.to_vec();
-    let l = label.to_string();
-    if !window_labels.contains(&l) {
-      window_labels.push(l);
-    }
-    webview_attributes = webview_attributes
-      .initialization_script(
-        r#"
-        if (!window.__TAURI_INTERNALS__) {
-          Object.defineProperty(window, '__TAURI_INTERNALS__', {
-            value: {
-              plugins: {}
-            }
-          })
-        }
-      "#,
-      )
-      .initialization_script(&self.inner.invoke_initialization_script)
-      .initialization_script(&format!(
-        r#"
-          Object.defineProperty(window.__TAURI_INTERNALS__, 'metadata', {{
-            value: {{
-              windows: {window_labels_array}.map(function (label) {{ return {{ label: label }} }}),
-              currentWindow: {{ label: {current_window_label} }}
-            }}
-          }})
-        "#,
-        window_labels_array = serde_json::to_string(&window_labels)?,
-        current_window_label = serde_json::to_string(&label)?,
-      ))
-      .initialization_script(&self.initialization_script(
-        &ipc_init.into_string(),
-        &pattern_init.into_string(),
-        &plugin_init,
-        is_init_global,
-      )?);
-
-    #[cfg(feature = "isolation")]
-    if let Pattern::Isolation { schema, .. } = self.pattern() {
-      webview_attributes = webview_attributes.initialization_script(
-        &IsolationJavascript {
-          isolation_src: &crate::pattern::format_real_schema(schema),
-          style: tauri_utils::pattern::isolation::IFRAME_STYLE,
-        }
-        .render_default(&Default::default())?
-        .into_string(),
-      );
-    }
-
-    pending.webview_attributes = webview_attributes;
-
-    let mut registered_scheme_protocols = Vec::new();
-
-    for (uri_scheme, protocol) in &*self.inner.uri_scheme_protocols.lock().unwrap() {
-      registered_scheme_protocols.push(uri_scheme.clone());
-      let protocol = protocol.clone();
-      let app_handle = Mutex::new(app_handle.clone());
-      pending.register_uri_scheme_protocol(uri_scheme.clone(), move |p, responder| {
-        (protocol.protocol)(
-          &app_handle.lock().unwrap(),
-          p,
-          UriSchemeResponder(responder),
-        )
-      });
-    }
-
-    let window_url = Url::parse(&pending.url).unwrap();
-    let window_origin = if window_url.scheme() == "data" {
-      "null".into()
-    } else if (cfg!(windows) || cfg!(target_os = "android"))
-      && window_url.scheme() != "http"
-      && window_url.scheme() != "https"
-    {
-      format!("http://{}.localhost", window_url.scheme())
-    } else {
-      format!(
-        "{}://{}{}",
-        window_url.scheme(),
-        window_url.host().unwrap(),
-        window_url
-          .port()
-          .map(|p| format!(":{p}"))
-          .unwrap_or_default()
-      )
-    };
-
-    if !registered_scheme_protocols.contains(&"tauri".into()) {
-      let web_resource_request_handler = pending.web_resource_request_handler.take();
-      let protocol =
-        crate::protocol::tauri::get(self, &window_origin, web_resource_request_handler);
-      pending.register_uri_scheme_protocol("tauri", move |request, responder| {
-        protocol(request, UriSchemeResponder(responder))
-      });
-      registered_scheme_protocols.push("tauri".into());
-    }
-
-    if !registered_scheme_protocols.contains(&"ipc".into()) {
-      let protocol = crate::ipc::protocol::get(self.clone(), pending.label.clone());
-      pending.register_uri_scheme_protocol("ipc", move |request, responder| {
-        protocol(request, UriSchemeResponder(responder))
-      });
-      registered_scheme_protocols.push("ipc".into());
-    }
-
-    let label = pending.label.clone();
-    let manager = self.clone();
-    let on_page_load_handler = pending.on_page_load_handler.take();
-    pending
-      .on_page_load_handler
-      .replace(Box::new(move |url, event| {
-        let payload = PageLoadPayload { url: &url, event };
-
-        if let Some(w) = manager.get_window(&label) {
-          if let Some(on_page_load) = &manager.inner.on_page_load {
-            on_page_load(&w, &payload);
-          }
-
-          manager
-            .inner
-            .plugins
-            .lock()
-            .unwrap()
-            .on_page_load(&w, &payload);
-        }
-
-        if let Some(handler) = &on_page_load_handler {
-          handler(url, event);
-        }
-      }));
-
-    #[cfg(feature = "protocol-asset")]
-    if !registered_scheme_protocols.contains(&"asset".into()) {
-      let asset_scope = self.state().get::<crate::Scopes>().asset_protocol.clone();
-      let protocol = crate::protocol::asset::get(asset_scope.clone(), window_origin.clone());
-      pending.register_uri_scheme_protocol("asset", move |request, responder| {
-        protocol(request, UriSchemeResponder(responder))
-      });
-    }
-
-    #[cfg(feature = "isolation")]
-    if let Pattern::Isolation {
-      assets,
-      schema,
-      key: _,
-      crypto_keys,
-    } = &self.inner.pattern
-    {
-      let protocol = crate::protocol::isolation::get(assets.clone(), *crypto_keys.aes_gcm().raw());
-      pending.register_uri_scheme_protocol(schema, move |request, responder| {
-        protocol(request, UriSchemeResponder(responder))
-      });
-    }
-
-    Ok(pending)
-  }
-
-  pub fn get_asset(&self, mut path: String) -> Result<Asset, Box<dyn std::error::Error>> {
-    let assets = &self.inner.assets;
-    if path.ends_with('/') {
-      path.pop();
-    }
-    path = percent_encoding::percent_decode(path.as_bytes())
-      .decode_utf8_lossy()
-      .to_string();
-    let path = if path.is_empty() {
-      // if the url is `tauri://localhost`, we should load `index.html`
-      "index.html".to_string()
-    } else {
-      // skip leading `/`
-      path.chars().skip(1).collect::<String>()
-    };
-
-    let mut asset_path = AssetKey::from(path.as_str());
-
-    let asset_response = assets
-      .get(&path.as_str().into())
-      .or_else(|| {
-        debug_eprintln!("Asset `{path}` not found; fallback to {path}.html");
-        let fallback = format!("{}.html", path.as_str()).into();
-        let asset = assets.get(&fallback);
-        asset_path = fallback;
-        asset
-      })
-      .or_else(|| {
-        debug_eprintln!(
-          "Asset `{}` not found; fallback to {}/index.html",
-          path,
-          path
-        );
-        let fallback = format!("{}/index.html", path.as_str()).into();
-        let asset = assets.get(&fallback);
-        asset_path = fallback;
-        asset
-      })
-      .or_else(|| {
-        debug_eprintln!("Asset `{}` not found; fallback to index.html", path);
-        let fallback = AssetKey::from("index.html");
-        let asset = assets.get(&fallback);
-        asset_path = fallback;
-        asset
-      })
-      .ok_or_else(|| crate::Error::AssetNotFound(path.clone()))
-      .map(Cow::into_owned);
-
-    let mut csp_header = None;
-    let is_html = asset_path.as_ref().ends_with(".html");
-
-    match asset_response {
-      Ok(asset) => {
-        let final_data = if is_html {
-          let mut asset = String::from_utf8_lossy(&asset).into_owned();
-          if let Some(csp) = self.csp() {
-            csp_header.replace(set_csp(
-              &mut asset,
-              self.inner.assets.clone(),
-              &asset_path,
-              self,
-              csp,
-            ));
-          }
-
-          asset.as_bytes().to_vec()
-        } else {
-          asset
-        };
-        let mime_type = tauri_utils::mime_type::MimeType::parse(&final_data, &path);
-        Ok(Asset {
-          bytes: final_data.to_vec(),
-          mime_type,
-          csp_header,
-        })
-      }
-      Err(e) => {
-        debug_eprintln!("{:?}", e); // TODO log::error!
-        Err(Box::new(e))
-      }
-    }
-  }
-
-  fn initialization_script(
-    &self,
-    ipc_script: &str,
-    pattern_script: &str,
-    plugin_initialization_script: &str,
-    with_global_tauri: bool,
-  ) -> crate::Result<String> {
-    #[derive(Template)]
-    #[default_template("../scripts/init.js")]
-    struct InitJavascript<'a> {
-      #[raw]
-      pattern_script: &'a str,
-      #[raw]
-      ipc_script: &'a str,
-      #[raw]
-      bundle_script: &'a str,
-      #[raw]
-      core_script: &'a str,
-      #[raw]
-      event_initialization_script: &'a str,
-      #[raw]
-      plugin_initialization_script: &'a str,
-      #[raw]
-      freeze_prototype: &'a str,
-    }
-
-    #[derive(Template)]
-    #[default_template("../scripts/core.js")]
-    struct CoreJavascript<'a> {
-      os_name: &'a str,
-    }
-
-    let bundle_script = if with_global_tauri {
-      include_str!("../scripts/bundle.global.js")
-    } else {
-      ""
-    };
-
-    let freeze_prototype = if self.inner.config.tauri.security.freeze_prototype {
-      include_str!("../scripts/freeze_prototype.js")
-    } else {
-      ""
-    };
-
-    InitJavascript {
-      pattern_script,
-      ipc_script,
-      bundle_script,
-      core_script: &CoreJavascript {
-        os_name: std::env::consts::OS,
-      }
-      .render_default(&Default::default())?
-      .into_string(),
-      event_initialization_script: &crate::event::event_initialization_script(
-        self.listeners().function_name(),
-        self.listeners().listeners_object_name(),
-      ),
-      plugin_initialization_script,
-      freeze_prototype,
-    }
-    .render_default(&Default::default())
-    .map(|s| s.into_string())
-    .map_err(Into::into)
-  }
-
-  pub(crate) fn listeners(&self) -> &Listeners<R> {
-    &self.inner.listeners
-  }
-}
-
-impl<R: Runtime> WindowManager<R> {
-  pub fn run_invoke_handler(&self, invoke: Invoke<R>) -> bool {
-    (self.inner.invoke_handler)(invoke)
-  }
-
-  pub fn extend_api(&self, plugin: &str, invoke: Invoke<R>) -> bool {
-    self
-      .inner
-      .plugins
-      .lock()
-      .expect("poisoned plugin store")
-      .extend_api(plugin, invoke)
-  }
-
-  pub fn initialize_plugins(&self, app: &AppHandle<R>) -> crate::Result<()> {
-    self
-      .inner
-      .plugins
-      .lock()
-      .expect("poisoned plugin store")
-      .initialize(app, &self.inner.config.plugins)
-  }
-
-  pub fn prepare_window(
-    &self,
-    app_handle: AppHandle<R>,
-    mut pending: PendingWindow<EventLoopMessage, R>,
-    window_labels: &[String],
-  ) -> crate::Result<PendingWindow<EventLoopMessage, R>> {
-    if self.windows_lock().contains_key(&pending.label) {
-      return Err(crate::Error::WindowLabelAlreadyExists(pending.label));
-    }
-    #[allow(unused_mut)] // mut url only for the data-url parsing
-    let mut url = match &pending.webview_attributes.url {
-      WindowUrl::App(path) => {
-        let url = if PROXY_DEV_SERVER {
-          Cow::Owned(Url::parse("tauri://localhost").unwrap())
-        } else {
-          self.get_url()
-        };
-        // ignore "index.html" just to simplify the url
-        if path.to_str() != Some("index.html") {
-          url
-            .join(&path.to_string_lossy())
-            .map_err(crate::Error::InvalidUrl)
-            // this will never fail
-            .unwrap()
-        } else {
-          url.into_owned()
-        }
-      }
-      WindowUrl::External(url) => {
-        let config_url = self.get_url();
-        let is_local = config_url.make_relative(url).is_some();
-        let mut url = url.clone();
-        if is_local && PROXY_DEV_SERVER {
-          url.set_scheme("tauri").unwrap();
-          url.set_host(Some("localhost")).unwrap();
-        }
-        url
-      }
-      _ => unimplemented!(),
-    };
-
-    #[cfg(not(feature = "window-data-url"))]
-    if url.scheme() == "data" {
-      return Err(crate::Error::InvalidWindowUrl(
-        "data URLs are not supported without the `window-data-url` feature.",
-      ));
-    }
-
-    #[cfg(feature = "window-data-url")]
-    if let Some(csp) = self.csp() {
-      if url.scheme() == "data" {
-        if let Ok(data_url) = data_url::DataUrl::process(url.as_str()) {
-          let (body, _) = data_url.decode_to_vec().unwrap();
-          let html = String::from_utf8_lossy(&body).into_owned();
-          // naive way to check if it's an html
-          if html.contains('<') && html.contains('>') {
-            let document = tauri_utils::html::parse(html);
-            tauri_utils::html::inject_csp(&document, &csp.to_string());
-            url.set_path(&format!("{},{}", mime::TEXT_HTML, document.to_string()));
-          }
-        }
-      }
-    }
-
-    pending.url = url.to_string();
-
-    if !pending.window_builder.has_icon() {
-      if let Some(default_window_icon) = self.inner.default_window_icon.clone() {
-        pending.window_builder = pending
-          .window_builder
-          .icon(default_window_icon.try_into()?)?;
-      }
-    }
-
-    #[cfg(target_os = "android")]
-    {
-      pending = pending.on_webview_created(move |ctx| {
-        let plugin_manager = ctx
-          .env
-          .call_method(
-            ctx.activity,
-            "getPluginManager",
-            "()Lapp/tauri/plugin/PluginManager;",
-            &[],
-          )?
-          .l()?;
-
-        // tell the manager the webview is ready
-        ctx.env.call_method(
-          plugin_manager,
-          "onWebViewCreated",
-          "(Landroid/webkit/WebView;)V",
-          &[ctx.webview.into()],
-        )?;
-
-        Ok(())
-      });
-    }
-
-    let label = pending.label.clone();
-    pending = self.prepare_pending_window(
-      pending,
-      &label,
-      window_labels,
-      #[allow(clippy::redundant_clone)]
-      app_handle.clone(),
-    )?;
-
-    #[cfg(any(target_os = "macos", target_os = "ios", not(ipc_custom_protocol)))]
-    {
-      pending.ipc_handler = Some(crate::ipc::protocol::message_handler(self.clone()));
-    }
-
-    // in `Windows`, we need to force a data_directory
-    // but we do respect user-specification
-    #[cfg(any(target_os = "linux", target_os = "windows"))]
-    if pending.webview_attributes.data_directory.is_none() {
-      let local_app_data = app_handle.path().resolve(
-        &self.inner.config.tauri.bundle.identifier,
-        BaseDirectory::LocalData,
-      );
-      if let Ok(user_data_dir) = local_app_data {
-        pending.webview_attributes.data_directory = Some(user_data_dir);
-      }
-    }
-
-    // make sure the directory is created and available to prevent a panic
-    if let Some(user_data_dir) = &pending.webview_attributes.data_directory {
-      if !user_data_dir.exists() {
-        create_dir_all(user_data_dir)?;
-      }
-    }
-
-    #[cfg(feature = "isolation")]
-    let pattern = self.pattern().clone();
-    let navigation_handler = pending.navigation_handler.take();
-    let manager = self.inner.clone();
-    let label = pending.label.clone();
-    pending.navigation_handler = Some(Box::new(move |url| {
-      // always allow navigation events for the isolation iframe and do not emit them for consumers
-      #[cfg(feature = "isolation")]
-      if let Pattern::Isolation { schema, .. } = &pattern {
-        if url.scheme() == schema
-          && url.domain() == Some(crate::pattern::ISOLATION_IFRAME_SRC_DOMAIN)
-        {
-          return true;
-        }
-      }
-      if let Some(handler) = &navigation_handler {
-        if !handler(url) {
-          return false;
-        }
-      }
-      let window = manager.windows.lock().unwrap().get(&label).cloned();
-      if let Some(w) = window {
-        manager
-          .plugins
-          .lock()
-          .expect("poisoned plugin store")
-          .on_navigation(&w, url)
-      } else {
-        true
-      }
-    }));
-
-    Ok(pending)
-  }
-
-  pub(crate) fn attach_window(
-    &self,
-    app_handle: AppHandle<R>,
-    window: DetachedWindow<EventLoopMessage, R>,
-    #[cfg(desktop)] menu: Option<crate::window::WindowMenu<R>>,
-  ) -> Window<R> {
-    let window = Window::new(
-      self.clone(),
-      window,
-      app_handle,
-      #[cfg(desktop)]
-      menu,
-    );
-
-    let window_ = window.clone();
-    let window_event_listeners = self.inner.window_event_listeners.clone();
-    let manager = self.clone();
-    window.on_window_event(move |event| {
-      let _ = on_window_event(&window_, &manager, event);
-      for handler in window_event_listeners.iter() {
-        handler(GlobalWindowEvent {
-          window: window_.clone(),
-          event: event.clone(),
-        });
-      }
-    });
-
-    // insert the window into our manager
-    {
-      self
-        .windows_lock()
-        .insert(window.label().to_string(), window.clone());
-    }
-
-    // let plugins know that a new window has been added to the manager
-    let manager = self.inner.clone();
-    let window_ = window.clone();
-    // run on main thread so the plugin store doesn't dead lock with the event loop handler in App
-    let _ = window.run_on_main_thread(move || {
-      manager
-        .plugins
-        .lock()
-        .expect("poisoned plugin store")
-        .created(window_);
-    });
-
-    #[cfg(target_os = "ios")]
-    {
-      window
-        .with_webview(|w| {
-          unsafe { crate::ios::on_webview_created(w.inner() as _, w.view_controller() as _) };
-        })
-        .expect("failed to run on_webview_created hook");
-    }
-
-    window
-  }
-
-  pub(crate) fn on_window_close(&self, label: &str) {
-    self.windows_lock().remove(label);
-  }
-
-  pub fn eval_script_all<S: Into<String>>(&self, script: S) -> crate::Result<()> {
-    let script = script.into();
-    self
-      .windows_lock()
-      .values()
-      .try_for_each(|window| window.eval(&script))
-  }
-
-  pub fn labels(&self) -> HashSet<String> {
-    self.windows_lock().keys().cloned().collect()
-  }
-
-  pub fn config(&self) -> Arc<Config> {
-    self.inner.config.clone()
-  }
-
-  pub fn package_info(&self) -> &PackageInfo {
-    &self.inner.package_info
-  }
-
-  pub fn listen<F: Fn(Event) + Send + 'static>(
-    &self,
-    event: String,
-    window: Option<Window<R>>,
-    handler: F,
-  ) -> EventId {
-    assert_event_name_is_valid(&event);
-    self.listeners().listen(event, window, handler)
-  }
-
-  pub fn unlisten(&self, id: EventId) {
-    self.listeners().unlisten(id)
-  }
-
-  pub fn once<F: FnOnce(Event) + Send + 'static>(
-    &self,
-    event: String,
-    window: Option<String>,
-    handler: F,
-  ) {
-    assert_event_name_is_valid(&event);
-    self
-      .listeners()
-      .once(event, window.and_then(|w| self.get_window(&w)), handler)
-  }
-
-  pub fn emit_filter<S, F>(
-    &self,
-    event: &str,
-    source_window_label: Option<&str>,
-    payload: S,
-    filter: F,
-  ) -> crate::Result<()>
-  where
-    S: Serialize + Clone,
-    F: Fn(&Window<R>) -> bool,
-  {
-    assert_event_name_is_valid(event);
-
-    let emit_args = EmitArgs::from(event, source_window_label, payload)?;
-
-    self
-      .windows_lock()
-      .values()
-      .filter(|w| {
-        w.has_js_listener(None, event)
-          || w.has_js_listener(source_window_label.map(Into::into), event)
-      })
-      .filter(|w| filter(w))
-      .try_for_each(|window| window.emit_js(&emit_args))?;
-
-    self.listeners().emit_filter(&emit_args, Some(filter))?;
-
-    Ok(())
-  }
-
-  pub fn emit<S: Serialize + Clone>(
-    &self,
-    event: &str,
-    source_window_label: Option<&str>,
-    payload: S,
-  ) -> crate::Result<()> {
-    assert_event_name_is_valid(event);
-
-    let emit_args = EmitArgs::from(event, source_window_label, payload)?;
-
-    self
-      .windows_lock()
-      .values()
-      .filter(|w| {
-        w.has_js_listener(None, event)
-          || w.has_js_listener(source_window_label.map(Into::into), event)
-      })
-      .try_for_each(|window| window.emit_js(&emit_args))?;
-
-    self.listeners().emit(&emit_args)?;
-
-    Ok(())
-  }
-
-  pub fn get_window(&self, label: &str) -> Option<Window<R>> {
-    self.windows_lock().get(label).cloned()
-  }
-
-  pub fn get_focused_window(&self) -> Option<Window<R>> {
-    self
-      .windows_lock()
-      .iter()
-      .find(|w| w.1.is_focused().unwrap_or(false))
-      .map(|w| w.1.clone())
-  }
-
-  pub fn windows(&self) -> HashMap<String, Window<R>> {
-    self.windows_lock().clone()
-  }
-}
-
-#[derive(Serialize, Clone)]
-struct FileDropPayload<'a> {
-  paths: &'a Vec<PathBuf>,
-  position: &'a PhysicalPosition<f64>,
-}
-
-fn on_window_event<R: Runtime>(
-  window: &Window<R>,
-  manager: &WindowManager<R>,
-  event: &WindowEvent,
-) -> crate::Result<()> {
-  match event {
-    WindowEvent::Resized(size) => window.emit(WINDOW_RESIZED_EVENT, size)?,
-    WindowEvent::Moved(position) => window.emit(WINDOW_MOVED_EVENT, position)?,
-    WindowEvent::CloseRequested { api } => {
-      if window.has_js_listener(Some(window.label().into()), WINDOW_CLOSE_REQUESTED_EVENT) {
-        api.prevent_close();
-      }
-      window.emit(WINDOW_CLOSE_REQUESTED_EVENT, ())?;
-    }
-    WindowEvent::Destroyed => {
-      window.emit(WINDOW_DESTROYED_EVENT, ())?;
-      let label = window.label();
-      let windows_map = manager.inner.windows.lock().unwrap();
-      let windows = windows_map.values();
-      for window in windows {
-        window.eval(&format!(
-          r#"(function () {{ const metadata = window.__TAURI_INTERNALS__.metadata; if (metadata != null) {{ metadata.windows = window.__TAURI_INTERNALS__.metadata.windows.filter(w => w.label !== "{label}"); }} }})()"#,
-        ))?;
-      }
-    }
-    WindowEvent::Focused(focused) => window.emit(
-      if *focused {
-        WINDOW_FOCUS_EVENT
-      } else {
-        WINDOW_BLUR_EVENT
-      },
-      (),
-    )?,
-    WindowEvent::ScaleFactorChanged {
-      scale_factor,
-      new_inner_size,
-      ..
-    } => window.emit(
-      WINDOW_SCALE_FACTOR_CHANGED_EVENT,
-      ScaleFactorChanged {
-        scale_factor: *scale_factor,
-        size: *new_inner_size,
-      },
-    )?,
-    WindowEvent::FileDrop(event) => match event {
-      FileDropEvent::Hovered { paths, position } => {
-        let payload = FileDropPayload { paths, position };
-        window.emit(WINDOW_FILE_DROP_HOVER_EVENT, payload)?
-      }
-      FileDropEvent::Dropped { paths, position } => {
-        let scopes = window.state::<Scopes>();
-        for path in paths {
-          if path.is_file() {
-            let _ = scopes.allow_file(path);
-          } else {
-            let _ = scopes.allow_directory(path, false);
-          }
-        }
-        let payload = FileDropPayload { paths, position };
-        window.emit(WINDOW_FILE_DROP_EVENT, payload)?
-      }
-      FileDropEvent::Cancelled => window.emit(WINDOW_FILE_DROP_CANCELLED_EVENT, ())?,
-      _ => unimplemented!(),
-    },
-    WindowEvent::ThemeChanged(theme) => window.emit(WINDOW_THEME_CHANGED, theme.to_string())?,
-  }
-  Ok(())
-}
-
-#[derive(Clone, Serialize)]
-#[serde(rename_all = "camelCase")]
-struct ScaleFactorChanged {
-  scale_factor: f64,
-  size: PhysicalSize<u32>,
-}
-
-#[cfg(test)]
-mod tests {
-  use super::replace_with_callback;
-
-  #[test]
-  fn string_replace_with_callback() {
-    let mut tauri_index = 0;
-    #[allow(clippy::single_element_loop)]
-    for (src, pattern, replacement, result) in [(
-      "tauri is awesome, tauri is amazing",
-      "tauri",
-      || {
-        tauri_index += 1;
-        tauri_index.to_string()
-      },
-      "1 is awesome, 2 is amazing",
-    )] {
-      assert_eq!(replace_with_callback(src, pattern, replacement), result);
-    }
-  }
-}
-
-#[cfg(test)]
-mod test {
-  use std::{
-    sync::mpsc::{channel, Receiver, Sender},
-    time::Duration,
-  };
-
-  use crate::{
-    generate_context,
-    plugin::PluginStore,
-    test::{mock_app, MockRuntime},
-    App, Manager, StateManager, Window, WindowBuilder, Wry,
-  };
-
-  use super::WindowManager;
-
-  const WINDOW_LISTEN_ID: &str = "Window::listen";
-  const WINDOW_LISTEN_GLOBAL_ID: &str = "Window::listen_global";
-  const APP_LISTEN_GLOBAL_ID: &str = "App::listen_global";
-  const TEST_EVENT_NAME: &str = "event";
-
-  #[test]
-  fn check_get_url() {
-    let context = generate_context!("test/fixture/src-tauri/tauri.conf.json", crate);
-    let manager: WindowManager<Wry> = WindowManager::with_handlers(
-      context,
-      PluginStore::default(),
-      Box::new(|_| false),
-      None,
-      Default::default(),
-      StateManager::new(),
-      Default::default(),
-      Default::default(),
-      (None, "".into()),
-    );
-
-    #[cfg(custom_protocol)]
-    {
-      assert_eq!(
-        manager.get_url().to_string(),
-        if cfg!(windows) || cfg!(target_os = "android") {
-          "http://tauri.localhost/"
-        } else {
-          "tauri://localhost"
-        }
-      );
-    }
-
-    #[cfg(dev)]
-    assert_eq!(manager.get_url().to_string(), "http://localhost:4000/");
-  }
-
-  struct EventSetup {
-    app: App<MockRuntime>,
-    window: Window<MockRuntime>,
-    tx: Sender<(&'static str, String)>,
-    rx: Receiver<(&'static str, String)>,
-  }
-
-  fn setup_events() -> EventSetup {
-    let app = mock_app();
-    let window = WindowBuilder::new(&app, "main", Default::default())
-      .build()
-      .unwrap();
-
-    let (tx, rx) = channel();
-
-    let tx_ = tx.clone();
-    window.listen(TEST_EVENT_NAME, move |evt| {
-      tx_
-        .send((
-          WINDOW_LISTEN_ID,
-          serde_json::from_str::<String>(evt.payload()).unwrap(),
-        ))
-        .unwrap();
-    });
-
-    let tx_ = tx.clone();
-    window.listen_global(TEST_EVENT_NAME, move |evt| {
-      tx_
-        .send((
-          WINDOW_LISTEN_GLOBAL_ID,
-          serde_json::from_str::<String>(evt.payload()).unwrap(),
-        ))
-        .unwrap();
-    });
-
-    let tx_ = tx.clone();
-    app.listen_global(TEST_EVENT_NAME, move |evt| {
-      tx_
-        .send((
-          APP_LISTEN_GLOBAL_ID,
-          serde_json::from_str::<String>(evt.payload()).unwrap(),
-        ))
-        .unwrap();
-    });
-
-    EventSetup {
-      app,
-      window,
-      tx,
-      rx,
-    }
-  }
-
-  fn assert_events(received: &[&str], expected: &[&str]) {
-    for e in expected {
-      assert!(received.contains(e), "{e} did not receive global event");
-    }
-    assert_eq!(
-      received.len(),
-      expected.len(),
-      "received {:?} events but expected {:?}",
-      received,
-      expected
-    );
-  }
-
-  #[test]
-  fn app_global_events() {
-    let EventSetup {
-      app,
-      window: _,
-      tx: _,
-      rx,
-    } = setup_events();
-
-    let mut received = Vec::new();
-    let payload = "global-payload";
-    app.emit(TEST_EVENT_NAME, payload).unwrap();
-    while let Ok((source, p)) = rx.recv_timeout(Duration::from_secs(1)) {
-      assert_eq!(p, payload);
-      received.push(source);
-    }
-    assert_events(
-      &received,
-      &[
-        WINDOW_LISTEN_ID,
-        WINDOW_LISTEN_GLOBAL_ID,
-        APP_LISTEN_GLOBAL_ID,
-      ],
-    );
-  }
-
-  #[test]
-  fn window_global_events() {
-    let EventSetup {
-      app: _,
-      window,
-      tx: _,
-      rx,
-    } = setup_events();
-
-    let mut received = Vec::new();
-    let payload = "global-payload";
-    window.emit(TEST_EVENT_NAME, payload).unwrap();
-    while let Ok((source, p)) = rx.recv_timeout(Duration::from_secs(1)) {
-      assert_eq!(p, payload);
-      received.push(source);
-    }
-    assert_events(
-      &received,
-      &[
-        WINDOW_LISTEN_ID,
-        WINDOW_LISTEN_GLOBAL_ID,
-        APP_LISTEN_GLOBAL_ID,
-      ],
-    );
-  }
-
-  #[test]
-  fn window_local_events() {
-    let EventSetup {
-      app,
-      window,
-      tx,
-      rx,
-    } = setup_events();
-
-    let mut received = Vec::new();
-    let payload = "global-payload";
-    window
-      .emit_to(window.label(), TEST_EVENT_NAME, payload)
-      .unwrap();
-    while let Ok((source, p)) = rx.recv_timeout(Duration::from_secs(1)) {
-      assert_eq!(p, payload);
-      received.push(source);
-    }
-    assert_events(&received, &[WINDOW_LISTEN_ID]);
-
-    received.clear();
-    let other_window_listen_id = "OtherWindow::listen";
-    let other_window = WindowBuilder::new(&app, "other", Default::default())
-      .build()
-      .unwrap();
-    other_window.listen(TEST_EVENT_NAME, move |evt| {
-      tx.send((
-        other_window_listen_id,
-        serde_json::from_str::<String>(evt.payload()).unwrap(),
-      ))
-      .unwrap();
-    });
-    window
-      .emit_to(other_window.label(), TEST_EVENT_NAME, payload)
-      .unwrap();
-    while let Ok((source, p)) = rx.recv_timeout(Duration::from_secs(1)) {
-      assert_eq!(p, payload);
-      received.push(source);
-    }
-    assert_events(&received, &[other_window_listen_id]);
-  }
-}

+ 90 - 0
core/tauri/src/manager/menu.rs

@@ -0,0 +1,90 @@
+// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use std::{
+  collections::HashMap,
+  sync::{Arc, Mutex, MutexGuard},
+};
+
+use crate::{
+  menu::{Menu, MenuId},
+  AppHandle, Runtime, Window,
+};
+
+pub struct MenuManager<R: Runtime> {
+  /// A set containing a reference to the active menus, including
+  /// the app-wide menu and the window-specific menus
+  ///
+  /// This should be mainly used to acceess [`Menu::haccel`]
+  /// to setup the accelerator handling in the event loop
+  pub menus: Arc<Mutex<HashMap<MenuId, Menu<R>>>>,
+  /// The menu set to all windows.
+  pub menu: Mutex<Option<Menu<R>>>,
+  /// Menu event listeners to all windows.
+  pub global_event_listeners: Mutex<Vec<crate::app::GlobalMenuEventListener<AppHandle<R>>>>,
+  /// Menu event listeners to specific windows.
+  pub event_listeners: Mutex<HashMap<String, crate::app::GlobalMenuEventListener<Window<R>>>>,
+}
+
+impl<R: Runtime> MenuManager<R> {
+  /// App-wide menu.
+  pub fn menu_lock(&self) -> MutexGuard<'_, Option<Menu<R>>> {
+    self.menu.lock().expect("poisoned menu mutex")
+  }
+
+  pub fn menus_stash_lock(&self) -> MutexGuard<'_, HashMap<MenuId, Menu<R>>> {
+    self.menus.lock().expect("poisoned menu mutex")
+  }
+
+  pub fn is_menu_in_use<I: PartialEq<MenuId>>(&self, id: &I) -> bool {
+    self
+      .menu_lock()
+      .as_ref()
+      .map(|m| id.eq(m.id()))
+      .unwrap_or(false)
+  }
+
+  pub fn insert_menu_into_stash(&self, menu: &Menu<R>) {
+    self
+      .menus_stash_lock()
+      .insert(menu.id().clone(), menu.clone());
+  }
+
+  pub(crate) fn prepare_window_menu_creation_handler(
+    &self,
+    window_menu: Option<&crate::window::WindowMenu<R>>,
+  ) -> Option<impl Fn(tauri_runtime::window::RawWindow<'_>)> {
+    {
+      if let Some(menu) = window_menu {
+        self
+          .menus_stash_lock()
+          .insert(menu.menu.id().clone(), menu.menu.clone());
+      }
+    }
+
+    #[cfg(target_os = "macos")]
+    return None;
+
+    #[cfg_attr(target_os = "macos", allow(unused_variables, unreachable_code))]
+    if let Some(menu) = &window_menu {
+      let menu = menu.menu.clone();
+      Some(move |raw: tauri_runtime::window::RawWindow<'_>| {
+        #[cfg(target_os = "windows")]
+        let _ = menu.inner().init_for_hwnd(raw.hwnd as _);
+        #[cfg(any(
+          target_os = "linux",
+          target_os = "dragonfly",
+          target_os = "freebsd",
+          target_os = "netbsd",
+          target_os = "openbsd"
+        ))]
+        let _ = menu
+          .inner()
+          .init_for_gtk_window(raw.gtk_window, raw.default_vbox);
+      })
+    } else {
+      None
+    }
+  }
+}

+ 789 - 0
core/tauri/src/manager/mod.rs

@@ -0,0 +1,789 @@
+// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use std::{
+  borrow::Cow,
+  collections::HashMap,
+  fmt,
+  sync::{Arc, Mutex},
+};
+
+use serde::Serialize;
+use url::Url;
+
+use tauri_macros::default_runtime;
+use tauri_utils::debug_eprintln;
+use tauri_utils::{
+  assets::{AssetKey, CspHash},
+  config::{Csp, CspDirectiveSources},
+  html::{SCRIPT_NONCE_TOKEN, STYLE_NONCE_TOKEN},
+};
+
+use crate::event::EmitArgs;
+use crate::{
+  app::{AppHandle, GlobalWindowEventListener, OnPageLoad},
+  event::{assert_event_name_is_valid, Event, EventId, Listeners},
+  ipc::{Invoke, InvokeHandler, InvokeResponder},
+  plugin::PluginStore,
+  utils::{
+    assets::Assets,
+    config::{AppUrl, Config, WindowUrl},
+    PackageInfo,
+  },
+  Context, Pattern, Runtime, StateManager, Window,
+};
+
+#[cfg(desktop)]
+mod menu;
+#[cfg(all(desktop, feature = "tray-icon"))]
+mod tray;
+pub mod window;
+
+#[derive(Default)]
+/// Spaced and quoted Content-Security-Policy hash values.
+struct CspHashStrings {
+  script: Vec<String>,
+  style: Vec<String>,
+}
+
+/// Sets the CSP value to the asset HTML if needed (on Linux).
+/// Returns the CSP string for access on the response header (on Windows and macOS).
+#[allow(clippy::borrowed_box)]
+fn set_csp<R: Runtime>(
+  asset: &mut String,
+  assets: &Box<dyn Assets>,
+  asset_path: &AssetKey,
+  manager: &AppManager<R>,
+  csp: Csp,
+) -> String {
+  let mut csp = csp.into();
+  let hash_strings =
+    assets
+      .csp_hashes(asset_path)
+      .fold(CspHashStrings::default(), |mut acc, hash| {
+        match hash {
+          CspHash::Script(hash) => {
+            acc.script.push(hash.into());
+          }
+          CspHash::Style(hash) => {
+            acc.style.push(hash.into());
+          }
+          _csp_hash => {
+            debug_eprintln!("Unknown CspHash variant encountered: {:?}", _csp_hash);
+          }
+        }
+
+        acc
+      });
+
+  let dangerous_disable_asset_csp_modification = &manager
+    .config()
+    .tauri
+    .security
+    .dangerous_disable_asset_csp_modification;
+  if dangerous_disable_asset_csp_modification.can_modify("script-src") {
+    replace_csp_nonce(
+      asset,
+      SCRIPT_NONCE_TOKEN,
+      &mut csp,
+      "script-src",
+      hash_strings.script,
+    );
+  }
+
+  if dangerous_disable_asset_csp_modification.can_modify("style-src") {
+    replace_csp_nonce(
+      asset,
+      STYLE_NONCE_TOKEN,
+      &mut csp,
+      "style-src",
+      hash_strings.style,
+    );
+  }
+
+  #[cfg(feature = "isolation")]
+  if let Pattern::Isolation { schema, .. } = &*manager.pattern {
+    let default_src = csp
+      .entry("default-src".into())
+      .or_insert_with(Default::default);
+    default_src.push(crate::pattern::format_real_schema(schema));
+  }
+
+  Csp::DirectiveMap(csp).to_string()
+}
+
+// inspired by https://github.com/rust-lang/rust/blob/1be5c8f90912c446ecbdc405cbc4a89f9acd20fd/library/alloc/src/str.rs#L260-L297
+fn replace_with_callback<F: FnMut() -> String>(
+  original: &str,
+  pattern: &str,
+  mut replacement: F,
+) -> String {
+  let mut result = String::new();
+  let mut last_end = 0;
+  for (start, part) in original.match_indices(pattern) {
+    result.push_str(unsafe { original.get_unchecked(last_end..start) });
+    result.push_str(&replacement());
+    last_end = start + part.len();
+  }
+  result.push_str(unsafe { original.get_unchecked(last_end..original.len()) });
+  result
+}
+
+fn replace_csp_nonce(
+  asset: &mut String,
+  token: &str,
+  csp: &mut HashMap<String, CspDirectiveSources>,
+  directive: &str,
+  hashes: Vec<String>,
+) {
+  let mut nonces = Vec::new();
+  *asset = replace_with_callback(asset, token, || {
+    #[cfg(target_pointer_width = "64")]
+    let mut raw = [0u8; 8];
+    #[cfg(target_pointer_width = "32")]
+    let mut raw = [0u8; 4];
+    #[cfg(target_pointer_width = "16")]
+    let mut raw = [0u8; 2];
+    getrandom::getrandom(&mut raw).expect("failed to get random bytes");
+    let nonce = usize::from_ne_bytes(raw);
+    nonces.push(nonce);
+    nonce.to_string()
+  });
+
+  if !(nonces.is_empty() && hashes.is_empty()) {
+    let nonce_sources = nonces
+      .into_iter()
+      .map(|n| format!("'nonce-{n}'"))
+      .collect::<Vec<String>>();
+    let sources = csp.entry(directive.into()).or_default();
+    let self_source = "'self'".to_string();
+    if !sources.contains(&self_source) {
+      sources.push(self_source);
+    }
+    sources.extend(nonce_sources);
+    sources.extend(hashes);
+  }
+}
+
+/// A resolved asset.
+pub struct Asset {
+  /// The asset bytes.
+  pub bytes: Vec<u8>,
+  /// The asset's mime type.
+  pub mime_type: String,
+  /// The `Content-Security-Policy` header value.
+  pub csp_header: Option<String>,
+}
+
+#[default_runtime(crate::Wry, wry)]
+pub struct AppManager<R: Runtime> {
+  pub window: window::WindowManager<R>,
+  #[cfg(all(desktop, feature = "tray-icon"))]
+  pub tray: tray::TrayManager<R>,
+  #[cfg(desktop)]
+  pub menu: menu::MenuManager<R>,
+
+  pub(crate) plugins: Mutex<PluginStore<R>>,
+  pub listeners: Listeners<R>,
+  pub state: Arc<StateManager>,
+  pub config: Config,
+  pub assets: Box<dyn Assets>,
+
+  pub app_icon: Option<Vec<u8>>,
+
+  pub package_info: PackageInfo,
+
+  /// Application pattern.
+  pub pattern: Arc<Pattern>,
+}
+
+impl<R: Runtime> fmt::Debug for AppManager<R> {
+  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+    let mut d = f.debug_struct("AppManager");
+
+    d.field("window", &self.window)
+      .field("plugins", &self.plugins)
+      .field("state", &self.state)
+      .field("config", &self.config)
+      .field("app_icon", &self.app_icon)
+      .field("package_info", &self.package_info)
+      .field("pattern", &self.pattern);
+
+    #[cfg(all(desktop, feature = "tray-icon"))]
+    {
+      d.field("tray", &self.tray);
+    }
+
+    d.finish()
+  }
+}
+
+impl<R: Runtime> AppManager<R> {
+  #[allow(clippy::too_many_arguments, clippy::type_complexity)]
+  pub(crate) fn with_handlers(
+    #[allow(unused_mut)] mut context: Context<impl Assets>,
+    plugins: PluginStore<R>,
+    invoke_handler: Box<InvokeHandler<R>>,
+    on_page_load: Option<Arc<OnPageLoad<R>>>,
+    uri_scheme_protocols: HashMap<String, Arc<window::UriSchemeProtocol<R>>>,
+    state: StateManager,
+    window_event_listeners: Vec<GlobalWindowEventListener<R>>,
+    #[cfg(desktop)] window_menu_event_listeners: HashMap<
+      String,
+      crate::app::GlobalMenuEventListener<Window<R>>,
+    >,
+    (invoke_responder, invoke_initialization_script): (Option<Arc<InvokeResponder<R>>>, String),
+  ) -> Self {
+    // generate a random isolation key at runtime
+    #[cfg(feature = "isolation")]
+    if let Pattern::Isolation { ref mut key, .. } = &mut context.pattern {
+      *key = uuid::Uuid::new_v4().to_string();
+    }
+
+    Self {
+      window: window::WindowManager {
+        windows: Mutex::default(),
+        invoke_handler,
+        on_page_load,
+        default_icon: context.default_window_icon,
+        uri_scheme_protocols: Mutex::new(uri_scheme_protocols),
+        event_listeners: Arc::new(window_event_listeners),
+        invoke_responder,
+        invoke_initialization_script,
+      },
+      #[cfg(all(desktop, feature = "tray-icon"))]
+      tray: tray::TrayManager {
+        icon: context.tray_icon,
+        icons: Default::default(),
+        global_event_listeners: Default::default(),
+        event_listeners: Default::default(),
+      },
+      #[cfg(desktop)]
+      menu: menu::MenuManager {
+        menus: Default::default(),
+        menu: Default::default(),
+        global_event_listeners: Default::default(),
+        event_listeners: Mutex::new(window_menu_event_listeners),
+      },
+      plugins: Mutex::new(plugins),
+      listeners: Listeners::default(),
+      state: Arc::new(state),
+      config: context.config,
+      assets: context.assets,
+      app_icon: context.app_icon,
+      package_info: context.package_info,
+      pattern: Arc::new(context.pattern),
+    }
+  }
+
+  /// State managed by the application.
+  pub(crate) fn state(&self) -> Arc<StateManager> {
+    self.state.clone()
+  }
+
+  /// Get the base path to serve data from.
+  ///
+  /// * In dev mode, this will be based on the `devPath` configuration value.
+  /// * Otherwise, this will be based on the `distDir` configuration value.
+  #[cfg(not(dev))]
+  fn base_path(&self) -> &AppUrl {
+    &self.config.build.dist_dir
+  }
+
+  #[cfg(dev)]
+  fn base_path(&self) -> &AppUrl {
+    &self.config.build.dev_path
+  }
+
+  /// Get the base URL to use for webview requests.
+  ///
+  /// In dev mode, this will be based on the `devPath` configuration value.
+  pub(crate) fn get_url(&self) -> Cow<'_, Url> {
+    match self.base_path() {
+      AppUrl::Url(WindowUrl::External(url)) => Cow::Borrowed(url),
+      _ => self.protocol_url(),
+    }
+  }
+
+  pub(crate) fn protocol_url(&self) -> Cow<'_, Url> {
+    if cfg!(windows) || cfg!(target_os = "android") {
+      Cow::Owned(Url::parse("http://tauri.localhost").unwrap())
+    } else {
+      Cow::Owned(Url::parse("tauri://localhost").unwrap())
+    }
+  }
+
+  fn csp(&self) -> Option<Csp> {
+    if cfg!(feature = "custom-protocol") {
+      self.config.tauri.security.csp.clone()
+    } else {
+      self
+        .config
+        .tauri
+        .security
+        .dev_csp
+        .clone()
+        .or_else(|| self.config.tauri.security.csp.clone())
+    }
+  }
+
+  pub fn get_asset(&self, mut path: String) -> Result<Asset, Box<dyn std::error::Error>> {
+    let assets = &self.assets;
+    if path.ends_with('/') {
+      path.pop();
+    }
+    path = percent_encoding::percent_decode(path.as_bytes())
+      .decode_utf8_lossy()
+      .to_string();
+    let path = if path.is_empty() {
+      // if the url is `tauri://localhost`, we should load `index.html`
+      "index.html".to_string()
+    } else {
+      // skip leading `/`
+      path.chars().skip(1).collect::<String>()
+    };
+
+    let mut asset_path = AssetKey::from(path.as_str());
+
+    let asset_response = assets
+      .get(&path.as_str().into())
+      .or_else(|| {
+        debug_eprintln!("Asset `{path}` not found; fallback to {path}.html");
+        let fallback = format!("{}.html", path.as_str()).into();
+        let asset = assets.get(&fallback);
+        asset_path = fallback;
+        asset
+      })
+      .or_else(|| {
+        debug_eprintln!(
+          "Asset `{}` not found; fallback to {}/index.html",
+          path,
+          path
+        );
+        let fallback = format!("{}/index.html", path.as_str()).into();
+        let asset = assets.get(&fallback);
+        asset_path = fallback;
+        asset
+      })
+      .or_else(|| {
+        debug_eprintln!("Asset `{}` not found; fallback to index.html", path);
+        let fallback = AssetKey::from("index.html");
+        let asset = assets.get(&fallback);
+        asset_path = fallback;
+        asset
+      })
+      .ok_or_else(|| crate::Error::AssetNotFound(path.clone()))
+      .map(Cow::into_owned);
+
+    let mut csp_header = None;
+    let is_html = asset_path.as_ref().ends_with(".html");
+
+    match asset_response {
+      Ok(asset) => {
+        let final_data = if is_html {
+          let mut asset = String::from_utf8_lossy(&asset).into_owned();
+          if let Some(csp) = self.csp() {
+            csp_header.replace(set_csp(&mut asset, &self.assets, &asset_path, self, csp));
+          }
+
+          asset.as_bytes().to_vec()
+        } else {
+          asset
+        };
+        let mime_type = tauri_utils::mime_type::MimeType::parse(&final_data, &path);
+        Ok(Asset {
+          bytes: final_data.to_vec(),
+          mime_type,
+          csp_header,
+        })
+      }
+      Err(e) => {
+        debug_eprintln!("{:?}", e); // TODO log::error!
+        Err(Box::new(e))
+      }
+    }
+  }
+
+  pub(crate) fn listeners(&self) -> &Listeners<R> {
+    &self.listeners
+  }
+
+  pub fn run_invoke_handler(&self, invoke: Invoke<R>) -> bool {
+    (self.window.invoke_handler)(invoke)
+  }
+
+  pub fn extend_api(&self, plugin: &str, invoke: Invoke<R>) -> bool {
+    self
+      .plugins
+      .lock()
+      .expect("poisoned plugin store")
+      .extend_api(plugin, invoke)
+  }
+
+  pub fn initialize_plugins(&self, app: &AppHandle<R>) -> crate::Result<()> {
+    self
+      .plugins
+      .lock()
+      .expect("poisoned plugin store")
+      .initialize(app, &self.config.plugins)
+  }
+
+  pub fn config(&self) -> &Config {
+    &self.config
+  }
+
+  pub fn package_info(&self) -> &PackageInfo {
+    &self.package_info
+  }
+
+  pub fn listen<F: Fn(Event) + Send + 'static>(
+    &self,
+    event: String,
+    window: Option<Window<R>>,
+    handler: F,
+  ) -> EventId {
+    assert_event_name_is_valid(&event);
+    self.listeners().listen(event, window, handler)
+  }
+
+  pub fn unlisten(&self, id: EventId) {
+    self.listeners().unlisten(id)
+  }
+
+  pub fn once<F: FnOnce(Event) + Send + 'static>(
+    &self,
+    event: String,
+    window: Option<String>,
+    handler: F,
+  ) {
+    assert_event_name_is_valid(&event);
+    self
+      .listeners()
+      .once(event, window.and_then(|w| self.get_window(&w)), handler)
+  }
+
+  pub fn emit_filter<S, F>(
+    &self,
+    event: &str,
+    source_window_label: Option<&str>,
+    payload: S,
+    filter: F,
+  ) -> crate::Result<()>
+  where
+    S: Serialize + Clone,
+    F: Fn(&Window<R>) -> bool,
+  {
+    assert_event_name_is_valid(event);
+
+    let emit_args = EmitArgs::from(event, source_window_label, payload)?;
+
+    self
+      .window
+      .windows_lock()
+      .values()
+      .filter(|w| {
+        w.has_js_listener(None, event)
+          || w.has_js_listener(source_window_label.map(Into::into), event)
+      })
+      .filter(|w| filter(w))
+      .try_for_each(|window| window.emit_js(&emit_args))?;
+
+    self.listeners().emit_filter(&emit_args, Some(filter))?;
+
+    Ok(())
+  }
+
+  pub fn emit<S: Serialize + Clone>(
+    &self,
+    event: &str,
+    source_window_label: Option<&str>,
+    payload: S,
+  ) -> crate::Result<()> {
+    assert_event_name_is_valid(event);
+
+    let emit_args = EmitArgs::from(event, source_window_label, payload)?;
+
+    self
+      .window
+      .windows_lock()
+      .values()
+      .filter(|w| {
+        w.has_js_listener(None, event)
+          || w.has_js_listener(source_window_label.map(Into::into), event)
+      })
+      .try_for_each(|window| window.emit_js(&emit_args))?;
+
+    self.listeners().emit(&emit_args)?;
+
+    Ok(())
+  }
+
+  pub fn get_window(&self, label: &str) -> Option<Window<R>> {
+    self.window.windows_lock().get(label).cloned()
+  }
+
+  pub fn get_focused_window(&self) -> Option<Window<R>> {
+    self
+      .window
+      .windows_lock()
+      .iter()
+      .find(|w| w.1.is_focused().unwrap_or(false))
+      .map(|w| w.1.clone())
+  }
+
+  pub fn windows(&self) -> HashMap<String, Window<R>> {
+    self.window.windows_lock().clone()
+  }
+}
+
+#[cfg(desktop)]
+impl<R: Runtime> AppManager<R> {
+  pub fn remove_menu_from_stash_by_id(&self, id: Option<&crate::menu::MenuId>) {
+    if let Some(id) = id {
+      let is_used_by_a_window = self
+        .window
+        .windows_lock()
+        .values()
+        .any(|w| w.is_menu_in_use(id));
+      if !(self.menu.is_menu_in_use(id) || is_used_by_a_window) {
+        self.menu.menus_stash_lock().remove(id);
+      }
+    }
+  }
+}
+
+#[cfg(test)]
+mod tests {
+  use super::replace_with_callback;
+
+  #[test]
+  fn string_replace_with_callback() {
+    let mut tauri_index = 0;
+    #[allow(clippy::single_element_loop)]
+    for (src, pattern, replacement, result) in [(
+      "tauri is awesome, tauri is amazing",
+      "tauri",
+      || {
+        tauri_index += 1;
+        tauri_index.to_string()
+      },
+      "1 is awesome, 2 is amazing",
+    )] {
+      assert_eq!(replace_with_callback(src, pattern, replacement), result);
+    }
+  }
+}
+
+#[cfg(test)]
+mod test {
+  use std::{
+    sync::mpsc::{channel, Receiver, Sender},
+    time::Duration,
+  };
+
+  use crate::{
+    generate_context,
+    plugin::PluginStore,
+    test::{mock_app, MockRuntime},
+    App, Manager, StateManager, Window, WindowBuilder, Wry,
+  };
+
+  use super::AppManager;
+
+  const WINDOW_LISTEN_ID: &str = "Window::listen";
+  const WINDOW_LISTEN_GLOBAL_ID: &str = "Window::listen_global";
+  const APP_LISTEN_GLOBAL_ID: &str = "App::listen_global";
+  const TEST_EVENT_NAME: &str = "event";
+
+  #[test]
+  fn check_get_url() {
+    let context = generate_context!("test/fixture/src-tauri/tauri.conf.json", crate);
+    let manager: AppManager<Wry> = AppManager::with_handlers(
+      context,
+      PluginStore::default(),
+      Box::new(|_| false),
+      None,
+      Default::default(),
+      StateManager::new(),
+      Default::default(),
+      Default::default(),
+      (None, "".into()),
+    );
+
+    #[cfg(custom_protocol)]
+    {
+      assert_eq!(
+        manager.get_url().to_string(),
+        if cfg!(windows) || cfg!(target_os = "android") {
+          "http://tauri.localhost/"
+        } else {
+          "tauri://localhost"
+        }
+      );
+    }
+
+    #[cfg(dev)]
+    assert_eq!(manager.get_url().to_string(), "http://localhost:4000/");
+  }
+
+  struct EventSetup {
+    app: App<MockRuntime>,
+    window: Window<MockRuntime>,
+    tx: Sender<(&'static str, String)>,
+    rx: Receiver<(&'static str, String)>,
+  }
+
+  fn setup_events() -> EventSetup {
+    let app = mock_app();
+    let window = WindowBuilder::new(&app, "main", Default::default())
+      .build()
+      .unwrap();
+
+    let (tx, rx) = channel();
+
+    let tx_ = tx.clone();
+    window.listen(TEST_EVENT_NAME, move |evt| {
+      tx_
+        .send((
+          WINDOW_LISTEN_ID,
+          serde_json::from_str::<String>(evt.payload()).unwrap(),
+        ))
+        .unwrap();
+    });
+
+    let tx_ = tx.clone();
+    window.listen_global(TEST_EVENT_NAME, move |evt| {
+      tx_
+        .send((
+          WINDOW_LISTEN_GLOBAL_ID,
+          serde_json::from_str::<String>(evt.payload()).unwrap(),
+        ))
+        .unwrap();
+    });
+
+    let tx_ = tx.clone();
+    app.listen_global(TEST_EVENT_NAME, move |evt| {
+      tx_
+        .send((
+          APP_LISTEN_GLOBAL_ID,
+          serde_json::from_str::<String>(evt.payload()).unwrap(),
+        ))
+        .unwrap();
+    });
+
+    EventSetup {
+      app,
+      window,
+      tx,
+      rx,
+    }
+  }
+
+  fn assert_events(received: &[&str], expected: &[&str]) {
+    for e in expected {
+      assert!(received.contains(e), "{e} did not receive global event");
+    }
+    assert_eq!(
+      received.len(),
+      expected.len(),
+      "received {:?} events but expected {:?}",
+      received,
+      expected
+    );
+  }
+
+  #[test]
+  fn app_global_events() {
+    let EventSetup {
+      app,
+      window: _,
+      tx: _,
+      rx,
+    } = setup_events();
+
+    let mut received = Vec::new();
+    let payload = "global-payload";
+    app.emit(TEST_EVENT_NAME, payload).unwrap();
+    while let Ok((source, p)) = rx.recv_timeout(Duration::from_secs(1)) {
+      assert_eq!(p, payload);
+      received.push(source);
+    }
+    assert_events(
+      &received,
+      &[
+        WINDOW_LISTEN_ID,
+        WINDOW_LISTEN_GLOBAL_ID,
+        APP_LISTEN_GLOBAL_ID,
+      ],
+    );
+  }
+
+  #[test]
+  fn window_global_events() {
+    let EventSetup {
+      app: _,
+      window,
+      tx: _,
+      rx,
+    } = setup_events();
+
+    let mut received = Vec::new();
+    let payload = "global-payload";
+    window.emit(TEST_EVENT_NAME, payload).unwrap();
+    while let Ok((source, p)) = rx.recv_timeout(Duration::from_secs(1)) {
+      assert_eq!(p, payload);
+      received.push(source);
+    }
+    assert_events(
+      &received,
+      &[
+        WINDOW_LISTEN_ID,
+        WINDOW_LISTEN_GLOBAL_ID,
+        APP_LISTEN_GLOBAL_ID,
+      ],
+    );
+  }
+
+  #[test]
+  fn window_local_events() {
+    let EventSetup {
+      app,
+      window,
+      tx,
+      rx,
+    } = setup_events();
+
+    let mut received = Vec::new();
+    let payload = "global-payload";
+    window
+      .emit_to(window.label(), TEST_EVENT_NAME, payload)
+      .unwrap();
+    while let Ok((source, p)) = rx.recv_timeout(Duration::from_secs(1)) {
+      assert_eq!(p, payload);
+      received.push(source);
+    }
+    assert_events(&received, &[WINDOW_LISTEN_ID]);
+
+    received.clear();
+    let other_window_listen_id = "OtherWindow::listen";
+    let other_window = WindowBuilder::new(&app, "other", Default::default())
+      .build()
+      .unwrap();
+    other_window.listen(TEST_EVENT_NAME, move |evt| {
+      tx.send((
+        other_window_listen_id,
+        serde_json::from_str::<String>(evt.payload()).unwrap(),
+      ))
+      .unwrap();
+    });
+    window
+      .emit_to(other_window.label(), TEST_EVENT_NAME, payload)
+      .unwrap();
+    while let Ok((source, p)) = rx.recv_timeout(Duration::from_secs(1)) {
+      assert_eq!(p, payload);
+      received.push(source);
+    }
+    assert_events(&received, &[other_window_listen_id]);
+  }
+}

+ 29 - 0
core/tauri/src/manager/tray.rs

@@ -0,0 +1,29 @@
+// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use std::{collections::HashMap, fmt, sync::Mutex};
+
+use crate::{
+  app::GlobalTrayIconEventListener,
+  tray::{TrayIcon, TrayIconId},
+  AppHandle, Icon, Runtime,
+};
+
+pub struct TrayManager<R: Runtime> {
+  pub(crate) icon: Option<Icon>,
+  /// Tray icons
+  pub(crate) icons: Mutex<Vec<TrayIcon<R>>>,
+  /// Global Tray icon event listeners.
+  pub(crate) global_event_listeners: Mutex<Vec<GlobalTrayIconEventListener<AppHandle<R>>>>,
+  /// Tray icon event listeners.
+  pub(crate) event_listeners: Mutex<HashMap<TrayIconId, GlobalTrayIconEventListener<TrayIcon<R>>>>,
+}
+
+impl<R: Runtime> fmt::Debug for TrayManager<R> {
+  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+    f.debug_struct("TrayManager")
+      .field("icon", &self.icon)
+      .finish()
+  }
+}

+ 712 - 0
core/tauri/src/manager/window.rs

@@ -0,0 +1,712 @@
+// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use std::{
+  borrow::Cow,
+  collections::{HashMap, HashSet},
+  fmt,
+  fs::create_dir_all,
+  path::PathBuf,
+  sync::{Arc, Mutex, MutexGuard},
+};
+
+use serde::Serialize;
+use serialize_to_javascript::{default_template, DefaultTemplate, Template};
+use tauri_runtime::{
+  webview::WindowBuilder,
+  window::{
+    dpi::{PhysicalPosition, PhysicalSize},
+    DetachedWindow, FileDropEvent, PendingWindow,
+  },
+};
+use tauri_utils::config::WindowUrl;
+use url::Url;
+
+use crate::{
+  app::{GlobalWindowEventListener, OnPageLoad, UriSchemeResponder},
+  ipc::{InvokeHandler, InvokeResponder},
+  pattern::PatternJavascript,
+  window::PageLoadPayload,
+  AppHandle, EventLoopMessage, GlobalWindowEvent, Icon, Manager, Runtime, Scopes, Window,
+  WindowEvent,
+};
+
+use super::AppManager;
+
+// we need to proxy the dev server on mobile because we can't use `localhost`, so we use the local IP address
+// and we do not get a secure context without the custom protocol that proxies to the dev server
+// additionally, we need the custom protocol to inject the initialization scripts on Android
+// must also keep in sync with the `let mut response` assignment in prepare_uri_scheme_protocol
+pub(crate) const PROXY_DEV_SERVER: bool = cfg!(all(dev, mobile));
+
+const WINDOW_RESIZED_EVENT: &str = "tauri://resize";
+const WINDOW_MOVED_EVENT: &str = "tauri://move";
+const WINDOW_CLOSE_REQUESTED_EVENT: &str = "tauri://close-requested";
+const WINDOW_DESTROYED_EVENT: &str = "tauri://destroyed";
+const WINDOW_FOCUS_EVENT: &str = "tauri://focus";
+const WINDOW_BLUR_EVENT: &str = "tauri://blur";
+const WINDOW_SCALE_FACTOR_CHANGED_EVENT: &str = "tauri://scale-change";
+const WINDOW_THEME_CHANGED: &str = "tauri://theme-changed";
+const WINDOW_FILE_DROP_EVENT: &str = "tauri://file-drop";
+const WINDOW_FILE_DROP_HOVER_EVENT: &str = "tauri://file-drop-hover";
+const WINDOW_FILE_DROP_CANCELLED_EVENT: &str = "tauri://file-drop-cancelled";
+
+pub(crate) const PROCESS_IPC_MESSAGE_FN: &str =
+  include_str!("../../scripts/process-ipc-message-fn.js");
+
+#[cfg(feature = "isolation")]
+#[derive(Template)]
+#[default_template("../../scripts/isolation.js")]
+pub(crate) struct IsolationJavascript<'a> {
+  pub(crate) isolation_src: &'a str,
+  pub(crate) style: &'a str,
+}
+
+#[derive(Template)]
+#[default_template("../../scripts/ipc.js")]
+pub(crate) struct IpcJavascript<'a> {
+  pub(crate) isolation_origin: &'a str,
+}
+
+/// Uses a custom URI scheme handler to resolve file requests
+pub struct UriSchemeProtocol<R: Runtime> {
+  /// Handler for protocol
+  #[allow(clippy::type_complexity)]
+  pub protocol:
+    Box<dyn Fn(&AppHandle<R>, http::Request<Vec<u8>>, UriSchemeResponder) + Send + Sync>,
+}
+
+pub struct WindowManager<R: Runtime> {
+  pub windows: Mutex<HashMap<String, Window<R>>>,
+  /// The JS message handler.
+  pub invoke_handler: Box<InvokeHandler<R>>,
+  /// The page load hook, invoked when the webview performs a navigation.
+  pub on_page_load: Option<Arc<OnPageLoad<R>>>,
+  pub default_icon: Option<Icon>,
+  /// The webview protocols available to all windows.
+  pub uri_scheme_protocols: Mutex<HashMap<String, Arc<UriSchemeProtocol<R>>>>,
+
+  /// Window event listeners to all windows.
+  pub event_listeners: Arc<Vec<GlobalWindowEventListener<R>>>,
+
+  /// Responder for invoke calls.
+  pub invoke_responder: Option<Arc<InvokeResponder<R>>>,
+  /// The script that initializes the invoke system.
+  pub invoke_initialization_script: String,
+}
+
+impl<R: Runtime> fmt::Debug for WindowManager<R> {
+  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+    f.debug_struct("WindowManager")
+      .field("default_window_icon", &self.default_icon)
+      .field(
+        "invoke_initialization_script",
+        &self.invoke_initialization_script,
+      )
+      .finish()
+  }
+}
+
+impl<R: Runtime> WindowManager<R> {
+  pub(crate) fn register_uri_scheme_protocol<N: Into<String>>(
+    &self,
+    uri_scheme: N,
+    protocol: Arc<UriSchemeProtocol<R>>,
+  ) {
+    let uri_scheme = uri_scheme.into();
+    self
+      .uri_scheme_protocols
+      .lock()
+      .unwrap()
+      .insert(uri_scheme, protocol);
+  }
+
+  /// Get a locked handle to the windows.
+  pub(crate) fn windows_lock(&self) -> MutexGuard<'_, HashMap<String, Window<R>>> {
+    self.windows.lock().expect("poisoned window manager")
+  }
+
+  fn prepare_pending_window(
+    &self,
+    mut pending: PendingWindow<EventLoopMessage, R>,
+    label: &str,
+    window_labels: &[String],
+    app_handle: AppHandle<R>,
+  ) -> crate::Result<PendingWindow<EventLoopMessage, R>> {
+    let is_init_global = app_handle.manager.config.build.with_global_tauri;
+    let plugin_init = app_handle
+      .manager
+      .plugins
+      .lock()
+      .expect("poisoned plugin store")
+      .initialization_script();
+
+    let pattern_init = PatternJavascript {
+      pattern: (&*app_handle.manager.pattern).into(),
+    }
+    .render_default(&Default::default())?;
+
+    let mut webview_attributes = pending.webview_attributes;
+
+    let ipc_init = IpcJavascript {
+      isolation_origin: &match &*app_handle.manager.pattern {
+        #[cfg(feature = "isolation")]
+        crate::Pattern::Isolation { schema, .. } => crate::pattern::format_real_schema(schema),
+        _ => "".to_string(),
+      },
+    }
+    .render_default(&Default::default())?;
+
+    let mut window_labels = window_labels.to_vec();
+    let l = label.to_string();
+    if !window_labels.contains(&l) {
+      window_labels.push(l);
+    }
+    webview_attributes = webview_attributes
+      .initialization_script(
+        r#"
+        if (!window.__TAURI_INTERNALS__) {
+          Object.defineProperty(window, '__TAURI_INTERNALS__', {
+            value: {
+              plugins: {}
+            }
+          })
+        }
+      "#,
+      )
+      .initialization_script(&self.invoke_initialization_script)
+      .initialization_script(&format!(
+        r#"
+          Object.defineProperty(window.__TAURI_INTERNALS__, 'metadata', {{
+            value: {{
+              windows: {window_labels_array}.map(function (label) {{ return {{ label: label }} }}),
+              currentWindow: {{ label: {current_window_label} }}
+            }}
+          }})
+        "#,
+        window_labels_array = serde_json::to_string(&window_labels)?,
+        current_window_label = serde_json::to_string(&label)?,
+      ))
+      .initialization_script(&self.initialization_script(
+        &app_handle.manager,
+        &ipc_init.into_string(),
+        &pattern_init.into_string(),
+        &plugin_init,
+        is_init_global,
+      )?);
+
+    #[cfg(feature = "isolation")]
+    if let crate::Pattern::Isolation { schema, .. } = &*app_handle.manager.pattern {
+      webview_attributes = webview_attributes.initialization_script(
+        &IsolationJavascript {
+          isolation_src: &crate::pattern::format_real_schema(schema),
+          style: tauri_utils::pattern::isolation::IFRAME_STYLE,
+        }
+        .render_default(&Default::default())?
+        .into_string(),
+      );
+    }
+
+    pending.webview_attributes = webview_attributes;
+
+    let mut registered_scheme_protocols = Vec::new();
+
+    for (uri_scheme, protocol) in &*self.uri_scheme_protocols.lock().unwrap() {
+      registered_scheme_protocols.push(uri_scheme.clone());
+      let protocol = protocol.clone();
+      let app_handle = Mutex::new(app_handle.clone());
+      pending.register_uri_scheme_protocol(uri_scheme.clone(), move |p, responder| {
+        (protocol.protocol)(
+          &app_handle.lock().unwrap(),
+          p,
+          UriSchemeResponder(responder),
+        )
+      });
+    }
+
+    let window_url = Url::parse(&pending.url).unwrap();
+    let window_origin = if window_url.scheme() == "data" {
+      "null".into()
+    } else if (cfg!(windows) || cfg!(target_os = "android"))
+      && window_url.scheme() != "http"
+      && window_url.scheme() != "https"
+    {
+      format!("http://{}.localhost", window_url.scheme())
+    } else {
+      format!(
+        "{}://{}{}",
+        window_url.scheme(),
+        window_url.host().unwrap(),
+        window_url
+          .port()
+          .map(|p| format!(":{p}"))
+          .unwrap_or_default()
+      )
+    };
+
+    if !registered_scheme_protocols.contains(&"tauri".into()) {
+      let web_resource_request_handler = pending.web_resource_request_handler.take();
+      let protocol = crate::protocol::tauri::get(
+        app_handle.manager.clone(),
+        &window_origin,
+        web_resource_request_handler,
+      );
+      pending.register_uri_scheme_protocol("tauri", move |request, responder| {
+        protocol(request, UriSchemeResponder(responder))
+      });
+      registered_scheme_protocols.push("tauri".into());
+    }
+
+    if !registered_scheme_protocols.contains(&"ipc".into()) {
+      let protocol = crate::ipc::protocol::get(app_handle.manager.clone(), pending.label.clone());
+      pending.register_uri_scheme_protocol("ipc", move |request, responder| {
+        protocol(request, UriSchemeResponder(responder))
+      });
+      registered_scheme_protocols.push("ipc".into());
+    }
+
+    let label = pending.label.clone();
+    let manager = app_handle.manager.clone();
+    let on_page_load_handler = pending.on_page_load_handler.take();
+    pending
+      .on_page_load_handler
+      .replace(Box::new(move |url, event| {
+        let payload = PageLoadPayload { url: &url, event };
+
+        if let Some(w) = manager.get_window(&label) {
+          if let Some(on_page_load) = &manager.window.on_page_load {
+            on_page_load(&w, &payload);
+          }
+
+          manager.plugins.lock().unwrap().on_page_load(&w, &payload);
+        }
+
+        if let Some(handler) = &on_page_load_handler {
+          handler(url, event);
+        }
+      }));
+
+    #[cfg(feature = "protocol-asset")]
+    if !registered_scheme_protocols.contains(&"asset".into()) {
+      let asset_scope = app_handle
+        .manager
+        .state()
+        .get::<crate::Scopes>()
+        .asset_protocol
+        .clone();
+      let protocol = crate::protocol::asset::get(asset_scope.clone(), window_origin.clone());
+      pending.register_uri_scheme_protocol("asset", move |request, responder| {
+        protocol(request, UriSchemeResponder(responder))
+      });
+    }
+
+    #[cfg(feature = "isolation")]
+    if let crate::Pattern::Isolation {
+      assets,
+      schema,
+      key: _,
+      crypto_keys,
+    } = &*app_handle.manager.pattern
+    {
+      let protocol = crate::protocol::isolation::get(assets.clone(), *crypto_keys.aes_gcm().raw());
+      pending.register_uri_scheme_protocol(schema, move |request, responder| {
+        protocol(request, UriSchemeResponder(responder))
+      });
+    }
+
+    Ok(pending)
+  }
+
+  fn initialization_script(
+    &self,
+    app_manager: &AppManager<R>,
+    ipc_script: &str,
+    pattern_script: &str,
+    plugin_initialization_script: &str,
+    with_global_tauri: bool,
+  ) -> crate::Result<String> {
+    #[derive(Template)]
+    #[default_template("../../scripts/init.js")]
+    struct InitJavascript<'a> {
+      #[raw]
+      pattern_script: &'a str,
+      #[raw]
+      ipc_script: &'a str,
+      #[raw]
+      bundle_script: &'a str,
+      #[raw]
+      core_script: &'a str,
+      #[raw]
+      event_initialization_script: &'a str,
+      #[raw]
+      plugin_initialization_script: &'a str,
+      #[raw]
+      freeze_prototype: &'a str,
+    }
+
+    #[derive(Template)]
+    #[default_template("../../scripts/core.js")]
+    struct CoreJavascript<'a> {
+      os_name: &'a str,
+    }
+
+    let bundle_script = if with_global_tauri {
+      include_str!("../../scripts/bundle.global.js")
+    } else {
+      ""
+    };
+
+    let freeze_prototype = if app_manager.config.tauri.security.freeze_prototype {
+      include_str!("../../scripts/freeze_prototype.js")
+    } else {
+      ""
+    };
+
+    InitJavascript {
+      pattern_script,
+      ipc_script,
+      bundle_script,
+      core_script: &CoreJavascript {
+        os_name: std::env::consts::OS,
+      }
+      .render_default(&Default::default())?
+      .into_string(),
+      event_initialization_script: &crate::event::event_initialization_script(
+        app_manager.listeners().function_name(),
+        app_manager.listeners().listeners_object_name(),
+      ),
+      plugin_initialization_script,
+      freeze_prototype,
+    }
+    .render_default(&Default::default())
+    .map(|s| s.into_string())
+    .map_err(Into::into)
+  }
+
+  pub fn prepare_window(
+    &self,
+    app_handle: AppHandle<R>,
+    mut pending: PendingWindow<EventLoopMessage, R>,
+    window_labels: &[String],
+  ) -> crate::Result<PendingWindow<EventLoopMessage, R>> {
+    if self.windows_lock().contains_key(&pending.label) {
+      return Err(crate::Error::WindowLabelAlreadyExists(pending.label));
+    }
+    #[allow(unused_mut)] // mut url only for the data-url parsing
+    let mut url = match &pending.webview_attributes.url {
+      WindowUrl::App(path) => {
+        let url = if PROXY_DEV_SERVER {
+          Cow::Owned(Url::parse("tauri://localhost").unwrap())
+        } else {
+          app_handle.manager.get_url()
+        };
+        // ignore "index.html" just to simplify the url
+        if path.to_str() != Some("index.html") {
+          url
+            .join(&path.to_string_lossy())
+            .map_err(crate::Error::InvalidUrl)
+            // this will never fail
+            .unwrap()
+        } else {
+          url.into_owned()
+        }
+      }
+      WindowUrl::External(url) => {
+        let config_url = app_handle.manager.get_url();
+        let is_local = config_url.make_relative(url).is_some();
+        let mut url = url.clone();
+        if is_local && PROXY_DEV_SERVER {
+          url.set_scheme("tauri").unwrap();
+          url.set_host(Some("localhost")).unwrap();
+        }
+        url
+      }
+      _ => unimplemented!(),
+    };
+
+    #[cfg(not(feature = "window-data-url"))]
+    if url.scheme() == "data" {
+      return Err(crate::Error::InvalidWindowUrl(
+        "data URLs are not supported without the `window-data-url` feature.",
+      ));
+    }
+
+    #[cfg(feature = "window-data-url")]
+    if let Some(csp) = app_handle.manager.csp() {
+      if url.scheme() == "data" {
+        if let Ok(data_url) = data_url::DataUrl::process(url.as_str()) {
+          let (body, _) = data_url.decode_to_vec().unwrap();
+          let html = String::from_utf8_lossy(&body).into_owned();
+          // naive way to check if it's an html
+          if html.contains('<') && html.contains('>') {
+            let document = tauri_utils::html::parse(html);
+            tauri_utils::html::inject_csp(&document, &csp.to_string());
+            url.set_path(&format!("{},{}", mime::TEXT_HTML, document.to_string()));
+          }
+        }
+      }
+    }
+
+    pending.url = url.to_string();
+
+    if !pending.window_builder.has_icon() {
+      if let Some(default_window_icon) = self.default_icon.clone() {
+        pending.window_builder = pending
+          .window_builder
+          .icon(default_window_icon.try_into()?)?;
+      }
+    }
+
+    #[cfg(target_os = "android")]
+    {
+      pending = pending.on_webview_created(move |ctx| {
+        let plugin_manager = ctx
+          .env
+          .call_method(
+            ctx.activity,
+            "getPluginManager",
+            "()Lapp/tauri/plugin/PluginManager;",
+            &[],
+          )?
+          .l()?;
+
+        // tell the manager the webview is ready
+        ctx.env.call_method(
+          plugin_manager,
+          "onWebViewCreated",
+          "(Landroid/webkit/WebView;)V",
+          &[ctx.webview.into()],
+        )?;
+
+        Ok(())
+      });
+    }
+
+    let label = pending.label.clone();
+    pending = self.prepare_pending_window(
+      pending,
+      &label,
+      window_labels,
+      #[allow(clippy::redundant_clone)]
+      app_handle.clone(),
+    )?;
+
+    #[cfg(any(target_os = "macos", target_os = "ios", not(ipc_custom_protocol)))]
+    {
+      pending.ipc_handler = Some(crate::ipc::protocol::message_handler(
+        app_handle.manager.clone(),
+      ));
+    }
+
+    // in `Windows`, we need to force a data_directory
+    // but we do respect user-specification
+    #[cfg(any(target_os = "linux", target_os = "windows"))]
+    if pending.webview_attributes.data_directory.is_none() {
+      let local_app_data = app_handle.path().resolve(
+        &app_handle.manager.config.tauri.bundle.identifier,
+        crate::path::BaseDirectory::LocalData,
+      );
+      if let Ok(user_data_dir) = local_app_data {
+        pending.webview_attributes.data_directory = Some(user_data_dir);
+      }
+    }
+
+    // make sure the directory is created and available to prevent a panic
+    if let Some(user_data_dir) = &pending.webview_attributes.data_directory {
+      if !user_data_dir.exists() {
+        create_dir_all(user_data_dir)?;
+      }
+    }
+
+    #[cfg(feature = "isolation")]
+    let pattern = app_handle.manager.pattern.clone();
+    let navigation_handler = pending.navigation_handler.take();
+    let manager = app_handle.manager.clone();
+    let label = pending.label.clone();
+    pending.navigation_handler = Some(Box::new(move |url| {
+      // always allow navigation events for the isolation iframe and do not emit them for consumers
+      #[cfg(feature = "isolation")]
+      if let crate::Pattern::Isolation { schema, .. } = &*pattern {
+        if url.scheme() == schema
+          && url.domain() == Some(crate::pattern::ISOLATION_IFRAME_SRC_DOMAIN)
+        {
+          return true;
+        }
+      }
+      if let Some(handler) = &navigation_handler {
+        if !handler(url) {
+          return false;
+        }
+      }
+      let window = manager.window.windows_lock().get(&label).cloned();
+      if let Some(w) = window {
+        manager
+          .plugins
+          .lock()
+          .expect("poisoned plugin store")
+          .on_navigation(&w, url)
+      } else {
+        true
+      }
+    }));
+
+    Ok(pending)
+  }
+
+  pub(crate) fn attach_window(
+    &self,
+    app_handle: AppHandle<R>,
+    window: DetachedWindow<EventLoopMessage, R>,
+    #[cfg(desktop)] menu: Option<crate::window::WindowMenu<R>>,
+  ) -> Window<R> {
+    let window = Window::new(
+      app_handle.manager.clone(),
+      window,
+      app_handle,
+      #[cfg(desktop)]
+      menu,
+    );
+
+    let window_ = window.clone();
+    let window_event_listeners = self.event_listeners.clone();
+    let manager = window.manager.clone();
+    window.on_window_event(move |event| {
+      let _ = on_window_event(&window_, &manager, event);
+      for handler in window_event_listeners.iter() {
+        handler(GlobalWindowEvent {
+          window: window_.clone(),
+          event: event.clone(),
+        });
+      }
+    });
+
+    // insert the window into our manager
+    {
+      self
+        .windows_lock()
+        .insert(window.label().to_string(), window.clone());
+    }
+
+    // let plugins know that a new window has been added to the manager
+    let manager = window.manager.clone();
+    let window_ = window.clone();
+    // run on main thread so the plugin store doesn't dead lock with the event loop handler in App
+    let _ = window.run_on_main_thread(move || {
+      manager
+        .plugins
+        .lock()
+        .expect("poisoned plugin store")
+        .created(window_);
+    });
+
+    #[cfg(target_os = "ios")]
+    {
+      window
+        .with_webview(|w| {
+          unsafe { crate::ios::on_webview_created(w.inner() as _, w.view_controller() as _) };
+        })
+        .expect("failed to run on_webview_created hook");
+    }
+
+    window
+  }
+
+  pub(crate) fn on_window_close(&self, label: &str) {
+    self.windows_lock().remove(label);
+  }
+
+  pub fn eval_script_all<S: Into<String>>(&self, script: S) -> crate::Result<()> {
+    let script = script.into();
+    self
+      .windows_lock()
+      .values()
+      .try_for_each(|window| window.eval(&script))
+  }
+
+  pub fn labels(&self) -> HashSet<String> {
+    self.windows_lock().keys().cloned().collect()
+  }
+}
+
+#[derive(Serialize, Clone)]
+struct FileDropPayload<'a> {
+  paths: &'a Vec<PathBuf>,
+  position: &'a PhysicalPosition<f64>,
+}
+
+fn on_window_event<R: Runtime>(
+  window: &Window<R>,
+  manager: &AppManager<R>,
+  event: &WindowEvent,
+) -> crate::Result<()> {
+  match event {
+    WindowEvent::Resized(size) => window.emit(WINDOW_RESIZED_EVENT, size)?,
+    WindowEvent::Moved(position) => window.emit(WINDOW_MOVED_EVENT, position)?,
+    WindowEvent::CloseRequested { api } => {
+      if window.has_js_listener(Some(window.label().into()), WINDOW_CLOSE_REQUESTED_EVENT) {
+        api.prevent_close();
+      }
+      window.emit(WINDOW_CLOSE_REQUESTED_EVENT, ())?;
+    }
+    WindowEvent::Destroyed => {
+      window.emit(WINDOW_DESTROYED_EVENT, ())?;
+      let label = window.label();
+      let windows_map = manager.window.windows_lock();
+      let windows = windows_map.values();
+      for window in windows {
+        window.eval(&format!(
+          r#"(function () {{ const metadata = window.__TAURI_INTERNALS__.metadata; if (metadata != null) {{ metadata.windows = window.__TAURI_INTERNALS__.metadata.windows.filter(w => w.label !== "{label}"); }} }})()"#,
+        ))?;
+      }
+    }
+    WindowEvent::Focused(focused) => window.emit(
+      if *focused {
+        WINDOW_FOCUS_EVENT
+      } else {
+        WINDOW_BLUR_EVENT
+      },
+      (),
+    )?,
+    WindowEvent::ScaleFactorChanged {
+      scale_factor,
+      new_inner_size,
+      ..
+    } => window.emit(
+      WINDOW_SCALE_FACTOR_CHANGED_EVENT,
+      ScaleFactorChanged {
+        scale_factor: *scale_factor,
+        size: *new_inner_size,
+      },
+    )?,
+    WindowEvent::FileDrop(event) => match event {
+      FileDropEvent::Hovered { paths, position } => {
+        let payload = FileDropPayload { paths, position };
+        window.emit(WINDOW_FILE_DROP_HOVER_EVENT, payload)?
+      }
+      FileDropEvent::Dropped { paths, position } => {
+        let scopes = window.state::<Scopes>();
+        for path in paths {
+          if path.is_file() {
+            let _ = scopes.allow_file(path);
+          } else {
+            let _ = scopes.allow_directory(path, false);
+          }
+        }
+        let payload = FileDropPayload { paths, position };
+        window.emit(WINDOW_FILE_DROP_EVENT, payload)?
+      }
+      FileDropEvent::Cancelled => window.emit(WINDOW_FILE_DROP_CANCELLED_EVENT, ())?,
+      _ => unimplemented!(),
+    },
+    WindowEvent::ThemeChanged(theme) => window.emit(WINDOW_THEME_CHANGED, theme.to_string())?,
+  }
+  Ok(())
+}
+
+#[derive(Clone, Serialize)]
+#[serde(rename_all = "camelCase")]
+struct ScaleFactorChanged {
+  scale_factor: f64,
+  size: PhysicalSize<u32>,
+}

+ 0 - 20
core/tauri/src/pattern.rs

@@ -38,26 +38,6 @@ pub enum Pattern<A: Assets = EmbeddedAssets> {
   },
 }
 
-impl<A: Assets> Clone for Pattern<A> {
-  fn clone(&self) -> Self {
-    match self {
-      Self::Brownfield(a) => Self::Brownfield(*a),
-      #[cfg(feature = "isolation")]
-      Self::Isolation {
-        assets,
-        schema,
-        key,
-        crypto_keys,
-      } => Self::Isolation {
-        assets: assets.clone(),
-        schema: schema.clone(),
-        key: key.clone(),
-        crypto_keys: crypto_keys.clone(),
-      },
-    }
-  }
-}
-
 /// The shape of the JavaScript Pattern config
 #[derive(Debug, Serialize)]
 #[serde(rename_all = "lowercase", tag = "pattern")]

+ 2 - 1
core/tauri/src/plugin.rs

@@ -8,7 +8,7 @@ use crate::{
   app::UriSchemeResponder,
   error::Error,
   ipc::{Invoke, InvokeHandler},
-  manager::UriSchemeProtocol,
+  manager::window::UriSchemeProtocol,
   utils::config::PluginConfig,
   window::PageLoadPayload,
   AppHandle, RunEvent, Runtime, Window,
@@ -636,6 +636,7 @@ impl<R: Runtime, C: DeserializeOwned> Plugin<R> for TauriPlugin<R, C> {
     for (uri_scheme, protocol) in &self.uri_scheme_protocols {
       app
         .manager
+        .window
         .register_uri_scheme_protocol(uri_scheme, protocol.clone())
     }
     Ok(())

+ 1 - 1
core/tauri/src/protocol/isolation.rs

@@ -8,7 +8,7 @@ use tauri_utils::assets::{Assets, EmbeddedAssets};
 
 use std::sync::Arc;
 
-use crate::{manager::PROCESS_IPC_MESSAGE_FN, window::UriSchemeProtocolHandler};
+use crate::{manager::window::PROCESS_IPC_MESSAGE_FN, window::UriSchemeProtocolHandler};
 
 pub fn get(assets: Arc<EmbeddedAssets>, aes_gcm_key: [u8; 32]) -> UriSchemeProtocolHandler {
   Box::new(move |request, responder| {

+ 5 - 8
core/tauri/src/protocol/tauri.rs

@@ -2,21 +2,18 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
-use std::borrow::Cow;
+use std::{borrow::Cow, sync::Arc};
 
 use http::{header::CONTENT_TYPE, Request, Response as HttpResponse, StatusCode};
 
 use crate::{
-  manager::{WindowManager, PROXY_DEV_SERVER},
+  manager::{window::PROXY_DEV_SERVER, AppManager},
   window::{UriSchemeProtocolHandler, WebResourceRequestHandler},
   Runtime,
 };
 
 #[cfg(all(dev, mobile))]
-use std::{
-  collections::HashMap,
-  sync::{Arc, Mutex},
-};
+use std::{collections::HashMap, sync::Mutex};
 
 #[cfg(all(dev, mobile))]
 #[derive(Clone)]
@@ -27,7 +24,7 @@ struct CachedResponse {
 }
 
 pub fn get<R: Runtime>(
-  #[allow(unused_variables)] manager: &WindowManager<R>,
+  #[allow(unused_variables)] manager: Arc<AppManager<R>>,
   window_origin: &str,
   web_resource_request_handler: Option<Box<WebResourceRequestHandler>>,
 ) -> UriSchemeProtocolHandler {
@@ -69,7 +66,7 @@ pub fn get<R: Runtime>(
 
 fn get_response<R: Runtime>(
   request: Request<Vec<u8>>,
-  #[allow(unused_variables)] manager: &WindowManager<R>,
+  #[allow(unused_variables)] manager: &AppManager<R>,
   window_origin: &str,
   web_resource_request_handler: Option<&WebResourceRequestHandler>,
   #[cfg(all(dev, mobile))] (url, response_cache): (

+ 1 - 2
core/tauri/src/test/mod.rs

@@ -65,7 +65,6 @@ use std::{
   borrow::Cow,
   fmt::Debug,
   hash::{Hash, Hasher},
-  sync::Arc,
 };
 
 use crate::{
@@ -130,7 +129,7 @@ pub fn mock_context<A: Assets>(assets: A) -> crate::Context<A> {
       build: Default::default(),
       plugins: Default::default(),
     },
-    assets: Arc::new(assets),
+    assets: Box::new(assets),
     default_window_icon: None,
     app_icon: None,
     #[cfg(all(desktop, feature = "tray-icon"))]

+ 10 - 10
core/tauri/src/tray.rs

@@ -294,8 +294,8 @@ impl<R: Runtime> TrayIcon<R> {
     if let Some(handler) = on_menu_event {
       app_handle
         .manager
-        .inner
-        .menu_event_listeners
+        .menu
+        .global_event_listeners
         .lock()
         .unwrap()
         .push(handler);
@@ -304,8 +304,8 @@ impl<R: Runtime> TrayIcon<R> {
     if let Some(handler) = on_tray_icon_event {
       app_handle
         .manager
-        .inner
-        .tray_event_listeners
+        .tray
+        .event_listeners
         .lock()
         .unwrap()
         .insert(self.id.clone(), handler);
@@ -313,8 +313,8 @@ impl<R: Runtime> TrayIcon<R> {
 
     app_handle
       .manager
-      .inner
-      .tray_icons
+      .tray
+      .icons
       .lock()
       .unwrap()
       .push(self.clone());
@@ -333,8 +333,8 @@ impl<R: Runtime> TrayIcon<R> {
     self
       .app_handle
       .manager
-      .inner
-      .menu_event_listeners
+      .menu
+      .global_event_listeners
       .lock()
       .unwrap()
       .push(Box::new(f));
@@ -348,8 +348,8 @@ impl<R: Runtime> TrayIcon<R> {
     self
       .app_handle
       .manager
-      .inner
-      .tray_event_listeners
+      .tray
+      .event_listeners
       .lock()
       .unwrap()
       .insert(self.id.clone(), Box::new(f));

+ 21 - 15
core/tauri/src/window/mod.rs

@@ -21,7 +21,7 @@ use crate::{
     CallbackFn, Invoke, InvokeBody, InvokeError, InvokeMessage, InvokeResolver,
     OwnedInvokeResponder,
   },
-  manager::WindowManager,
+  manager::AppManager,
   runtime::{
     monitor::Monitor as RuntimeMonitor,
     webview::{WebviewAttributes, WindowBuilder as _},
@@ -139,7 +139,7 @@ impl Monitor {
 /// A builder for a webview window managed by Tauri.
 #[default_runtime(crate::Wry, wry)]
 pub struct WindowBuilder<'a, R: Runtime> {
-  manager: WindowManager<R>,
+  manager: Arc<AppManager<R>>,
   runtime: RuntimeOrDispatch<'a, R>,
   app_handle: AppHandle<R>,
   label: String,
@@ -218,7 +218,7 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> {
     let runtime = manager.runtime();
     let app_handle = manager.app_handle().clone();
     Self {
-      manager: manager.manager().clone(),
+      manager: manager.manager_owned(),
       runtime,
       app_handle,
       label: label.into(),
@@ -259,7 +259,7 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> {
   /// [the Webview2 issue]: https://github.com/tauri-apps/wry/issues/583
   pub fn from_config<M: Manager<R>>(manager: &'a M, config: WindowConfig) -> Self {
     let builder = Self {
-      manager: manager.manager().clone(),
+      manager: manager.manager_owned(),
       runtime: manager.runtime(),
       app_handle: manager.app_handle().clone(),
       label: config.label.clone(),
@@ -455,9 +455,10 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> {
         }));
     }
 
-    let labels = self.manager.labels().into_iter().collect::<Vec<_>>();
+    let labels = self.manager.window.labels().into_iter().collect::<Vec<_>>();
     let pending = self
       .manager
+      .window
       .prepare_window(self.app_handle.clone(), pending, &labels)?;
 
     #[cfg(desktop)]
@@ -472,6 +473,7 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> {
     #[cfg(desktop)]
     let handler = self
       .manager
+      .menu
       .prepare_window_menu_creation_handler(window_menu.as_ref());
     #[cfg(not(desktop))]
     #[allow(clippy::type_complexity)]
@@ -484,7 +486,7 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> {
       RuntimeOrDispatch::Dispatch(dispatcher) => dispatcher.create_window(pending, handler),
     }
     .map(|window| {
-      self.manager.attach_window(
+      self.manager.window.attach_window(
         self.app_handle.clone(),
         window,
         #[cfg(desktop)]
@@ -500,9 +502,9 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> {
     if let Some(effects) = window_effects {
       crate::vibrancy::set_window_effects(&window, Some(effects))?;
     }
-    self.manager.eval_script_all(format!(
+    self.manager.window.eval_script_all(format!(
       "window.__TAURI_INTERNALS__.metadata.windows = {window_labels_array}.map(function (label) {{ return {{ label: label }} }})",
-      window_labels_array = serde_json::to_string(&self.manager.labels())?,
+      window_labels_array = serde_json::to_string(&self.manager.window.labels())?,
     ))?;
 
     self.manager.emit_filter(
@@ -976,7 +978,7 @@ pub struct Window<R: Runtime> {
   /// The webview window created by the runtime.
   pub(crate) window: DetachedWindow<EventLoopMessage, R>,
   /// The manager to associate this webview window with.
-  pub(crate) manager: WindowManager<R>,
+  pub(crate) manager: Arc<AppManager<R>>,
   pub(crate) app_handle: AppHandle<R>,
   js_event_listeners: Arc<Mutex<HashMap<JsEventListenerKey, HashSet<EventId>>>>,
   // The menu set for this window
@@ -1057,10 +1059,14 @@ impl<R: Runtime> Manager<R> for Window<R> {
   }
 }
 impl<R: Runtime> ManagerBase<R> for Window<R> {
-  fn manager(&self) -> &WindowManager<R> {
+  fn manager(&self) -> &AppManager<R> {
     &self.manager
   }
 
+  fn manager_owned(&self) -> Arc<AppManager<R>> {
+    self.manager.clone()
+  }
+
   fn runtime(&self) -> RuntimeOrDispatch<'_, R> {
     RuntimeOrDispatch::Dispatch(self.dispatcher())
   }
@@ -1162,7 +1168,7 @@ impl PlatformWebview {
 impl<R: Runtime> Window<R> {
   /// Create a new window that is attached to the manager.
   pub(crate) fn new(
-    manager: WindowManager<R>,
+    manager: Arc<AppManager<R>>,
     window: DetachedWindow<EventLoopMessage, R>,
     app_handle: AppHandle<R>,
     #[cfg(desktop)] menu: Option<WindowMenu<R>>,
@@ -1323,8 +1329,8 @@ impl<R: Runtime> Window<R> {
   ) {
     self
       .manager
-      .inner
-      .window_menu_event_listeners
+      .menu
+      .event_listeners
       .lock()
       .unwrap()
       .insert(self.label().to_string(), Box::new(f));
@@ -1367,7 +1373,7 @@ impl<R: Runtime> Window<R> {
   pub fn set_menu(&self, menu: Menu<R>) -> crate::Result<Option<Menu<R>>> {
     let prev_menu = self.remove_menu()?;
 
-    self.manager.insert_menu_into_stash(&menu);
+    self.manager.menu.insert_menu_into_stash(&menu);
 
     let window = self.clone();
     let menu_ = menu.clone();
@@ -2210,7 +2216,7 @@ impl<R: Runtime> Window<R> {
       }
     };
 
-    let custom_responder = self.manager.invoke_responder();
+    let custom_responder = self.manager.window.invoke_responder.clone();
 
     let resolver = InvokeResolver::new(
       self.clone(),