Bladeren bron

feat(core): add mult-window support (#1217)

Lucas Fernandes Nogueira 4 jaren geleden
bovenliggende
commit
07208dff6c
63 gewijzigde bestanden met toevoegingen van 1046 en 508 verwijderingen
  1. 5 0
      .changes/event.md
  2. 5 0
      .changes/multiwindow.md
  3. 1 2
      README.md
  4. 0 51
      cli/core/src/helpers/config.rs
  5. 55 27
      cli/tauri.js/src/types/config.schema.json
  6. 9 7
      cli/tauri.js/src/types/config.ts
  7. 55 22
      cli/tauri.js/src/types/config.validator.ts
  8. 7 5
      cli/tauri.js/test/jest/fixtures/app/src-tauri/src/main.rs
  9. 5 3
      cli/tauri.js/test/jest/fixtures/app/src-tauri/tauri.conf.json
  10. 134 18
      tauri-utils/src/config.rs
  11. 5 1
      tauri/Cargo.toml
  12. 0 1
      tauri/examples/api/public/index.html
  13. 2 1
      tauri/examples/api/src-tauri/Cargo.lock
  14. 8 6
      tauri/examples/api/src-tauri/src/main.rs
  15. 3 3
      tauri/examples/api/src-tauri/tauri.conf.json
  16. 3 2
      tauri/examples/api/src/components/Window.svelte
  17. 1 0
      tauri/examples/communication/dist/communication.js
  18. 8 6
      tauri/examples/communication/src-tauri/src/main.rs
  19. 5 4
      tauri/examples/communication/src-tauri/tauri.conf.json
  20. 0 0
      tauri/examples/multiwindow/dist/__tauri.js
  21. 36 0
      tauri/examples/multiwindow/dist/index.html
  22. 11 0
      tauri/examples/multiwindow/package.json
  23. 10 0
      tauri/examples/multiwindow/src-tauri/.gitignore
  24. 42 0
      tauri/examples/multiwindow/src-tauri/Cargo.toml
  25. BIN
      tauri/examples/multiwindow/src-tauri/icons/128x128.png
  26. BIN
      tauri/examples/multiwindow/src-tauri/icons/128x128@2x.png
  27. BIN
      tauri/examples/multiwindow/src-tauri/icons/32x32.png
  28. BIN
      tauri/examples/multiwindow/src-tauri/icons/Square107x107Logo.png
  29. BIN
      tauri/examples/multiwindow/src-tauri/icons/Square142x142Logo.png
  30. BIN
      tauri/examples/multiwindow/src-tauri/icons/Square150x150Logo.png
  31. BIN
      tauri/examples/multiwindow/src-tauri/icons/Square284x284Logo.png
  32. BIN
      tauri/examples/multiwindow/src-tauri/icons/Square30x30Logo.png
  33. BIN
      tauri/examples/multiwindow/src-tauri/icons/Square310x310Logo.png
  34. BIN
      tauri/examples/multiwindow/src-tauri/icons/Square44x44Logo.png
  35. BIN
      tauri/examples/multiwindow/src-tauri/icons/Square71x71Logo.png
  36. BIN
      tauri/examples/multiwindow/src-tauri/icons/Square89x89Logo.png
  37. BIN
      tauri/examples/multiwindow/src-tauri/icons/StoreLogo.png
  38. BIN
      tauri/examples/multiwindow/src-tauri/icons/icon.icns
  39. BIN
      tauri/examples/multiwindow/src-tauri/icons/icon.ico
  40. BIN
      tauri/examples/multiwindow/src-tauri/icons/icon.png
  41. 14 0
      tauri/examples/multiwindow/src-tauri/rustfmt.toml
  42. 16 0
      tauri/examples/multiwindow/src-tauri/src/build.rs
  43. 25 0
      tauri/examples/multiwindow/src-tauri/src/main.rs
  44. 44 0
      tauri/examples/multiwindow/src-tauri/tauri.conf.json
  45. 4 0
      tauri/examples/multiwindow/yarn.lock
  46. 16 28
      tauri/src/app.rs
  47. 18 14
      tauri/src/app/event.rs
  48. 87 110
      tauri/src/app/runner.rs
  49. 90 0
      tauri/src/app/webview_manager.rs
  50. 88 62
      tauri/src/endpoints.rs
  51. 6 6
      tauri/src/endpoints/dialog.rs
  52. 3 3
      tauri/src/endpoints/event.rs
  53. 21 21
      tauri/src/endpoints/file_system.rs
  54. 2 2
      tauri/src/endpoints/http.rs
  55. 6 6
      tauri/src/endpoints/notification.rs
  56. 2 2
      tauri/src/endpoints/path.rs
  57. 4 2
      tauri/src/endpoints/salt.rs
  58. 3 0
      tauri/src/error.rs
  59. 26 31
      tauri/src/lib.rs
  60. 16 10
      tauri/src/plugin.rs
  61. 80 7
      tauri/src/webview.rs
  62. 63 43
      tauri/src/webview/wry.rs
  63. 2 2
      tauri/test/fixture/src-tauri/tauri.conf.json

+ 5 - 0
.changes/event.md

@@ -0,0 +1,5 @@
+---
+"tauri": minor
+---
+
+The `tauri::event` module has been moved to a Webview manager API.

+ 5 - 0
.changes/multiwindow.md

@@ -0,0 +1,5 @@
+---
+"tauri": minor
+---
+
+Added support to multiple windows.

+ 1 - 2
README.md

@@ -104,7 +104,7 @@ If you are interested in making a tauri-app, please visit the [documentation web
 | Multithreading | Yes | No |
 | Bytecode Delivery | Yes | No |
 | Can Render PDF | Yes | No |
-| Multiple Windows | Soon | Yes |
+| Multiple Windows | Yes | Yes |
 | Auto Updater | Soon | Yes (2) |
 | Cross Platform | Yes | Yes |
 | Custom App Icon | Yes | Yes |
@@ -116,7 +116,6 @@ If you are interested in making a tauri-app, please visit the [documentation web
 | Localhost Server | Yes | Yes |
 | No localhost option | Yes | No |
 | Desktop Tray | Soon | Yes |
-| Splashscreen | Yes | Yes |
 | Sidecar Binaries | Yes | No |
 
 #### Notes

+ 0 - 51
cli/core/src/helpers/config.rs

@@ -21,53 +21,6 @@ fn config_handle() -> &'static ConfigHandle {
   &CONFING_HANDLE
 }
 
-/// The window configuration object.
-#[derive(PartialEq, Clone, Deserialize, Serialize, Debug)]
-#[serde(tag = "window", rename_all = "camelCase")]
-pub struct WindowConfig {
-  /// The window width.
-  #[serde(default = "default_width")]
-  pub width: i32,
-  /// The window height.
-  #[serde(default = "default_height")]
-  pub height: i32,
-  /// Whether the window is resizable or not.
-  #[serde(default = "default_resizable")]
-  pub resizable: bool,
-  /// The window title.
-  #[serde(default = "default_title")]
-  pub title: String,
-  /// Whether the window starts as fullscreen or not.
-  #[serde(default)]
-  pub fullscreen: bool,
-}
-
-fn default_width() -> i32 {
-  800
-}
-
-fn default_height() -> i32 {
-  600
-}
-
-fn default_resizable() -> bool {
-  true
-}
-
-fn default_title() -> String {
-  "Tauri App".to_string()
-}
-
-fn default_window() -> WindowConfig {
-  WindowConfig {
-    width: default_width(),
-    height: default_height(),
-    resizable: default_resizable(),
-    title: default_title(),
-    fullscreen: false,
-  }
-}
-
 /// The embedded server port.
 #[derive(PartialEq, Clone, Debug, Deserialize, Serialize)]
 pub enum Port {
@@ -312,9 +265,6 @@ fn default_bundle() -> BundleConfig {
 #[derive(PartialEq, Clone, Deserialize, Serialize, Debug)]
 #[serde(tag = "tauri", rename_all = "camelCase")]
 pub struct TauriConfig {
-  /// The window configuration.
-  #[serde(default = "default_window")]
-  pub window: WindowConfig,
   /// The embeddedServer configuration.
   #[serde(default = "default_embedded_server")]
   pub embedded_server: EmbeddedServerConfig,
@@ -370,7 +320,6 @@ pub struct Config {
 
 fn default_tauri() -> TauriConfig {
   TauriConfig {
-    window: default_window(),
     embedded_server: default_embedded_server(),
     cli: None,
     bundle: default_bundle(),

+ 55 - 27
cli/tauri.js/src/types/config.schema.json

@@ -420,36 +420,64 @@
           },
           "type": "object"
         },
-        "window": {
-          "additionalProperties": false,
-          "defaultProperties": [],
-          "properties": {
-            "fullscreen": {
-              "type": "boolean"
-            },
-            "height": {
-              "type": "number"
-            },
-            "resizable": {
-              "type": "boolean"
-            },
-            "title": {
-              "type": "string"
-            },
-            "width": {
-              "type": "number"
-            }
+        "windows": {
+          "additionalItems": {
+            "anyOf": [
+              {
+                "additionalProperties": false,
+                "defaultProperties": [],
+                "properties": {
+                  "fullscreen": {
+                    "type": "boolean"
+                  },
+                  "height": {
+                    "type": "number"
+                  },
+                  "resizable": {
+                    "type": "boolean"
+                  },
+                  "title": {
+                    "type": "string"
+                  },
+                  "width": {
+                    "type": "number"
+                  }
+                },
+                "required": ["title"],
+                "type": "object"
+              }
+            ]
           },
-          "required": ["title"],
-          "type": "object"
+          "items": [
+            {
+              "additionalProperties": false,
+              "defaultProperties": [],
+              "properties": {
+                "fullscreen": {
+                  "type": "boolean"
+                },
+                "height": {
+                  "type": "number"
+                },
+                "resizable": {
+                  "type": "boolean"
+                },
+                "title": {
+                  "type": "string"
+                },
+                "width": {
+                  "type": "number"
+                }
+              },
+              "required": ["title"],
+              "type": "object"
+            }
+          ],
+          "minItems": 1,
+          "type": "array"
         }
       },
-      "required": [
-        "allowlist",
-        "bundle",
-        "security",
-        "window"
-      ],
+      "required": ["allowlist", "bundle", "security", "windows"],
       "type": "object"
     },
     "verbose": {

+ 9 - 7
cli/tauri.js/src/types/config.ts

@@ -277,13 +277,15 @@ export interface TauriConfig {
       all: boolean
       [index: string]: boolean
     }
-    window: {
-      title: string
-      width?: number
-      height?: number
-      resizable?: boolean
-      fullscreen?: boolean
-    }
+    windows: [
+      {
+        title: string
+        width?: number
+        height?: number
+        resizable?: boolean
+        fullscreen?: boolean
+      }
+    ]
     security: {
       csp?: string
     }

+ 55 - 22
cli/tauri.js/src/types/config.validator.ts

@@ -472,31 +472,64 @@ export const TauriConfigSchema = {
           },
           type: 'object'
         },
-        window: {
-          additionalProperties: false,
-          defaultProperties: [],
-          properties: {
-            fullscreen: {
-              type: 'boolean'
-            },
-            height: {
-              type: 'number'
-            },
-            resizable: {
-              type: 'boolean'
-            },
-            title: {
-              type: 'string'
-            },
-            width: {
-              type: 'number'
-            }
+        windows: {
+          additionalItems: {
+            anyOf: [
+              {
+                additionalProperties: false,
+                defaultProperties: [],
+                properties: {
+                  fullscreen: {
+                    type: 'boolean'
+                  },
+                  height: {
+                    type: 'number'
+                  },
+                  resizable: {
+                    type: 'boolean'
+                  },
+                  title: {
+                    type: 'string'
+                  },
+                  width: {
+                    type: 'number'
+                  }
+                },
+                required: ['title'],
+                type: 'object'
+              }
+            ]
           },
-          required: ['title'],
-          type: 'object'
+          items: [
+            {
+              additionalProperties: false,
+              defaultProperties: [],
+              properties: {
+                fullscreen: {
+                  type: 'boolean'
+                },
+                height: {
+                  type: 'number'
+                },
+                resizable: {
+                  type: 'boolean'
+                },
+                title: {
+                  type: 'string'
+                },
+                width: {
+                  type: 'number'
+                }
+              },
+              required: ['title'],
+              type: 'object'
+            }
+          ],
+          minItems: 1,
+          type: 'array'
         }
       },
-      required: ['allowlist', 'bundle', 'security', 'window'],
+      required: ['allowlist', 'bundle', 'security', 'windows'],
       type: 'object'
     },
     verbose: {

+ 7 - 5
cli/tauri.js/test/jest/fixtures/app/src-tauri/src/main.rs

@@ -7,19 +7,21 @@ struct Context;
 
 fn main() {
   tauri::AppBuilder::<tauri::flavors::Wry, Context>::new()
-    .setup(|dispatcher, _| async move {
-      let mut dispatcher_ = dispatcher.clone();
+    .setup(|webview_manager| async move {
+      let mut webview_manager_ = webview_manager.clone();
       tauri::event::listen(String::from("hello"), move |_| {
         tauri::event::emit(
-          &mut dispatcher_,
+          &webview_manager_,
           String::from("reply"),
           Some("{ msg: 'TEST' }".to_string()),
         )
         .unwrap();
       });
-      dispatcher.eval("window.onTauriInit && window.onTauriInit()");
+      webview_manager
+        .current_webview()
+        .eval("window.onTauriInit && window.onTauriInit()");
     })
-    .invoke_handler(|dispatcher, arg| async move {
+    .invoke_handler(|webview_manager, arg| async move {
       use cmd::Cmd::*;
       match serde_json::from_str(&arg) {
         Err(e) => Err(e.to_string()),

+ 5 - 3
cli/tauri.js/test/jest/fixtures/app/src-tauri/tauri.conf.json

@@ -17,8 +17,10 @@
       ],
       "identifier": "fixture.app"
     },
-    "window": {
-      "title": "Fixture"
-    }
+    "windows": [
+      {
+        "title": "Fixture"
+      }
+    ]
   }
 }

+ 134 - 18
tauri-utils/src/config.rs

@@ -6,16 +6,75 @@ use serde_json::Value as JsonValue;
 
 use std::collections::HashMap;
 
+/// The window webview URL options.
+#[derive(PartialEq, Debug, Clone)]
+pub enum WindowUrl {
+  /// The app's index URL.
+  App,
+  /// A custom URL.
+  Custom(String),
+}
+
+impl Default for WindowUrl {
+  fn default() -> Self {
+    Self::App
+  }
+}
+
+impl<'de> Deserialize<'de> for WindowUrl {
+  fn deserialize<D>(deserializer: D) -> Result<WindowUrl, D::Error>
+  where
+    D: Deserializer<'de>,
+  {
+    struct StringVisitor;
+    impl<'de> Visitor<'de> for StringVisitor {
+      type Value = WindowUrl;
+      fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        formatter.write_str("a string representing an url")
+      }
+
+      fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+      where
+        E: serde::de::Error,
+      {
+        if v.to_lowercase() == "app" {
+          Ok(WindowUrl::App)
+        } else {
+          Ok(WindowUrl::Custom(v.to_string()))
+        }
+      }
+    }
+    deserializer.deserialize_str(StringVisitor)
+  }
+}
+
 /// The window configuration object.
-#[derive(PartialEq, Deserialize, Debug)]
-#[serde(tag = "window", rename_all = "camelCase")]
+#[derive(PartialEq, Deserialize, Debug, Clone)]
 pub struct WindowConfig {
+  #[serde(default = "default_window_label")]
+  /// The window identifier.
+  pub label: String,
+  /// The window webview URL.
+  #[serde(default)]
+  pub url: WindowUrl,
+  /// The horizontal position of the window's top left corner
+  pub x: Option<f64>,
+  /// The vertical position of the window's top left corner
+  pub y: Option<f64>,
   /// The window width.
   #[serde(default = "default_width")]
-  pub width: i32,
+  pub width: f64,
   /// The window height.
   #[serde(default = "default_height")]
-  pub height: i32,
+  pub height: f64,
+  /// The min window width.
+  pub min_width: Option<f64>,
+  /// The min window height.
+  pub min_height: Option<f64>,
+  /// The max window width.
+  pub max_width: Option<f64>,
+  /// The max window height.
+  pub max_height: Option<f64>,
   /// Whether the window is resizable or not.
   #[serde(default = "default_resizable")]
   pub resizable: bool,
@@ -25,20 +84,47 @@ pub struct WindowConfig {
   /// Whether the window starts as fullscreen or not.
   #[serde(default)]
   pub fullscreen: bool,
+  /// Whether the window is transparent or not.
+  #[serde(default)]
+  pub transparent: bool,
+  /// Whether the window is maximized or not.
+  #[serde(default)]
+  pub maximized: bool,
+  /// Whether the window is visible or not.
+  #[serde(default = "default_visible")]
+  pub visible: bool,
+  /// Whether the window should have borders and bars.
+  #[serde(default = "default_decorations")]
+  pub decorations: bool,
+  /// Whether the window should always be on top of other windows.
+  #[serde(default)]
+  pub always_on_top: bool,
+}
+
+fn default_window_label() -> String {
+  "main".to_string()
 }
 
-fn default_width() -> i32 {
-  800
+fn default_width() -> f64 {
+  800f64
 }
 
-fn default_height() -> i32 {
-  600
+fn default_height() -> f64 {
+  600f64
 }
 
 fn default_resizable() -> bool {
   true
 }
 
+fn default_visible() -> bool {
+  true
+}
+
+fn default_decorations() -> bool {
+  true
+}
+
 fn default_title() -> String {
   "Tauri App".to_string()
 }
@@ -46,11 +132,24 @@ fn default_title() -> String {
 impl Default for WindowConfig {
   fn default() -> Self {
     Self {
+      label: default_window_label(),
+      url: WindowUrl::App,
+      x: None,
+      y: None,
       width: default_width(),
       height: default_height(),
+      min_width: None,
+      min_height: None,
+      max_width: None,
+      max_height: None,
       resizable: default_resizable(),
       title: default_title(),
       fullscreen: false,
+      transparent: false,
+      maximized: false,
+      visible: default_visible(),
+      decorations: default_decorations(),
+      always_on_top: false,
     }
   }
 }
@@ -335,13 +434,17 @@ impl Default for BundleConfig {
   }
 }
 
+fn default_window_config() -> Vec<WindowConfig> {
+  vec![Default::default()]
+}
+
 /// The Tauri configuration object.
 #[derive(PartialEq, Deserialize, Debug)]
 #[serde(tag = "tauri", rename_all = "camelCase")]
 pub struct TauriConfig {
   /// The window configuration.
-  #[serde(default)]
-  pub window: WindowConfig,
+  #[serde(default = "default_window_config")]
+  pub windows: Vec<WindowConfig>,
   /// The embeddedServer configuration.
   #[serde(default)]
   pub embedded_server: EmbeddedServerConfig,
@@ -356,7 +459,7 @@ pub struct TauriConfig {
 impl Default for TauriConfig {
   fn default() -> Self {
     Self {
-      window: WindowConfig::default(),
+      windows: default_window_config(),
       embedded_server: EmbeddedServerConfig::default(),
       cli: None,
       bundle: BundleConfig::default(),
@@ -441,7 +544,7 @@ mod test {
     // get default embedded server
     let de_server = EmbeddedServerConfig::default();
     // get default window
-    let d_window = WindowConfig::default();
+    let d_windows = default_window_config();
     // get default title
     let d_title = default_title();
     // get default bundle
@@ -449,13 +552,26 @@ mod test {
 
     // create a tauri config.
     let tauri = TauriConfig {
-      window: WindowConfig {
-        width: 800,
-        height: 600,
+      windows: vec![WindowConfig {
+        label: "main".to_string(),
+        url: WindowUrl::App,
+        x: None,
+        y: None,
+        width: 800f64,
+        height: 600f64,
+        min_width: None,
+        min_height: None,
+        max_width: None,
+        max_height: None,
         resizable: true,
         title: String::from("Tauri App"),
         fullscreen: false,
-      },
+        transparent: false,
+        maximized: false,
+        visible: true,
+        decorations: true,
+        always_on_top: false,
+      }],
       embedded_server: EmbeddedServerConfig {
         host: String::from("http://127.0.0.1"),
         port: Port::Random,
@@ -479,7 +595,7 @@ mod test {
     assert_eq!(de_server, tauri.embedded_server);
     assert_eq!(d_bundle, tauri.bundle);
     assert_eq!(d_path, String::from("http://localhost:8080"));
-    assert_eq!(d_title, tauri.window.title);
-    assert_eq!(d_window, tauri.window);
+    assert_eq!(d_title, tauri.windows[0].title);
+    assert_eq!(d_windows, tauri.windows);
   }
 }

+ 5 - 1
tauri/Cargo.toml

@@ -32,7 +32,7 @@ thiserror = "1.0.23"
 once_cell = "1.5.2"
 tauri-api = { version = "0.7.5", path = "../tauri-api" }
 tauri-macros = { version = "0.1", path = "../tauri-macros" }
-wry = { git = "https://github.com/tauri-apps/wry", rev = "42f4f2133f7921ed5adc47908787094da8abeac5" }
+wry = { git = "https://github.com/tauri-apps/wry", rev = "f4edf89de5dc40b77a94f6b94fcaf76fdac6bbf4" }
 
 [target."cfg(target_os = \"windows\")".dependencies]
 runas = "0.2"
@@ -74,3 +74,7 @@ notification = [ "tauri-api/notification" ]
 [[example]]
 name = "communication"
 path = "examples/communication/src-tauri/src/main.rs"
+
+[[example]]
+name = "multiwindow"
+path = "examples/multiwindow/src-tauri/src/main.rs"

+ 0 - 1
tauri/examples/api/public/index.html

@@ -9,7 +9,6 @@
 
   <link rel='icon' type='image/png' href='favicon.png'>
   <link rel='stylesheet' href='global.css'>
-  <!-- <link rel='stylesheet' href='build/bundle.css'> -->
 
   <script defer src='build/bundle.js'></script>
 </head>

+ 2 - 1
tauri/examples/api/src-tauri/Cargo.lock

@@ -3094,11 +3094,12 @@ dependencies = [
 [[package]]
 name = "wry"
 version = "0.4.1"
-source = "git+https://github.com/tauri-apps/wry?rev=42f4f2133f7921ed5adc47908787094da8abeac5#42f4f2133f7921ed5adc47908787094da8abeac5"
+source = "git+https://github.com/tauri-apps/wry?rev=f4edf89de5dc40b77a94f6b94fcaf76fdac6bbf4#f4edf89de5dc40b77a94f6b94fcaf76fdac6bbf4"
 dependencies = [
  "cc",
  "cocoa",
  "core-graphics 0.22.2",
+ "gdk",
  "gio",
  "glib",
  "gtk",

+ 8 - 6
tauri/examples/api/src-tauri/src/main.rs

@@ -17,19 +17,21 @@ struct Context;
 
 fn main() {
   tauri::AppBuilder::<tauri::flavors::Wry, Context>::new()
-    .setup(|dispatcher, _source| async move {
-      let mut dispatcher = dispatcher.clone();
-      tauri::event::listen(String::from("js-event"), move |msg| {
+    .setup(|webview_manager| async move {
+      let dispatcher = webview_manager.current_webview().unwrap();
+      let dispatcher_ = dispatcher.clone();
+      dispatcher.listen("js-event", move |msg| {
         println!("got js-event with message '{:?}'", msg);
         let reply = Reply {
           data: "something else".to_string(),
         };
 
-        tauri::event::emit(&mut dispatcher, String::from("rust-event"), Some(reply))
+        dispatcher_
+          .emit("rust-event", Some(reply))
           .expect("failed to emit");
       });
     })
-    .invoke_handler(|mut dispatcher, arg| async move {
+    .invoke_handler(|webview_manager, arg| async move {
       use cmd::Cmd::*;
       match serde_json::from_str(&arg) {
         Err(e) => Err(e.to_string()),
@@ -47,7 +49,7 @@ fn main() {
               // tauri::execute_promise is a helper for APIs that uses the tauri.promisified JS function
               // so you can easily communicate between JS and Rust with promises
               tauri::execute_promise(
-                &mut dispatcher,
+                &webview_manager,
                 async move {
                   println!("{} {:?}", endpoint, body);
                   // perform an async operation here

+ 3 - 3
tauri/examples/api/src-tauri/tauri.conf.json

@@ -1,7 +1,7 @@
 {
   "build": {
     "distDir": "../public",
-    "devPath": "../public",
+    "devPath": "http://localhost:5000",
     "withGlobalTauri": true,
     "beforeBuildCommand": "yarn build",
     "beforeDevCommand": "yarn dev"
@@ -57,9 +57,9 @@
     "allowlist": {
       "all": true
     },
-    "window": {
+    "windows": [{
       "title": "Tauri API Validation"
-    },
+    }],
     "security": {
       "csp": "default-src blob: data: filesystem: ws: http: https: 'unsafe-eval' 'unsafe-inline'"
     }

+ 3 - 2
tauri/examples/api/src/components/Window.svelte

@@ -2,18 +2,19 @@
   import { setTitle, open } from "@tauri-apps/api/window";
 
   let urlValue = "https://tauri.studio";
+  let windowTitle = 'Awesome Tauri Example!';
 
   function openUrl() {
     open(urlValue);
   }
 
   function setWindowTitle() {
-    setTitle(urlValue);
+    setTitle(windowTitle);
   }
 </script>
 
 <form style="margin-top: 24px" on:submit|preventDefault={setWindowTitle}>
-  <input id="title" value="Awesome Tauri Example!" />
+  <input id="title" bind:value={windowTitle} />
   <button class="button" type="submit">Set title</button>
 </form>
 <form style="margin-top: 24px" on:submit|preventDefault={openUrl}>

+ 1 - 0
tauri/examples/communication/dist/communication.js

@@ -1,4 +1,5 @@
 document.getElementById("log").addEventListener("click", function () {
+  console.log('log')
   window.__TAURI__.tauri.invoke({
     cmd: "logOperation",
     event: "tauri-click",

+ 8 - 6
tauri/examples/communication/src-tauri/src/main.rs

@@ -18,19 +18,21 @@ struct Context;
 
 fn main() {
   tauri::AppBuilder::<tauri::flavors::Wry, Context>::new()
-    .setup(|dispatcher, _source| async move {
-      let mut dispatcher = dispatcher.clone();
-      tauri::event::listen(String::from("js-event"), move |msg| {
+    .setup(|webview_manager| async move {
+      let current_webview = webview_manager.current_webview().unwrap().clone();
+      let current_webview_ = current_webview.clone();
+      current_webview.listen(String::from("js-event"), move |msg| {
         println!("got js-event with message '{:?}'", msg);
         let reply = Reply {
           data: "something else".to_string(),
         };
 
-        tauri::event::emit(&mut dispatcher, String::from("rust-event"), Some(reply))
+        current_webview_
+          .emit(String::from("rust-event"), Some(reply))
           .expect("failed to emit");
       });
     })
-    .invoke_handler(|mut dispatcher, arg| async move {
+    .invoke_handler(|webview_manager, arg| async move {
       use cmd::Cmd::*;
       match serde_json::from_str(&arg) {
         Err(e) => Err(e.to_string()),
@@ -48,7 +50,7 @@ fn main() {
               // tauri::execute_promise is a helper for APIs that uses the tauri.promisified JS function
               // so you can easily communicate between JS and Rust with promises
               tauri::execute_promise(
-                &mut dispatcher,
+                &webview_manager,
                 async move {
                   println!("{} {:?}", endpoint, body);
                   // perform an async operation here

+ 5 - 4
tauri/examples/communication/src-tauri/tauri.conf.json

@@ -1,7 +1,7 @@
 {
   "build": {
     "distDir": "../dist",
-    "devPath": "../dist",
+    "devPath": "http://localhost:4000",
     "withGlobalTauri": true
   },
   "ctx": {},
@@ -56,9 +56,10 @@
     "allowlist": {
       "all": true
     },
-    "window": {
-      "title": "Tauri API Validation"
-    },
+    "windows": [{
+      "title": "Tauri API Validation",
+      "resizable": false
+    }],
     "security": {
       "csp": "default-src blob: data: filesystem: ws: http: https: 'unsafe-eval' 'unsafe-inline'"
     }

File diff suppressed because it is too large
+ 0 - 0
tauri/examples/multiwindow/dist/__tauri.js


+ 36 - 0
tauri/examples/multiwindow/dist/index.html

@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<html>
+
+<body>
+  <div id="window-label"></div>
+  <div id="container"></div>
+  <div id="response"></div>
+
+  <script>
+    var windowLabel = window.__TAURI__.currentWindow.label
+    var windowLabelContainer = document.getElementById('window-label')
+    windowLabelContainer.innerHTML = 'This is the ' + windowLabel + ' window.'
+
+    var responseContainer = document.getElementById('response')
+    window.__TAURI__.event.listen('window://' + windowLabel, function (event) {
+      responseContainer.innerHTML = 'Got ' + JSON.stringify(event)
+    })
+
+    var container = document.getElementById('container')
+    for (var index in window.__TAURI__.windows) {
+      const label = window.__TAURI__.windows[index].label
+      if (label === windowLabel) {
+        continue;
+      }
+      const button = document.createElement('button')
+      button.innerHTML = 'Send message to ' + label
+      button.addEventListener('click', function () {
+        window.__TAURI__.event.emit('window://' + label, 'message from ' + windowLabel)
+      })
+      container.appendChild(button)
+    }
+
+  </script>
+</body>
+
+</html>

+ 11 - 0
tauri/examples/multiwindow/package.json

@@ -0,0 +1,11 @@
+{
+  "name": "communication-example",
+  "version": "1.0.0",
+  "description": "A Tauri example showcasing the JS-Rust communication",
+  "main": "index.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1",
+    "build": "node ../../../cli/tauri.js/bin/tauri build"
+  },
+  "private": true
+}

+ 10 - 0
tauri/examples/multiwindow/src-tauri/.gitignore

@@ -0,0 +1,10 @@
+# Generated by Cargo
+# will have compiled files and executables
+/target/
+WixTools
+
+# These are backup files generated by rustfmt
+**/*.rs.bk
+
+config.json
+bundle.json

+ 42 - 0
tauri/examples/multiwindow/src-tauri/Cargo.toml

@@ -0,0 +1,42 @@
+workspace = { }
+
+[package]
+name = "app"
+version = "0.1.0"
+description = "A Tauri Multiwindow App"
+authors = [ "you" ]
+license = ""
+repository = ""
+default-run = "app"
+edition = "2018"
+build = "src/build.rs"
+
+[package.metadata.bundle]
+identifier = "com.tauri.dev"
+icon = [
+  "icons/32x32.png",
+  "icons/128x128.png",
+  "icons/128x128@2x.png",
+  "icons/icon.icns",
+  "icons/icon.ico"
+]
+
+[dependencies]
+tauri = { path = "../../..", features =["all-api", "cli"]}
+
+[target."cfg(windows)".build-dependencies]
+winres = "0.1"
+
+[features]
+embedded-server = [ "tauri/embedded-server" ]
+
+[[bin]]
+name = "app"
+path = "src/main.rs"
+
+[profile.release]
+panic = "abort"
+codegen-units = 1
+lto = true
+incremental = false
+opt-level = "z"

BIN
tauri/examples/multiwindow/src-tauri/icons/128x128.png


BIN
tauri/examples/multiwindow/src-tauri/icons/128x128@2x.png


BIN
tauri/examples/multiwindow/src-tauri/icons/32x32.png


BIN
tauri/examples/multiwindow/src-tauri/icons/Square107x107Logo.png


BIN
tauri/examples/multiwindow/src-tauri/icons/Square142x142Logo.png


BIN
tauri/examples/multiwindow/src-tauri/icons/Square150x150Logo.png


BIN
tauri/examples/multiwindow/src-tauri/icons/Square284x284Logo.png


BIN
tauri/examples/multiwindow/src-tauri/icons/Square30x30Logo.png


BIN
tauri/examples/multiwindow/src-tauri/icons/Square310x310Logo.png


BIN
tauri/examples/multiwindow/src-tauri/icons/Square44x44Logo.png


BIN
tauri/examples/multiwindow/src-tauri/icons/Square71x71Logo.png


BIN
tauri/examples/multiwindow/src-tauri/icons/Square89x89Logo.png


BIN
tauri/examples/multiwindow/src-tauri/icons/StoreLogo.png


BIN
tauri/examples/multiwindow/src-tauri/icons/icon.icns


BIN
tauri/examples/multiwindow/src-tauri/icons/icon.ico


BIN
tauri/examples/multiwindow/src-tauri/icons/icon.png


+ 14 - 0
tauri/examples/multiwindow/src-tauri/rustfmt.toml

@@ -0,0 +1,14 @@
+max_width = 100
+hard_tabs = false
+tab_spaces = 2
+newline_style = "Auto"
+use_small_heuristics = "Default"
+reorder_imports = true
+reorder_modules = true
+remove_nested_parens = true
+edition = "2018"
+merge_derives = true
+use_try_shorthand = false
+use_field_init_shorthand = false
+force_explicit_abi = true
+imports_granularity = "Crate"

+ 16 - 0
tauri/examples/multiwindow/src-tauri/src/build.rs

@@ -0,0 +1,16 @@
+#[cfg(windows)]
+extern crate winres;
+
+#[cfg(windows)]
+fn main() {
+  if std::path::Path::new("icons/icon.ico").exists() {
+    let mut res = winres::WindowsResource::new();
+    res.set_icon_with_id("icons/icon.ico", "32512");
+    res.compile().expect("Unable to find visual studio tools");
+  } else {
+    panic!("No Icon.ico found. Please add one or check the path");
+  }
+}
+
+#[cfg(not(windows))]
+fn main() {}

+ 25 - 0
tauri/examples/multiwindow/src-tauri/src/main.rs

@@ -0,0 +1,25 @@
+#![cfg_attr(
+  all(not(debug_assertions), target_os = "windows"),
+  windows_subsystem = "windows"
+)]
+
+#[derive(tauri::FromTauriContext)]
+#[config_path = "examples/multiwindow/src-tauri/tauri.conf.json"]
+struct Context;
+
+fn main() {
+  tauri::AppBuilder::<tauri::flavors::Wry, Context>::new()
+    .setup(|webview_manager| async move {
+      let current_webview = webview_manager.current_webview().unwrap().clone();
+      let current_webview_ = current_webview.clone();
+      let event_name = format!("window://{}", webview_manager.current_window_label());
+      current_webview.listen(event_name.clone(), move |msg| {
+        current_webview_
+          .emit(&event_name, msg)
+          .expect("failed to emit");
+      });
+    })
+    .build()
+    .unwrap()
+    .run();
+}

+ 44 - 0
tauri/examples/multiwindow/src-tauri/tauri.conf.json

@@ -0,0 +1,44 @@
+{
+  "build": {
+    "distDir": "../dist",
+    "devPath": "../dist",
+    "withGlobalTauri": true
+  },
+  "tauri": {
+    "bundle": {
+      "active": true,
+      "targets": "all",
+      "identifier": "com.tauri.dev",
+      "icon": [
+        "icons/32x32.png",
+        "icons/128x128.png",
+        "icons/128x128@2x.png",
+        "icons/icon.icns",
+        "icons/icon.ico"
+      ],
+      "resources": [],
+      "externalBin": [],
+      "copyright": "",
+      "category": "DeveloperTool"
+    },
+    "allowlist": {
+      "all": true
+    },
+    "windows": [{
+      "label": "Main",
+      "url": "app",
+      "title": "Tauri - Main",
+      "width": 800,
+      "height": 600
+    }, {
+      "label": "Secondary",
+      "url": "app",
+      "title": "Tauri - Secondary",
+      "width": 600,
+      "height": 400
+    }],
+    "security": {
+      "csp": "default-src blob: data: filesystem: ws: http: https: 'unsafe-eval' 'unsafe-inline'"
+    }
+  }
+}

+ 4 - 0
tauri/examples/multiwindow/yarn.lock

@@ -0,0 +1,4 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+

+ 16 - 28
tauri/src/app.rs

@@ -3,10 +3,15 @@ use futures::future::BoxFuture;
 use std::marker::PhantomData;
 use tauri_api::{config::Config, private::AsTauriContext};
 
+pub(crate) mod event;
 mod runner;
+mod webview_manager;
 
-type InvokeHandler<W> = dyn Fn(W, String) -> BoxFuture<'static, Result<(), String>> + Send + Sync;
-type Setup<W> = dyn Fn(W, String) -> BoxFuture<'static, ()> + Send + Sync;
+pub use webview_manager::{WebviewDispatcher, WebviewManager};
+
+type InvokeHandler<D> =
+  dyn Fn(WebviewManager<D>, String) -> BoxFuture<'static, Result<(), String>> + Send + Sync;
+type Setup<D> = dyn Fn(WebviewManager<D>) -> BoxFuture<'static, ()> + Send + Sync;
 
 /// `App` runtime information.
 pub struct Context {
@@ -31,8 +36,6 @@ pub struct App<A: ApplicationExt> {
   invoke_handler: Option<Box<InvokeHandler<A::Dispatcher>>>,
   /// The setup callback, invoked when the webview is ready.
   setup: Option<Box<Setup<A::Dispatcher>>>,
-  /// The HTML of the splashscreen to render.
-  splashscreen_html: Option<String>,
   /// The context the App was created with
   pub(crate) context: Context,
 }
@@ -48,7 +51,7 @@ impl<A: ApplicationExt + 'static> App<A> {
   /// The message is considered consumed if the handler exists and returns an Ok Result.
   pub(crate) async fn run_invoke_handler(
     &self,
-    dispatcher: &mut A::Dispatcher,
+    dispatcher: &WebviewManager<A::Dispatcher>,
     arg: &str,
   ) -> Result<bool, String> {
     if let Some(ref invoke_handler) = self.invoke_handler {
@@ -60,17 +63,12 @@ impl<A: ApplicationExt + 'static> App<A> {
   }
 
   /// Runs the setup callback if defined.
-  pub(crate) async fn run_setup(&self, dispatcher: &mut A::Dispatcher, source: String) {
+  pub(crate) async fn run_setup(&self, dispatcher: &WebviewManager<A::Dispatcher>) {
     if let Some(ref setup) = self.setup {
-      let fut = setup(dispatcher.clone(), source);
+      let fut = setup(dispatcher.clone());
       fut.await;
     }
   }
-
-  /// Returns the splashscreen HTML.
-  pub fn splashscreen_html(&self) -> Option<&String> {
-    self.splashscreen_html.as_ref()
-  }
 }
 
 /// The App builder.
@@ -80,8 +78,6 @@ pub struct AppBuilder<A: ApplicationExt, C: AsTauriContext> {
   invoke_handler: Option<Box<InvokeHandler<A::Dispatcher>>>,
   /// The setup callback, invoked when the webview is ready.
   setup: Option<Box<Setup<A::Dispatcher>>>,
-  /// The HTML of the splashscreen to render.
-  splashscreen_html: Option<String>,
   /// The configuration used
   config: PhantomData<C>,
 }
@@ -92,7 +88,6 @@ impl<A: ApplicationExt + 'static, C: AsTauriContext> AppBuilder<A, C> {
     Self {
       invoke_handler: None,
       setup: None,
-      splashscreen_html: None,
       config: Default::default(),
     }
   }
@@ -100,13 +95,13 @@ impl<A: ApplicationExt + 'static, C: AsTauriContext> AppBuilder<A, C> {
   /// Defines the JS message handler callback.
   pub fn invoke_handler<
     T: futures::Future<Output = Result<(), String>> + Send + Sync + 'static,
-    F: Fn(A::Dispatcher, String) -> T + Send + Sync + 'static,
+    F: Fn(WebviewManager<A::Dispatcher>, String) -> T + Send + Sync + 'static,
   >(
     mut self,
     invoke_handler: F,
   ) -> Self {
-    self.invoke_handler = Some(Box::new(move |dispatcher, arg| {
-      Box::pin(invoke_handler(dispatcher, arg))
+    self.invoke_handler = Some(Box::new(move |webview_manager, arg| {
+      Box::pin(invoke_handler(webview_manager, arg))
     }));
     self
   }
@@ -114,23 +109,17 @@ impl<A: ApplicationExt + 'static, C: AsTauriContext> AppBuilder<A, C> {
   /// Defines the setup callback.
   pub fn setup<
     T: futures::Future<Output = ()> + Send + Sync + 'static,
-    F: Fn(A::Dispatcher, String) -> T + Send + Sync + 'static,
+    F: Fn(WebviewManager<A::Dispatcher>) -> T + Send + Sync + 'static,
   >(
     mut self,
     setup: F,
   ) -> Self {
-    self.setup = Some(Box::new(move |dispatcher, source| {
-      Box::pin(setup(dispatcher, source))
+    self.setup = Some(Box::new(move |webview_manager| {
+      Box::pin(setup(webview_manager))
     }));
     self
   }
 
-  /// Defines the splashscreen HTML to render.
-  pub fn splashscreen_html(mut self, html: &str) -> Self {
-    self.splashscreen_html = Some(html.to_string());
-    self
-  }
-
   /// Adds a plugin to the runtime.
   pub fn plugin(
     self,
@@ -145,7 +134,6 @@ impl<A: ApplicationExt + 'static, C: AsTauriContext> AppBuilder<A, C> {
     Ok(App {
       invoke_handler: self.invoke_handler,
       setup: self.setup,
-      splashscreen_html: self.splashscreen_html,
       context: Context::new::<C>()?,
     })
   }

+ 18 - 14
tauri/src/event.rs → tauri/src/app/event.rs

@@ -16,7 +16,7 @@ struct EventHandler {
   on_event: Box<dyn FnMut(Option<String>) + Send>,
 }
 
-type Listeners = Arc<Mutex<HashMap<String, EventHandler>>>;
+type Listeners = Arc<Mutex<HashMap<String, Vec<EventHandler>>>>;
 
 lazy_static! {
   static ref EMIT_FUNCTION_NAME: String = uuid::Uuid::new_v4().to_string();
@@ -46,22 +46,24 @@ pub fn event_queue_object_name() -> String {
 }
 
 /// Adds an event listener for JS events.
-pub fn listen<F: FnMut(Option<String>) + Send + 'static>(id: impl Into<String>, handler: F) {
+pub fn listen<F: FnMut(Option<String>) + Send + 'static>(id: impl AsRef<str>, handler: F) {
   let mut l = listeners()
     .lock()
     .expect("Failed to lock listeners: listen()");
-  l.insert(
-    id.into(),
-    EventHandler {
-      on_event: Box::new(handler),
-    },
-  );
+  let handler = EventHandler {
+    on_event: Box::new(handler),
+  };
+  if let Some(listeners) = l.get_mut(id.as_ref()) {
+    listeners.push(handler);
+  } else {
+    l.insert(id.as_ref().to_string(), vec![handler]);
+  }
 }
 
 /// Emits an event to JS.
 pub fn emit<D: ApplicationDispatcherExt, S: Serialize>(
-  dispatcher: &mut D,
-  event: impl AsRef<str> + Send + 'static,
+  webview_dispatcher: &crate::WebviewDispatcher<D>,
+  event: impl AsRef<str>,
   payload: Option<S>,
 ) -> crate::Result<()> {
   let salt = crate::salt::generate();
@@ -72,7 +74,7 @@ pub fn emit<D: ApplicationDispatcherExt, S: Serialize>(
     JsonValue::Null
   };
 
-  dispatcher.eval(&format!(
+  webview_dispatcher.eval(&format!(
     "window['{}']({{type: '{}', payload: {}}}, '{}')",
     emit_function_name(),
     event.as_ref(),
@@ -90,14 +92,16 @@ pub fn on_event(event: String, data: Option<String>) {
     .expect("Failed to lock listeners: on_event()");
 
   if l.contains_key(&event) {
-    let handler = l.get_mut(&event).expect("Failed to get mutable handler");
-    (handler.on_event)(data);
+    let listeners = l.get_mut(&event).expect("Failed to get mutable handler");
+    for handler in listeners {
+      (handler.on_event)(data.clone());
+    }
   }
 }
 
 #[cfg(test)]
 mod test {
-  use crate::event::*;
+  use super::*;
   use proptest::prelude::*;
 
   // dummy event handler function

+ 87 - 110
tauri/src/app/runner.rs

@@ -1,16 +1,13 @@
 #[cfg(dev)]
 use std::io::Read;
-use std::sync::{
-  atomic::{AtomicBool, Ordering},
-  Arc,
-};
+use std::{collections::HashMap, sync::Arc};
 
 #[cfg(dev)]
 use crate::api::assets::{AssetFetch, Assets};
 
-use crate::{ApplicationDispatcherExt, ApplicationExt, WebviewBuilderExt, WindowBuilderExt};
+use crate::{api::config::WindowUrl, ApplicationExt, WebviewBuilderExt};
 
-use super::App;
+use super::{App, WebviewDispatcher, WebviewManager};
 #[cfg(embedded_server)]
 use crate::api::tcp::{get_available_port, port_is_available};
 use crate::app::Context;
@@ -45,24 +42,8 @@ pub(crate) fn run<A: ApplicationExt + 'static>(application: App<A>) -> crate::Re
     spawn_server(server_url, &application.context);
   }
 
-  let splashscreen_content = if application.splashscreen_html().is_some() {
-    Some(Content::Html(
-      application
-        .splashscreen_html()
-        .expect("failed to get splashscreen_html")
-        .to_string(),
-    ))
-  } else {
-    None
-  };
-
   // build the webview
-  let (webview_application, mut dispatcher) =
-    build_webview(application, main_content, splashscreen_content)?;
-
-  crate::async_runtime::spawn(async move {
-    crate::plugin::created(A::plugin_store(), &mut dispatcher).await
-  });
+  let webview_application = build_webview(application, main_content)?;
 
   // spin up the updater process
   #[cfg(feature = "updater")]
@@ -250,28 +231,12 @@ pub fn event_initialization_script() -> String {
 fn build_webview<A: ApplicationExt + 'static>(
   application: App<A>,
   content: Content<String>,
-  splashscreen_content: Option<Content<String>>,
-) -> crate::Result<(A, A::Dispatcher)> {
-  let config = &application.context.config;
+) -> crate::Result<A> {
   // TODO let debug = cfg!(debug_assertions);
-  // get properties from config struct
-  // TODO let width = config.tauri.window.width;
-  // TODO let height = config.tauri.window.height;
-  let resizable = config.tauri.window.resizable;
-  // let fullscreen = config.tauri.window.fullscreen;
-  let title = config.tauri.window.title.clone();
-
-  let has_splashscreen = splashscreen_content.is_some();
-  let initialized_splashscreen = Arc::new(AtomicBool::new(false));
-
   let content_url = match content {
     Content::Html(s) => s,
     Content::Url(s) => s,
   };
-  let url = match splashscreen_content {
-    Some(Content::Html(s)) => s,
-    _ => content_url.to_string(),
-  };
 
   let initialization_script = format!(
     r#"
@@ -296,90 +261,102 @@ fn build_webview<A: ApplicationExt + 'static>(
 
   let mut webview_application = A::new()?;
 
-  let main_window =
-    webview_application.create_window(A::WindowBuilder::new().resizable(resizable).title(title))?;
-
-  let dispatcher = webview_application.dispatcher(&main_window);
-
-  let tauri_invoke_handler = crate::Callback::<A::Dispatcher> {
-    name: "__TAURI_INVOKE_HANDLER__".to_string(),
-    function: Box::new(move |dispatcher, _, arg| {
-      let arg = arg.into_iter().next().unwrap_or_else(String::new);
-      let application = application.clone();
-      let mut dispatcher = dispatcher.clone();
-      let content_url = content_url.to_string();
-      let initialized_splashscreen = initialized_splashscreen.clone();
-
-      crate::async_runtime::spawn(async move {
-        if arg == r#"{"cmd":"__initialized"}"# {
-          let source = if has_splashscreen && !initialized_splashscreen.load(Ordering::Relaxed) {
-            initialized_splashscreen.swap(true, Ordering::Relaxed);
-            "splashscreen"
+  let mut window_refs = Vec::new();
+  let mut dispatchers = HashMap::new();
+
+  for window_config in application.context.config.tauri.windows.clone() {
+    let window = crate::webview::WindowBuilder::from(&window_config);
+    let window = webview_application.create_window(window.get())?;
+    let dispatcher = webview_application.dispatcher(&window);
+    dispatchers.insert(
+      window_config.label.to_string(),
+      WebviewDispatcher::new(dispatcher),
+    );
+    window_refs.push((window_config, window));
+  }
+
+  for (window_config, window) in window_refs {
+    let webview_manager = WebviewManager::new(dispatchers.clone(), window_config.label.to_string());
+
+    let application = application.clone();
+    let content_url = content_url.to_string();
+
+    let webview_manager_ = webview_manager.clone();
+    let tauri_invoke_handler = crate::Callback::<A::Dispatcher> {
+      name: "__TAURI_INVOKE_HANDLER__".to_string(),
+      function: Box::new(move |_, _, arg| {
+        let arg = arg.into_iter().next().unwrap_or_else(String::new);
+        let application = application.clone();
+        let webview_manager = webview_manager_.clone();
+
+        crate::async_runtime::spawn(async move {
+          if arg == r#"{"cmd":"__initialized"}"# {
+            application.run_setup(&webview_manager).await;
+            crate::plugin::ready(A::plugin_store(), &webview_manager).await;
           } else {
-            "window-1"
-          };
-          application
-            .run_setup(&mut dispatcher, source.to_string())
-            .await;
-          if source == "window-1" {
-            crate::plugin::ready(A::plugin_store(), &mut dispatcher).await;
-          }
-        } else if arg == r#"{"cmd":"closeSplashscreen"}"# {
-          dispatcher.eval(&format!(r#"window.location.href = "{}""#, content_url));
-        } else {
-          let mut endpoint_handle =
-            crate::endpoints::handle(&mut dispatcher, &arg, &application.context)
-              .await
-              .map_err(|e| e.to_string());
-          if let Err(ref tauri_handle_error) = endpoint_handle {
-            if tauri_handle_error.contains("unknown variant") {
-              let error = match application.run_invoke_handler(&mut dispatcher, &arg).await {
+            let mut endpoint_handle =
+              crate::endpoints::handle(&webview_manager, &arg, &application.context).await;
+            if let Err(crate::Error::UnknownApi) = endpoint_handle {
+              let response = match application.run_invoke_handler(&webview_manager, &arg).await {
                 Ok(handled) => {
                   if handled {
-                    String::from("")
+                    Ok(())
                   } else {
-                    tauri_handle_error.to_string()
+                    endpoint_handle
                   }
                 }
-                Err(e) => e,
+                Err(_) => Err(crate::Error::UnknownApi),
               };
-              endpoint_handle = Err(error);
+              endpoint_handle = response;
             }
-          }
-          if let Err(ref app_handle_error) = endpoint_handle {
-            if app_handle_error.contains("unknown variant") {
-              let error =
-                match crate::plugin::extend_api(A::plugin_store(), &mut dispatcher, &arg).await {
-                  Ok(_) => String::from(""),
-                  Err(e) => e.to_string(),
-                };
-              endpoint_handle = Err(error);
+            if let Err(crate::Error::UnknownApi) = endpoint_handle {
+              endpoint_handle =
+                crate::plugin::extend_api(A::plugin_store(), &webview_manager, &arg)
+                  .await
+                  .map(|_| ());
             }
-          }
-          endpoint_handle = endpoint_handle.map_err(|e| e.replace("'", "\\'"));
-          if let Err(handler_error_message) = endpoint_handle {
-            if !handler_error_message.is_empty() {
-              dispatcher.eval(&get_api_error_message(&arg, handler_error_message));
+            if let Err(handler_error_message) = endpoint_handle {
+              let handler_error_message = handler_error_message.to_string().replace("'", "\\'");
+              if !handler_error_message.is_empty() {
+                if let Ok(dispatcher) = webview_manager.current_webview() {
+                  dispatcher.eval(&get_api_error_message(&arg, handler_error_message));
+                }
+              }
             }
           }
-        }
-      });
-      0
-    }),
-  };
+        });
+        0
+      }),
+    };
 
-  webview_application.create_webview(
-    A::WebviewBuilder::new()
-      .url(url)
-      .initialization_script(&initialization_script),
-    main_window,
-    vec![tauri_invoke_handler],
-  )?;
+    let webview_url = match &window_config.url {
+      WindowUrl::App => content_url.to_string(),
+      WindowUrl::Custom(url) => url.to_string(),
+    };
 
-  // TODO waiting for webview window API
-  // webview.set_fullscreen(fullscreen);
+    webview_application.create_webview(
+      A::WebviewBuilder::new()
+        .url(webview_url)
+        .initialization_script(&initialization_script)
+        .initialization_script(&format!(
+          r#"
+              window.__TAURI__.windows = {window_labels_array}.map(function (label) {{ return {{ label: label }} }});
+              window.__TAURI__.currentWindow = {{ label: "{current_window_label}" }}
+            "#,
+          window_labels_array =
+            serde_json::to_string(&dispatchers.keys().collect::<Vec<&String>>()).unwrap(),
+          current_window_label = window_config.label,
+        )),
+      window,
+      vec![tauri_invoke_handler],
+    )?;
+
+    crate::async_runtime::spawn(async move {
+      crate::plugin::created(A::plugin_store(), &webview_manager).await
+    });
+  }
 
-  Ok((webview_application, dispatcher))
+  Ok(webview_application)
 }
 
 // Formats an invoke handler error message to print to console.error

+ 90 - 0
tauri/src/app/webview_manager.rs

@@ -0,0 +1,90 @@
+use std::collections::HashMap;
+
+use crate::{
+  webview::{Event, Message},
+  ApplicationDispatcherExt,
+};
+
+use serde::Serialize;
+
+/// The webview dispatcher.
+#[derive(Clone)]
+pub struct WebviewDispatcher<A: Clone>(A);
+
+impl<A: ApplicationDispatcherExt> WebviewDispatcher<A> {
+  pub(crate) fn new(dispatcher: A) -> Self {
+    Self(dispatcher)
+  }
+
+  pub(crate) fn send_event(&self, event: Event) {
+    self.0.send_message(Message::Event(event))
+  }
+
+  /// Listen to an event.
+  pub fn listen<F: FnMut(Option<String>) + Send + 'static>(
+    &self,
+    event: impl AsRef<str>,
+    handler: F,
+  ) {
+    super::event::listen(event, handler)
+  }
+
+  /// Emits an event.
+  pub fn emit<S: Serialize>(
+    &self,
+    event: impl AsRef<str>,
+    payload: Option<S>,
+  ) -> crate::Result<()> {
+    super::event::emit(&self, event, payload)
+  }
+
+  pub(crate) fn on_event(&self, event: String, data: Option<String>) {
+    super::event::on_event(event, data)
+  }
+
+  /// Evaluates a JS script.
+  pub fn eval(&self, js: &str) {
+    self.0.send_message(Message::EvalScript(js.to_string()))
+  }
+
+  /// Updates the window title.
+  pub fn set_title(&self, title: &str) {
+    self
+      .0
+      .send_message(Message::SetWindowTitle(title.to_string()))
+  }
+}
+
+/// The webview manager.
+#[derive(Clone)]
+pub struct WebviewManager<A: Clone> {
+  dispatchers: HashMap<String, WebviewDispatcher<A>>,
+  current_webview_window_label: String,
+}
+
+impl<A: ApplicationDispatcherExt> WebviewManager<A> {
+  pub(crate) fn new(dispatchers: HashMap<String, WebviewDispatcher<A>>, label: String) -> Self {
+    Self {
+      dispatchers,
+      current_webview_window_label: label,
+    }
+  }
+
+  /// Returns the label of the window associated with the current context.
+  pub fn current_window_label(&self) -> &str {
+    &self.current_webview_window_label
+  }
+
+  /// Gets the webview associated with the current context.
+  pub fn current_webview(&self) -> crate::Result<&WebviewDispatcher<A>> {
+    self.get_webview(&self.current_webview_window_label)
+  }
+
+  /// Gets the webview associated with the given window label.
+  pub fn get_webview(&self, window_label: &str) -> crate::Result<&WebviewDispatcher<A>> {
+    self
+      .dispatchers
+      .get(window_label)
+      .ok_or(crate::Error::WebviewNotFound)
+  }
+}

+ 88 - 62
tauri/src/endpoints.rs

@@ -14,11 +14,11 @@ mod http;
 #[cfg(notification)]
 mod notification;
 
-use crate::{app::Context, ApplicationDispatcherExt, Event};
+use crate::{app::Context, webview::Event, ApplicationDispatcherExt};
 
 #[allow(unused_variables)]
 pub(crate) async fn handle<D: ApplicationDispatcherExt + 'static>(
-  dispatcher: &mut D,
+  webview_manager: &crate::WebviewManager<D>,
   arg: &str,
   context: &Context,
 ) -> crate::Result<()> {
@@ -34,9 +34,9 @@ pub(crate) async fn handle<D: ApplicationDispatcherExt + 'static>(
           error,
         } => {
           #[cfg(read_text_file)]
-          file_system::read_text_file(dispatcher, path, options, callback, error).await;
+          file_system::read_text_file(webview_manager, path, options, callback, error).await;
           #[cfg(not(read_text_file))]
-          allowlist_error(dispatcher, error, "readTextFile");
+          allowlist_error(webview_manager, error, "readTextFile");
         }
         ReadBinaryFile {
           path,
@@ -45,9 +45,9 @@ pub(crate) async fn handle<D: ApplicationDispatcherExt + 'static>(
           error,
         } => {
           #[cfg(read_binary_file)]
-          file_system::read_binary_file(dispatcher, path, options, callback, error).await;
+          file_system::read_binary_file(webview_manager, path, options, callback, error).await;
           #[cfg(not(read_binary_file))]
-          allowlist_error(dispatcher, error, "readBinaryFile");
+          allowlist_error(webview_manager, error, "readBinaryFile");
         }
         WriteFile {
           path,
@@ -57,9 +57,9 @@ pub(crate) async fn handle<D: ApplicationDispatcherExt + 'static>(
           error,
         } => {
           #[cfg(write_file)]
-          file_system::write_file(dispatcher, path, contents, options, callback, error).await;
+          file_system::write_file(webview_manager, path, contents, options, callback, error).await;
           #[cfg(not(write_file))]
-          allowlist_error(dispatcher, error, "writeFile");
+          allowlist_error(webview_manager, error, "writeFile");
         }
         WriteBinaryFile {
           path,
@@ -69,10 +69,10 @@ pub(crate) async fn handle<D: ApplicationDispatcherExt + 'static>(
           error,
         } => {
           #[cfg(write_binary_file)]
-          file_system::write_binary_file(dispatcher, path, contents, options, callback, error)
+          file_system::write_binary_file(webview_manager, path, contents, options, callback, error)
             .await;
           #[cfg(not(write_binary_file))]
-          allowlist_error(dispatcher, error, "writeBinaryFile");
+          allowlist_error(webview_manager, error, "writeBinaryFile");
         }
         ReadDir {
           path,
@@ -81,9 +81,9 @@ pub(crate) async fn handle<D: ApplicationDispatcherExt + 'static>(
           error,
         } => {
           #[cfg(read_dir)]
-          file_system::read_dir(dispatcher, path, options, callback, error).await;
+          file_system::read_dir(webview_manager, path, options, callback, error).await;
           #[cfg(not(read_dir))]
-          allowlist_error(dispatcher, error, "readDir");
+          allowlist_error(webview_manager, error, "readDir");
         }
         CopyFile {
           source,
@@ -93,9 +93,17 @@ pub(crate) async fn handle<D: ApplicationDispatcherExt + 'static>(
           error,
         } => {
           #[cfg(copy_file)]
-          file_system::copy_file(dispatcher, source, destination, options, callback, error).await;
+          file_system::copy_file(
+            webview_manager,
+            source,
+            destination,
+            options,
+            callback,
+            error,
+          )
+          .await;
           #[cfg(not(copy_file))]
-          allowlist_error(dispatcher, error, "copyFile");
+          allowlist_error(webview_manager, error, "copyFile");
         }
         CreateDir {
           path,
@@ -104,9 +112,9 @@ pub(crate) async fn handle<D: ApplicationDispatcherExt + 'static>(
           error,
         } => {
           #[cfg(create_dir)]
-          file_system::create_dir(dispatcher, path, options, callback, error).await;
+          file_system::create_dir(webview_manager, path, options, callback, error).await;
           #[cfg(not(create_dir))]
-          allowlist_error(dispatcher, error, "createDir");
+          allowlist_error(webview_manager, error, "createDir");
         }
         RemoveDir {
           path,
@@ -115,9 +123,9 @@ pub(crate) async fn handle<D: ApplicationDispatcherExt + 'static>(
           error,
         } => {
           #[cfg(remove_dir)]
-          file_system::remove_dir(dispatcher, path, options, callback, error).await;
+          file_system::remove_dir(webview_manager, path, options, callback, error).await;
           #[cfg(not(remove_dir))]
-          allowlist_error(dispatcher, error, "removeDir");
+          allowlist_error(webview_manager, error, "removeDir");
         }
         RemoveFile {
           path,
@@ -126,9 +134,9 @@ pub(crate) async fn handle<D: ApplicationDispatcherExt + 'static>(
           error,
         } => {
           #[cfg(remove_file)]
-          file_system::remove_file(dispatcher, path, options, callback, error).await;
+          file_system::remove_file(webview_manager, path, options, callback, error).await;
           #[cfg(not(remove_file))]
-          allowlist_error(dispatcher, error, "removeFile");
+          allowlist_error(webview_manager, error, "removeFile");
         }
         RenameFile {
           old_path,
@@ -138,9 +146,17 @@ pub(crate) async fn handle<D: ApplicationDispatcherExt + 'static>(
           error,
         } => {
           #[cfg(rename_file)]
-          file_system::rename_file(dispatcher, old_path, new_path, options, callback, error).await;
+          file_system::rename_file(
+            webview_manager,
+            old_path,
+            new_path,
+            options,
+            callback,
+            error,
+          )
+          .await;
           #[cfg(not(rename_file))]
-          allowlist_error(dispatcher, error, "renameFile");
+          allowlist_error(webview_manager, error, "renameFile");
         }
         ResolvePath {
           path,
@@ -149,18 +165,14 @@ pub(crate) async fn handle<D: ApplicationDispatcherExt + 'static>(
           error,
         } => {
           #[cfg(path_api)]
-          path::resolve_path(dispatcher, path, directory, callback, error).await;
+          path::resolve_path(webview_manager, path, directory, callback, error).await;
           #[cfg(not(path_api))]
-          allowlist_error(dispatcher, error, "pathApi");
+          allowlist_error(webview_manager, error, "pathApi");
         }
         SetTitle { title } => {
-          // TODO
-          /*#[cfg(set_title)]
-          webview.dispatch(move |w| {
-            w.set_title(&title);
-          });*/
+          webview_manager.current_webview()?.set_title(&title);
           #[cfg(not(set_title))]
-          throw_allowlist_error(dispatcher, "title");
+          throw_allowlist_error(webview_manager, "title");
         }
         Execute {
           command,
@@ -169,22 +181,22 @@ pub(crate) async fn handle<D: ApplicationDispatcherExt + 'static>(
           error,
         } => {
           #[cfg(execute)]
-          crate::call(dispatcher, command, args, callback, error).await;
+          crate::call(webview_manager, command, args, callback, error).await;
           #[cfg(not(execute))]
-          throw_allowlist_error(dispatcher, "execute");
+          throw_allowlist_error(webview_manager, "execute");
         }
         Open { uri } => {
           #[cfg(open)]
           browser::open(uri);
           #[cfg(not(open))]
-          throw_allowlist_error(dispatcher, "open");
+          throw_allowlist_error(webview_manager, "open");
         }
         ValidateSalt {
           salt,
           callback,
           error,
         } => {
-          salt::validate(dispatcher, salt, callback, error)?;
+          salt::validate(webview_manager, salt, callback, error)?;
         }
         Listen {
           event,
@@ -194,16 +206,17 @@ pub(crate) async fn handle<D: ApplicationDispatcherExt + 'static>(
           #[cfg(event)]
           {
             let js_string = event::listen_fn(event, handler, once)?;
-            dispatcher.eval(&js_string);
+            webview_manager.current_webview()?.eval(&js_string);
           }
           #[cfg(not(event))]
-          throw_allowlist_error(dispatcher, "event");
+          throw_allowlist_error(webview_manager, "event");
         }
         Emit { event, payload } => {
+          // TODO emit to optional window
           #[cfg(event)]
-          crate::event::on_event(event, payload);
+          webview_manager.current_webview()?.on_event(event, payload);
           #[cfg(not(event))]
-          throw_allowlist_error(dispatcher, "event");
+          throw_allowlist_error(webview_manager, "event");
         }
         OpenDialog {
           options,
@@ -211,9 +224,9 @@ pub(crate) async fn handle<D: ApplicationDispatcherExt + 'static>(
           error,
         } => {
           #[cfg(open_dialog)]
-          dialog::open(dispatcher, options, callback, error)?;
+          dialog::open(webview_manager, options, callback, error)?;
           #[cfg(not(open_dialog))]
-          allowlist_error(dispatcher, error, "title");
+          allowlist_error(webview_manager, error, "title");
         }
         SaveDialog {
           options,
@@ -221,9 +234,9 @@ pub(crate) async fn handle<D: ApplicationDispatcherExt + 'static>(
           error,
         } => {
           #[cfg(save_dialog)]
-          dialog::save(dispatcher, options, callback, error)?;
+          dialog::save(webview_manager, options, callback, error)?;
           #[cfg(not(save_dialog))]
-          throw_allowlist_error(dispatcher, "saveDialog");
+          throw_allowlist_error(webview_manager, "saveDialog");
         }
         MessageDialog { message } => {
           let exe = std::env::current_exe()?;
@@ -233,9 +246,11 @@ pub(crate) async fn handle<D: ApplicationDispatcherExt + 'static>(
             .expect("failed to get exe filename")
             .to_string_lossy()
             .to_string();
-          dispatcher.send_event(Event::Run(Box::new(move || {
-            dialog::message(app_name, message);
-          })));
+          webview_manager
+            .current_webview()?
+            .send_event(Event::Run(Box::new(move || {
+              dialog::message(app_name, message);
+            })));
         }
         AskDialog {
           title,
@@ -245,7 +260,7 @@ pub(crate) async fn handle<D: ApplicationDispatcherExt + 'static>(
         } => {
           let exe = std::env::current_exe()?;
           dialog::ask(
-            dispatcher,
+            webview_manager,
             title.unwrap_or_else(|| {
               let exe_dir = exe.parent().expect("failed to get exe directory");
               exe
@@ -265,19 +280,19 @@ pub(crate) async fn handle<D: ApplicationDispatcherExt + 'static>(
           error,
         } => {
           #[cfg(http_request)]
-          http::make_request(dispatcher, *options, callback, error).await;
+          http::make_request(webview_manager, *options, callback, error).await;
           #[cfg(not(http_request))]
-          allowlist_error(dispatcher, error, "httpRequest");
+          allowlist_error(webview_manager, error, "httpRequest");
         }
         CliMatches { callback, error } => {
           #[cfg(cli)]
           {
             let matches = tauri_api::cli::get_matches(&context.config).map_err(|e| e.into());
-            crate::execute_promise(dispatcher, async move { matches }, callback, error).await;
+            crate::execute_promise(webview_manager, async move { matches }, callback, error).await;
           }
           #[cfg(not(cli))]
           api_error(
-            dispatcher,
+            webview_manager,
             error,
             "CLI definition not set under tauri.conf.json > tauri > cli (https://tauri.studio/docs/api/config#tauri.cli)",
           );
@@ -288,21 +303,21 @@ pub(crate) async fn handle<D: ApplicationDispatcherExt + 'static>(
           error,
         } => {
           #[cfg(notification)]
-          notification::send(dispatcher, options, callback, error, &context.config).await;
+          notification::send(webview_manager, options, callback, error, &context.config).await;
           #[cfg(not(notification))]
-          allowlist_error(dispatcher, error, "notification");
+          allowlist_error(webview_manager, error, "notification");
         }
         IsNotificationPermissionGranted { callback, error } => {
           #[cfg(notification)]
-          notification::is_permission_granted(dispatcher, callback, error).await;
+          notification::is_permission_granted(webview_manager, callback, error).await;
           #[cfg(not(notification))]
-          allowlist_error(dispatcher, error, "notification");
+          allowlist_error(webview_manager, error, "notification");
         }
         RequestNotificationPermission { callback, error } => {
           #[cfg(notification)]
-          notification::request_permission(dispatcher, callback, error)?;
+          notification::request_permission(webview_manager, callback, error)?;
           #[cfg(not(notification))]
-          allowlist_error(dispatcher, error, "notification");
+          allowlist_error(webview_manager, error, "notification");
         }
       }
       Ok(())
@@ -311,19 +326,25 @@ pub(crate) async fn handle<D: ApplicationDispatcherExt + 'static>(
 }
 
 #[allow(dead_code)]
-fn api_error<D: ApplicationDispatcherExt>(dispatcher: &mut D, error_fn: String, message: &str) {
+fn api_error<D: ApplicationDispatcherExt>(
+  webview_manager: &crate::WebviewManager<D>,
+  error_fn: String,
+  message: &str,
+) {
   let reject_code = tauri_api::rpc::format_callback(error_fn, message);
-  let _ = dispatcher.eval(&reject_code);
+  if let Ok(dispatcher) = webview_manager.current_webview() {
+    dispatcher.eval(&reject_code);
+  }
 }
 
 #[allow(dead_code)]
 fn allowlist_error<D: ApplicationDispatcherExt>(
-  dispatcher: &mut D,
+  webview_manager: &crate::WebviewManager<D>,
   error_fn: String,
   allowlist_key: &str,
 ) {
   api_error(
-    dispatcher,
+    webview_manager,
     error_fn,
     &format!(
       "{}' not on the allowlist (https://tauri.studio/docs/api/config#tauri.allowlist)",
@@ -333,12 +354,17 @@ fn allowlist_error<D: ApplicationDispatcherExt>(
 }
 
 #[allow(dead_code)]
-fn throw_allowlist_error<D: ApplicationDispatcherExt>(dispatcher: &mut D, allowlist_key: &str) {
+fn throw_allowlist_error<D: ApplicationDispatcherExt>(
+  webview_manager: &crate::WebviewManager<D>,
+  allowlist_key: &str,
+) {
   let reject_code = format!(
     r#"throw new Error("'{}' not on the allowlist")"#,
     allowlist_key
   );
-  let _ = dispatcher.eval(&reject_code);
+  if let Ok(dispatcher) = webview_manager.current_webview() {
+    dispatcher.eval(&reject_code);
+  }
 }
 
 #[cfg(test)]

+ 6 - 6
tauri/src/endpoints/dialog.rs

@@ -21,13 +21,13 @@ fn map_response(response: Response) -> JsonValue {
 /// Shows an open dialog.
 #[cfg(open_dialog)]
 pub fn open<D: ApplicationDispatcherExt + 'static>(
-  dispatcher: &mut D,
+  webview_manager: &crate::WebviewManager<D>,
   options: OpenDialogOptions,
   callback: String,
   error: String,
 ) -> crate::Result<()> {
   crate::execute_promise_sync(
-    dispatcher,
+    webview_manager,
     move || {
       let response = if options.multiple {
         select_multiple(options.filter, options.default_path)
@@ -48,13 +48,13 @@ pub fn open<D: ApplicationDispatcherExt + 'static>(
 /// Shows a save dialog.
 #[cfg(save_dialog)]
 pub fn save<D: ApplicationDispatcherExt + 'static>(
-  dispatcher: &mut D,
+  webview_manager: &crate::WebviewManager<D>,
   options: SaveDialogOptions,
   callback: String,
   error: String,
 ) -> crate::Result<()> {
   crate::execute_promise_sync(
-    dispatcher,
+    webview_manager,
     move || {
       save_file(options.filter, options.default_path)
         .map(map_response)
@@ -73,14 +73,14 @@ pub fn message(title: String, message: String) {
 
 /// Shows a dialog with a yes/no question.
 pub fn ask<D: ApplicationDispatcherExt + 'static>(
-  dispatcher: &mut D,
+  webview_manager: &crate::WebviewManager<D>,
   title: String,
   message: String,
   callback: String,
   error: String,
 ) -> crate::Result<()> {
   crate::execute_promise_sync(
-    dispatcher,
+    webview_manager,
     move || match ask_dialog(message, title) {
       DialogSelection::Yes => crate::Result::Ok(true),
       _ => crate::Result::Ok(false),

+ 3 - 3
tauri/src/endpoints/event.rs

@@ -17,9 +17,9 @@ pub fn listen_fn(event: String, handler: String, once: bool) -> crate::Result<St
       window['{emit}'](e.payload, e.salt, true)
     }}
   ",
-    listeners = crate::event::event_listeners_object_name(),
-    queue = crate::event::event_queue_object_name(),
-    emit = crate::event::emit_function_name(),
+    listeners = crate::app::event::event_listeners_object_name(),
+    queue = crate::app::event::event_queue_object_name(),
+    emit = crate::app::event::emit_function_name(),
     evt = event,
     handler = handler,
     once_flag = if once { "true" } else { "false" }

+ 21 - 21
tauri/src/endpoints/file_system.rs

@@ -9,14 +9,14 @@ use super::cmd::{DirOperationOptions, FileOperationOptions};
 /// Reads a directory.
 #[cfg(read_dir)]
 pub async fn read_dir<D: ApplicationDispatcherExt>(
-  dispatcher: &mut D,
+  webview_manager: &crate::WebviewManager<D>,
   path: PathBuf,
   options: Option<DirOperationOptions>,
   callback: String,
   error: String,
 ) {
   crate::execute_promise(
-    dispatcher,
+    webview_manager,
     async move {
       let (recursive, dir) = if let Some(options_value) = options {
         (options_value.recursive, options_value.dir)
@@ -34,7 +34,7 @@ pub async fn read_dir<D: ApplicationDispatcherExt>(
 /// Copies a file.
 #[cfg(copy_file)]
 pub async fn copy_file<D: ApplicationDispatcherExt>(
-  dispatcher: &mut D,
+  webview_manager: &crate::WebviewManager<D>,
   source: PathBuf,
   destination: PathBuf,
   options: Option<FileOperationOptions>,
@@ -42,7 +42,7 @@ pub async fn copy_file<D: ApplicationDispatcherExt>(
   error: String,
 ) {
   crate::execute_promise(
-    dispatcher,
+    webview_manager,
     async move {
       let (src, dest) = match options.and_then(|o| o.dir) {
         Some(dir) => (
@@ -63,14 +63,14 @@ pub async fn copy_file<D: ApplicationDispatcherExt>(
 /// Creates a directory.
 #[cfg(create_dir)]
 pub async fn create_dir<D: ApplicationDispatcherExt>(
-  dispatcher: &mut D,
+  webview_manager: &crate::WebviewManager<D>,
   path: PathBuf,
   options: Option<DirOperationOptions>,
   callback: String,
   error: String,
 ) {
   crate::execute_promise(
-    dispatcher,
+    webview_manager,
     async move {
       let (recursive, dir) = if let Some(options_value) = options {
         (options_value.recursive, options_value.dir)
@@ -95,14 +95,14 @@ pub async fn create_dir<D: ApplicationDispatcherExt>(
 /// Removes a directory.
 #[cfg(remove_dir)]
 pub async fn remove_dir<D: ApplicationDispatcherExt>(
-  dispatcher: &mut D,
+  webview_manager: &crate::WebviewManager<D>,
   path: PathBuf,
   options: Option<DirOperationOptions>,
   callback: String,
   error: String,
 ) {
   crate::execute_promise(
-    dispatcher,
+    webview_manager,
     async move {
       let (recursive, dir) = if let Some(options_value) = options {
         (options_value.recursive, options_value.dir)
@@ -127,14 +127,14 @@ pub async fn remove_dir<D: ApplicationDispatcherExt>(
 /// Removes a file
 #[cfg(remove_file)]
 pub async fn remove_file<D: ApplicationDispatcherExt>(
-  dispatcher: &mut D,
+  webview_manager: &crate::WebviewManager<D>,
   path: PathBuf,
   options: Option<FileOperationOptions>,
   callback: String,
   error: String,
 ) {
   crate::execute_promise(
-    dispatcher,
+    webview_manager,
     async move {
       let resolved_path = resolve_path(path, options.and_then(|o| o.dir))?;
       fs::remove_file(resolved_path)?;
@@ -149,7 +149,7 @@ pub async fn remove_file<D: ApplicationDispatcherExt>(
 /// Renames a file.
 #[cfg(rename_file)]
 pub async fn rename_file<D: ApplicationDispatcherExt>(
-  dispatcher: &mut D,
+  webview_manager: &crate::WebviewManager<D>,
   old_path: PathBuf,
   new_path: PathBuf,
   options: Option<FileOperationOptions>,
@@ -157,7 +157,7 @@ pub async fn rename_file<D: ApplicationDispatcherExt>(
   error: String,
 ) {
   crate::execute_promise(
-    dispatcher,
+    webview_manager,
     async move {
       let (old, new) = match options.and_then(|o| o.dir) {
         Some(dir) => (
@@ -177,7 +177,7 @@ pub async fn rename_file<D: ApplicationDispatcherExt>(
 /// Writes a text file.
 #[cfg(write_file)]
 pub async fn write_file<D: ApplicationDispatcherExt>(
-  dispatcher: &mut D,
+  webview_manager: &crate::WebviewManager<D>,
   path: PathBuf,
   contents: String,
   options: Option<FileOperationOptions>,
@@ -185,7 +185,7 @@ pub async fn write_file<D: ApplicationDispatcherExt>(
   error: String,
 ) {
   crate::execute_promise(
-    dispatcher,
+    webview_manager,
     async move {
       File::create(resolve_path(path, options.and_then(|o| o.dir))?)
         .map_err(crate::Error::Io)
@@ -201,7 +201,7 @@ pub async fn write_file<D: ApplicationDispatcherExt>(
 /// Writes a binary file.
 #[cfg(write_binary_file)]
 pub async fn write_binary_file<D: ApplicationDispatcherExt>(
-  dispatcher: &mut D,
+  webview_manager: &crate::WebviewManager<D>,
   path: PathBuf,
   contents: String,
   options: Option<FileOperationOptions>,
@@ -209,7 +209,7 @@ pub async fn write_binary_file<D: ApplicationDispatcherExt>(
   error: String,
 ) {
   crate::execute_promise(
-    dispatcher,
+    webview_manager,
     async move {
       base64::decode(contents)
         .map_err(crate::Error::Base64Decode)
@@ -229,14 +229,14 @@ pub async fn write_binary_file<D: ApplicationDispatcherExt>(
 /// Reads a text file.
 #[cfg(read_text_file)]
 pub async fn read_text_file<D: ApplicationDispatcherExt>(
-  dispatcher: &mut D,
+  webview_manager: &crate::WebviewManager<D>,
   path: PathBuf,
   options: Option<FileOperationOptions>,
   callback: String,
   error: String,
 ) {
   crate::execute_promise(
-    dispatcher,
+    webview_manager,
     async move {
       file::read_string(resolve_path(path, options.and_then(|o| o.dir))?)
         .map_err(crate::Error::FailedToExecuteApi)
@@ -250,14 +250,14 @@ pub async fn read_text_file<D: ApplicationDispatcherExt>(
 /// Reads a binary file.
 #[cfg(read_binary_file)]
 pub async fn read_binary_file<D: ApplicationDispatcherExt>(
-  dispatcher: &mut D,
+  webview_manager: &crate::WebviewManager<D>,
   path: PathBuf,
   options: Option<FileOperationOptions>,
   callback: String,
   error: String,
 ) {
   crate::execute_promise(
-    dispatcher,
+    webview_manager,
     async move {
       file::read_binary(resolve_path(path, options.and_then(|o| o.dir))?)
         .map_err(crate::Error::FailedToExecuteApi)
@@ -312,7 +312,7 @@ mod test {
 
     //call write file with the path and contents.
     write_file(
-      &mut dispatcher,
+      &webview_manager,
       path.clone(),
       contents.clone(),
       String::from(""),

+ 2 - 2
tauri/src/endpoints/http.rs

@@ -3,13 +3,13 @@ use tauri_api::http::{make_request as request, HttpRequestOptions};
 
 /// Makes an HTTP request and resolves the response to the webview
 pub async fn make_request<D: ApplicationDispatcherExt>(
-  dispatcher: &mut D,
+  webview_manager: &crate::WebviewManager<D>,
   options: HttpRequestOptions,
   callback: String,
   error: String,
 ) {
   crate::execute_promise(
-    dispatcher,
+    webview_manager,
     async move { request(options).map_err(|e| e.into()) },
     callback,
     error,

+ 6 - 6
tauri/src/endpoints/notification.rs

@@ -4,7 +4,7 @@ use serde_json::Value as JsonValue;
 use tauri_api::{config::Config, notification::Notification};
 
 pub async fn send<D: ApplicationDispatcherExt>(
-  dispatcher: &mut D,
+  webview_manager: &crate::WebviewManager<D>,
   options: NotificationOptions,
   callback: String,
   error: String,
@@ -13,7 +13,7 @@ pub async fn send<D: ApplicationDispatcherExt>(
   let identifier = config.tauri.bundle.identifier.clone();
 
   crate::execute_promise(
-    dispatcher,
+    webview_manager,
     async move {
       let mut notification = Notification::new(identifier).title(options.title);
       if let Some(body) = options.body {
@@ -32,12 +32,12 @@ pub async fn send<D: ApplicationDispatcherExt>(
 }
 
 pub async fn is_permission_granted<D: ApplicationDispatcherExt>(
-  dispatcher: &mut D,
+  webview_manager: &crate::WebviewManager<D>,
   callback: String,
   error: String,
 ) {
   crate::execute_promise(
-    dispatcher,
+    webview_manager,
     async move {
       let settings = crate::settings::read_settings()?;
       if let Some(allow_notification) = settings.allow_notification {
@@ -53,12 +53,12 @@ pub async fn is_permission_granted<D: ApplicationDispatcherExt>(
 }
 
 pub fn request_permission<D: ApplicationDispatcherExt + 'static>(
-  dispatcher: &mut D,
+  webview_manager: &crate::WebviewManager<D>,
   callback: String,
   error: String,
 ) -> crate::Result<()> {
   crate::execute_promise_sync(
-    dispatcher,
+    webview_manager,
     move || {
       let mut settings = crate::settings::read_settings()?;
       let granted = "granted".to_string();

+ 2 - 2
tauri/src/endpoints/path.rs

@@ -3,14 +3,14 @@ use crate::ApplicationDispatcherExt;
 use tauri_api::{path, path::BaseDirectory};
 
 pub async fn resolve_path<D: ApplicationDispatcherExt>(
-  dispatcher: &mut D,
+  webview_manager: &crate::WebviewManager<D>,
   path: String,
   directory: Option<BaseDirectory>,
   callback: String,
   error: String,
 ) {
   crate::execute_promise(
-    dispatcher,
+    webview_manager,
     async move { path::resolve_path(path, directory).map_err(|e| e.into()) },
     callback,
     error,

+ 4 - 2
tauri/src/endpoints/salt.rs

@@ -2,7 +2,7 @@ use crate::ApplicationDispatcherExt;
 
 /// Validates a salt.
 pub fn validate<D: ApplicationDispatcherExt>(
-  dispatcher: &mut D,
+  webview_manager: &crate::WebviewManager<D>,
   salt: String,
   callback: String,
   error: String,
@@ -13,6 +13,8 @@ pub fn validate<D: ApplicationDispatcherExt>(
     Err("Invalid salt")
   };
   let callback_string = crate::api::rpc::format_callback_result(response, callback, error)?;
-  dispatcher.eval(callback_string.as_str());
+  webview_manager
+    .current_webview()?
+    .eval(callback_string.as_str());
   Ok(())
 }

+ 3 - 0
tauri/src/error.rs

@@ -7,6 +7,9 @@ pub enum Error {
   /// Failed to create window.
   #[error("failed to create window")]
   CreateWindow,
+  /// Can't access webview dispatcher because the webview was closed or not found.
+  #[error("webview not found: invalid label or it was closed")]
+  WebviewNotFound,
   /// Embedded asset not found.
   #[error("asset not found: {0}")]
   AssetNotFound(String),

+ 26 - 31
tauri/src/lib.rs

@@ -6,8 +6,6 @@
 //! Tauri uses (and contributes to) the MIT licensed project that you can find at [webview](https://github.com/webview/webview).
 #![warn(missing_docs, rust_2018_idioms)]
 
-/// The event system module.
-pub mod event;
 /// The embedded server helpers.
 #[cfg(embedded_server)]
 pub mod server;
@@ -40,7 +38,7 @@ pub use app::*;
 pub use tauri_api as api;
 pub use tauri_macros::FromTauriContext;
 pub use webview::{
-  ApplicationDispatcherExt, ApplicationExt, Callback, Event, WebviewBuilderExt, WindowBuilderExt,
+  ApplicationDispatcherExt, ApplicationExt, Callback, WebviewBuilderExt, WindowBuilderExt,
 };
 
 /// The Tauri webview implementations.
@@ -60,25 +58,29 @@ pub fn execute_promise_sync<
   R: Serialize,
   F: FnOnce() -> Result<R> + Send + 'static,
 >(
-  dispatcher: &mut D,
+  webview_manager: &crate::WebviewManager<D>,
   task: F,
   callback: String,
   error: String,
 ) {
-  let mut dispatcher_ = dispatcher.clone();
-  dispatcher.send_event(Event::Run(Box::new(move || {
-    let callback_string =
-      match format_callback_result(task().map_err(|err| err.to_string()), &callback, &error) {
-        Ok(js) => js,
-        Err(e) => format_callback_result(
-          std::result::Result::<(), String>::Err(e.to_string()),
-          &callback,
-          &error,
-        )
-        .unwrap(),
-      };
-    dispatcher_.eval(callback_string.as_str());
-  })));
+  let webview_manager_ = webview_manager.clone();
+  if let Ok(dispatcher) = webview_manager.current_webview() {
+    dispatcher.send_event(webview::Event::Run(Box::new(move || {
+      let callback_string =
+        match format_callback_result(task().map_err(|err| err.to_string()), &callback, &error) {
+          Ok(js) => js,
+          Err(e) => format_callback_result(
+            std::result::Result::<(), String>::Err(e.to_string()),
+            &callback,
+            &error,
+          )
+          .unwrap(),
+        };
+      if let Ok(dispatcher) = webview_manager_.current_webview() {
+        dispatcher.eval(callback_string.as_str());
+      }
+    })));
+  }
 }
 
 /// Asynchronously executes the given task
@@ -91,7 +93,7 @@ pub async fn execute_promise<
   R: Serialize,
   F: futures::Future<Output = Result<R>> + Send + 'static,
 >(
-  dispatcher: &mut D,
+  webview_manager: &crate::WebviewManager<D>,
   task: F,
   success_callback: String,
   error_callback: String,
@@ -104,19 +106,21 @@ pub async fn execute_promise<
     Ok(callback_string) => callback_string,
     Err(e) => format_callback(error_callback, e.to_string()),
   };
-  dispatcher.eval(callback_string.as_str());
+  if let Ok(dispatcher) = webview_manager.current_webview() {
+    dispatcher.eval(callback_string.as_str());
+  }
 }
 
 /// Calls the given command and evaluates its output to the JS promise described by the `callback` and `error` function names.
 pub async fn call<D: ApplicationDispatcherExt>(
-  dispatcher: &mut D,
+  webview_manager: &crate::WebviewManager<D>,
   command: String,
   args: Vec<String>,
   callback: String,
   error: String,
 ) {
   execute_promise(
-    dispatcher,
+    webview_manager,
     async move { api::command::get_output(command, args, Stdio::piped()).map_err(|e| e.into()) },
     callback,
     error,
@@ -124,15 +128,6 @@ pub async fn call<D: ApplicationDispatcherExt>(
   .await;
 }
 
-/// Closes the splashscreen.
-pub fn close_splashscreen<D: ApplicationDispatcherExt>(dispatcher: &mut D) -> crate::Result<()> {
-  // send a signal to the runner so it knows that it should redirect to the main app content
-  dispatcher
-    .eval(r#"window.__TAURI_INVOKE_HANDLER__(JSON.stringify({ cmd: "closeSplashscreen" }))"#);
-
-  Ok(())
-}
-
 #[cfg(test)]
 mod test {
   use proptest::prelude::*;

+ 16 - 10
tauri/src/plugin.rs

@@ -1,4 +1,6 @@
-use crate::{api::config::PluginConfig, async_runtime::Mutex, ApplicationDispatcherExt};
+use crate::{
+  api::config::PluginConfig, async_runtime::Mutex, ApplicationDispatcherExt, WebviewManager,
+};
 
 use futures::future::join_all;
 
@@ -27,15 +29,19 @@ pub trait Plugin<D: ApplicationDispatcherExt + 'static>: Send + Sync {
 
   /// Callback invoked when the webview is created.
   #[allow(unused_variables)]
-  async fn created(&mut self, dispatcher: D) {}
+  async fn created(&mut self, webview_manager: WebviewManager<D>) {}
 
   /// Callback invoked when the webview is ready.
   #[allow(unused_variables)]
-  async fn ready(&mut self, dispatcher: D) {}
+  async fn ready(&mut self, webview_manager: WebviewManager<D>) {}
 
   /// Add invoke_handler API extension commands.
   #[allow(unused_variables)]
-  async fn extend_api(&mut self, dispatcher: D, payload: &str) -> crate::Result<()> {
+  async fn extend_api(
+    &mut self,
+    webview_manager: WebviewManager<D>,
+    payload: &str,
+  ) -> crate::Result<()> {
     Err(crate::Error::UnknownApi)
   }
 }
@@ -93,36 +99,36 @@ pub(crate) async fn initialization_script<D: ApplicationDispatcherExt + 'static>
 
 pub(crate) async fn created<D: ApplicationDispatcherExt + 'static>(
   store: &PluginStore<D>,
-  dispatcher: &mut D,
+  webview_manager: &crate::WebviewManager<D>,
 ) {
   let mut plugins = store.lock().await;
   let mut futures = Vec::new();
   for plugin in plugins.iter_mut() {
-    futures.push(plugin.created(dispatcher.clone()));
+    futures.push(plugin.created(webview_manager.clone()));
   }
   join_all(futures).await;
 }
 
 pub(crate) async fn ready<D: ApplicationDispatcherExt + 'static>(
   store: &PluginStore<D>,
-  dispatcher: &mut D,
+  webview_manager: &crate::WebviewManager<D>,
 ) {
   let mut plugins = store.lock().await;
   let mut futures = Vec::new();
   for plugin in plugins.iter_mut() {
-    futures.push(plugin.ready(dispatcher.clone()));
+    futures.push(plugin.ready(webview_manager.clone()));
   }
   join_all(futures).await;
 }
 
 pub(crate) async fn extend_api<D: ApplicationDispatcherExt + 'static>(
   store: &PluginStore<D>,
-  dispatcher: &mut D,
+  webview_manager: &crate::WebviewManager<D>,
   arg: &str,
 ) -> crate::Result<bool> {
   let mut plugins = store.lock().await;
   for ext in plugins.iter_mut() {
-    match ext.extend_api(dispatcher.clone(), arg).await {
+    match ext.extend_api(webview_manager.clone(), arg).await {
       Ok(_) => {
         return Ok(true);
       }

+ 80 - 7
tauri/src/webview.rs

@@ -1,6 +1,6 @@
 pub(crate) mod wry;
 
-pub use crate::plugin::PluginStore;
+pub use crate::{api::config::WindowConfig, plugin::PluginStore};
 
 /// An event to be posted to the webview event loop.
 pub enum Event {
@@ -8,6 +8,12 @@ pub enum Event {
   Run(crate::SyncTask),
 }
 
+pub enum Message {
+  EvalScript(String),
+  SetWindowTitle(String),
+  Event(Event),
+}
+
 /// The window builder.
 pub trait WindowBuilderExt: Sized {
   /// The window type.
@@ -16,12 +22,39 @@ pub trait WindowBuilderExt: Sized {
   /// Initializes a new window builder.
   fn new() -> Self;
 
+  /// The horizontal position of the window's top left corner.
+  fn x(self, x: f64) -> Self;
+
+  /// The vertical position of the window's top left corner.
+  fn y(self, y: f64) -> Self;
+
+  /// Window width.
+  fn width(self, width: f64) -> Self;
+
+  /// Window height.
+  fn height(self, height: f64) -> Self;
+
+  /// Window min width.
+  fn min_width(self, min_width: f64) -> Self;
+
+  /// Window min height.
+  fn min_height(self, min_height: f64) -> Self;
+
+  /// Window max width.
+  fn max_width(self, max_width: f64) -> Self;
+
+  /// Window max height.
+  fn max_height(self, max_height: f64) -> Self;
+
   /// Whether the window is resizable or not.
   fn resizable(self, resizable: bool) -> Self;
 
   /// The title of the window in the title bar.
   fn title(self, title: String) -> Self;
 
+  /// Whether to start the window in fullscreen or not.
+  fn fullscreen(self, fullscreen: bool) -> Self;
+
   /// Whether the window should be maximized upon creation.
   fn maximized(self, maximized: bool) -> Self;
 
@@ -42,6 +75,50 @@ pub trait WindowBuilderExt: Sized {
   fn finish(self) -> crate::Result<Self::Window>;
 }
 
+pub struct WindowBuilder<T>(T);
+
+impl<T> WindowBuilder<T> {
+  pub fn get(self) -> T {
+    self.0
+  }
+}
+
+impl<T: WindowBuilderExt> From<&WindowConfig> for WindowBuilder<T> {
+  fn from(config: &WindowConfig) -> Self {
+    let mut window = T::new()
+      .title(config.title.to_string())
+      .width(config.width)
+      .height(config.height)
+      .visible(config.visible)
+      .resizable(config.resizable)
+      .decorations(config.decorations)
+      .maximized(config.maximized)
+      .fullscreen(config.fullscreen)
+      .transparent(config.transparent)
+      .always_on_top(config.always_on_top);
+    if let Some(min_width) = config.min_width {
+      window = window.min_width(min_width);
+    }
+    if let Some(min_height) = config.min_height {
+      window = window.min_height(min_height);
+    }
+    if let Some(max_width) = config.max_width {
+      window = window.max_width(max_width);
+    }
+    if let Some(max_height) = config.max_height {
+      window = window.max_height(max_height);
+    }
+    if let Some(x) = config.x {
+      window = window.x(x);
+    }
+    if let Some(y) = config.y {
+      window = window.y(y);
+    }
+
+    Self(window)
+  }
+}
+
 /// The webview builder.
 pub trait WebviewBuilderExt: Sized {
   /// The webview object that this builder creates.
@@ -70,12 +147,8 @@ pub struct Callback<D> {
 
 /// Webview dispatcher. A thread-safe handle to the webview API.
 pub trait ApplicationDispatcherExt: Clone + Send + Sync + Sized {
-  /// Eval a JS string on the current webview.
-  fn eval(&mut self, js: &str);
-  /// Eval a JS string on the webview associated with the given window.
-  fn eval_on_window(&mut self, window_id: &str, js: &str);
-  /// Sends a event to the webview.
-  fn send_event(&self, event: Event);
+  /// Sends a message to the window.
+  fn send_message(&self, message: Message);
 }
 
 /// The application interface.

+ 63 - 43
tauri/src/webview/wry.rs

@@ -1,17 +1,15 @@
 use super::{
-  ApplicationDispatcherExt, ApplicationExt, Callback, Event, WebviewBuilderExt, WindowBuilderExt,
+  ApplicationDispatcherExt, ApplicationExt, Callback, Event, Message, WebviewBuilderExt,
+  WindowBuilderExt,
 };
 
-use wry::{ApplicationDispatcher, ApplicationExt as _, WindowExt};
+use wry::{ApplicationDispatcher, ApplicationExt as _, WebviewMessage, WindowExt, WindowMessage};
 
 use once_cell::sync::Lazy;
 
 use crate::plugin::PluginStore;
 
-use std::{
-  collections::HashMap,
-  sync::{Arc, Mutex},
-};
+use std::sync::{Arc, Mutex};
 
 impl WindowBuilderExt for wry::AppWindowAttributes {
   type Window = Self;
@@ -20,6 +18,46 @@ impl WindowBuilderExt for wry::AppWindowAttributes {
     Default::default()
   }
 
+  fn x(mut self, x: f64) -> Self {
+    self.x = Some(x);
+    self
+  }
+
+  fn y(mut self, y: f64) -> Self {
+    self.y = Some(y);
+    self
+  }
+
+  fn width(mut self, width: f64) -> Self {
+    self.width = width;
+    self
+  }
+
+  fn height(mut self, height: f64) -> Self {
+    self.height = height;
+    self
+  }
+
+  fn min_width(mut self, min_width: f64) -> Self {
+    self.min_width = Some(min_width);
+    self
+  }
+
+  fn min_height(mut self, min_height: f64) -> Self {
+    self.min_height = Some(min_height);
+    self
+  }
+
+  fn max_width(mut self, max_width: f64) -> Self {
+    self.max_width = Some(max_width);
+    self
+  }
+
+  fn max_height(mut self, max_height: f64) -> Self {
+    self.max_height = Some(max_height);
+    self
+  }
+
   fn resizable(mut self, resizable: bool) -> Self {
     self.resizable = resizable;
     self
@@ -30,6 +68,11 @@ impl WindowBuilderExt for wry::AppWindowAttributes {
     self
   }
 
+  fn fullscreen(mut self, fullscreen: bool) -> Self {
+    self.fullscreen = fullscreen;
+    self
+  }
+
   fn maximized(mut self, maximized: bool) -> Self {
     self.maximized = maximized;
     self
@@ -50,7 +93,6 @@ impl WindowBuilderExt for wry::AppWindowAttributes {
     self
   }
 
-  /// Whether the window should always be on top of other windows.
   fn always_on_top(mut self, always_on_top: bool) -> Self {
     self.always_on_top = always_on_top;
     self
@@ -90,45 +132,29 @@ impl WebviewBuilderExt for wry::WebViewAttributes {
 pub struct WryDispatcher {
   inner: Arc<Mutex<wry::AppDispatcher<Event>>>,
   current_window: wry::WindowId,
-  windows: Arc<Mutex<HashMap<String, wry::WindowId>>>,
 }
 
-impl ApplicationDispatcherExt for WryDispatcher {
-  fn eval(&mut self, js: &str) {
-    #[cfg(target_os = "linux")]
-    let window_id = self.current_window;
-    #[cfg(not(target_os = "linux"))]
-    let window_id = self.current_window.clone();
+struct WryMessage(wry::Message<wry::WindowId, Event>);
 
-    self
-      .inner
-      .lock()
-      .unwrap()
-      .dispatch_message(wry::Message::Script(window_id, js.to_string()))
-      .unwrap();
-  }
-
-  fn eval_on_window(&mut self, window_id: &str, js: &str) {
-    if let Some(window_id) = self.windows.lock().unwrap().get(window_id) {
-      #[cfg(target_os = "linux")]
-      let window_id = *window_id;
-      #[cfg(not(target_os = "linux"))]
-      let window_id = window_id.clone();
-      self
-        .inner
-        .lock()
-        .unwrap()
-        .dispatch_message(wry::Message::Script(window_id, js.to_string()))
-        .unwrap();
-    }
+impl From<(wry::WindowId, Message)> for WryMessage {
+  fn from((id, message): (wry::WindowId, Message)) -> Self {
+    let message = match message {
+      Message::EvalScript(js) => wry::Message::Webview(id, WebviewMessage::EvalScript(js)),
+      Message::SetWindowTitle(title) => wry::Message::Window(id, WindowMessage::SetTitle(title)),
+      Message::Event(event) => wry::Message::Custom(event),
+    };
+    WryMessage(message)
   }
+}
 
-  fn send_event(&self, event: Event) {
+impl ApplicationDispatcherExt for WryDispatcher {
+  fn send_message(&self, message: Message) {
+    let message: WryMessage = (self.current_window, message).into();
     self
       .inner
       .lock()
       .unwrap()
-      .dispatch_message(wry::Message::Custom(event))
+      .dispatch_message(message.0)
       .unwrap();
   }
 }
@@ -136,7 +162,6 @@ impl ApplicationDispatcherExt for WryDispatcher {
 /// A wrapper around the wry Application interface.
 pub struct WryApplication {
   inner: wry::Application<Event>,
-  windows: Arc<Mutex<HashMap<String, wry::WindowId>>>,
   dispatcher_handle: Arc<Mutex<wry::AppDispatcher<Event>>>,
 }
 
@@ -154,11 +179,9 @@ impl ApplicationExt for WryApplication {
   fn new() -> crate::Result<Self> {
     let app = wry::Application::new().map_err(|_| crate::Error::CreateWebview)?;
     let dispatcher = app.dispatcher();
-    let windows = Arc::new(Mutex::new(HashMap::new()));
 
     Ok(Self {
       inner: app,
-      windows,
       dispatcher_handle: Arc::new(Mutex::new(dispatcher)),
     })
   }
@@ -166,7 +189,6 @@ impl ApplicationExt for WryApplication {
   fn dispatcher(&self, window: &Self::Window) -> Self::Dispatcher {
     WryDispatcher {
       inner: self.dispatcher_handle.clone(),
-      windows: self.windows.clone(),
       current_window: window.id(),
     }
   }
@@ -188,7 +210,6 @@ impl ApplicationExt for WryApplication {
     let mut wry_callbacks = Vec::new();
     for mut callback in callbacks {
       let dispatcher_handle = self.dispatcher_handle.clone();
-      let windows = self.windows.clone();
       let window_id = window.id();
 
       let callback = wry::Callback {
@@ -197,7 +218,6 @@ impl ApplicationExt for WryApplication {
           (callback.function)(
             &WryDispatcher {
               inner: dispatcher_handle.clone(),
-              windows: windows.clone(),
               current_window: window_id,
             },
             seq,

+ 2 - 2
tauri/test/fixture/src-tauri/tauri.conf.json

@@ -12,9 +12,9 @@
     "allowlist": {
       "all": true
     },
-    "window": {
+    "windows": [{
       "title": "Tauri App"
-    },
+    }],
     "security": {
       "csp": "default-src blob: data: filesystem: ws: http: https: 'unsafe-eval' 'unsafe-inline'"
     }

Some files were not shown because too many files changed in this diff