Bläddra i källkod

feat(core): window creation at runtime (#1249)

Lucas Fernandes Nogueira 4 år sedan
förälder
incheckning
641374b153

+ 1 - 1
.changes/window-api.md

@@ -3,4 +3,4 @@
 "tauri": minor
 ---
 
-Added window management APIs.
+Added window management and window creation APIs.

+ 35 - 1
api/src/window.ts

@@ -371,4 +371,38 @@ class WindowManager {
 
 const manager = new WindowManager()
 
-export { TauriWindow, getTauriWindow, getCurrentWindow, getWindows, manager }
+export interface WindowOptions {
+  url?: 'app' | string
+  x?: number
+  y?: number
+  width?: number
+  height?: number
+  minWidth?: number
+  minHeight?: number
+  maxWidth?: number
+  maxHeight?: number
+  resizable?: boolean
+  title?: string
+  fullscreen?: boolean
+  transparent?: boolean
+  maximized?: boolean
+  visible?: boolean
+  decorations?: boolean
+  alwaysOnTop?: boolean
+}
+
+async function createWindow(label: string, options: WindowOptions = {}): Promise<TauriWindow> {
+  await invoke({
+    __tauriModule: 'Window',
+    message: {
+      cmd: 'createWebview',
+      options: {
+        label,
+        ...options
+      }
+    }
+  })
+  return new TauriWindow(label)
+}
+
+export { TauriWindow, getTauriWindow, getCurrentWindow, getWindows, manager, createWindow }

+ 14 - 0
cli/core/src/templates/tauri.js

@@ -194,6 +194,20 @@ if (!String.prototype.startsWith) {
     );
   }
 
+  window.__TAURI__.invoke({
+    __tauriModule: 'Event',
+    message: {
+      cmd: 'listen',
+      event: 'tauri://window-created',
+      handler: window.__TAURI__.transformCallback(function (event) {
+        if (event.payload) {
+          var windowLabel = event.payload.label
+          window.__TAURI__.__windows.push({ label: windowLabel })
+        }
+      })
+    }
+  })
+
   let permissionSettable = false;
   let permissionValue = "default";
 

+ 78 - 2
cli/tauri.js/src/types/config.schema.json

@@ -427,23 +427,61 @@
                 "additionalProperties": false,
                 "defaultProperties": [],
                 "properties": {
+                  "alwaysOnTop": {
+                    "type": "boolean"
+                  },
+                  "decorations": {
+                    "type": "boolean"
+                  },
                   "fullscreen": {
                     "type": "boolean"
                   },
                   "height": {
                     "type": "number"
                   },
+                  "label": {
+                    "type": "string"
+                  },
+                  "maxHeight": {
+                    "type": "number"
+                  },
+                  "maxWidth": {
+                    "type": "number"
+                  },
+                  "maximized": {
+                    "type": "boolean"
+                  },
+                  "minHeight": {
+                    "type": "number"
+                  },
+                  "minWidth": {
+                    "type": "number"
+                  },
                   "resizable": {
                     "type": "boolean"
                   },
                   "title": {
                     "type": "string"
                   },
+                  "transparent": {
+                    "type": "boolean"
+                  },
+                  "url": {
+                    "type": "string"
+                  },
+                  "visible": {
+                    "type": "boolean"
+                  },
                   "width": {
                     "type": "number"
+                  },
+                  "x": {
+                    "type": "number"
+                  },
+                  "y": {
+                    "type": "number"
                   }
                 },
-                "required": ["title"],
                 "type": "object"
               }
             ]
@@ -453,23 +491,61 @@
               "additionalProperties": false,
               "defaultProperties": [],
               "properties": {
+                "alwaysOnTop": {
+                  "type": "boolean"
+                },
+                "decorations": {
+                  "type": "boolean"
+                },
                 "fullscreen": {
                   "type": "boolean"
                 },
                 "height": {
                   "type": "number"
                 },
+                "label": {
+                  "type": "string"
+                },
+                "maxHeight": {
+                  "type": "number"
+                },
+                "maxWidth": {
+                  "type": "number"
+                },
+                "maximized": {
+                  "type": "boolean"
+                },
+                "minHeight": {
+                  "type": "number"
+                },
+                "minWidth": {
+                  "type": "number"
+                },
                 "resizable": {
                   "type": "boolean"
                 },
                 "title": {
                   "type": "string"
                 },
+                "transparent": {
+                  "type": "boolean"
+                },
+                "url": {
+                  "type": "string"
+                },
+                "visible": {
+                  "type": "boolean"
+                },
                 "width": {
                   "type": "number"
+                },
+                "x": {
+                  "type": "number"
+                },
+                "y": {
+                  "type": "number"
                 }
               },
-              "required": ["title"],
               "type": "object"
             }
           ],

+ 14 - 1
cli/tauri.js/src/types/config.ts

@@ -279,11 +279,24 @@ export interface TauriConfig {
     }
     windows: [
       {
-        title: string
+        label?: string
+        url?: 'app' | string
+        x?: number
+        y?: number
         width?: number
         height?: number
+        minWidth?: number
+        minHeight?: number
+        maxWidth?: number
+        maxHeight?: number
         resizable?: boolean
+        title?: string
         fullscreen?: boolean
+        transparent?: boolean
+        maximized?: boolean
+        visible?: boolean
+        decorations?: boolean
+        alwaysOnTop?: boolean
       }
     ]
     security: {

+ 78 - 2
cli/tauri.js/src/types/config.validator.ts

@@ -479,23 +479,61 @@ export const TauriConfigSchema = {
                 additionalProperties: false,
                 defaultProperties: [],
                 properties: {
+                  alwaysOnTop: {
+                    type: 'boolean'
+                  },
+                  decorations: {
+                    type: 'boolean'
+                  },
                   fullscreen: {
                     type: 'boolean'
                   },
                   height: {
                     type: 'number'
                   },
+                  label: {
+                    type: 'string'
+                  },
+                  maxHeight: {
+                    type: 'number'
+                  },
+                  maxWidth: {
+                    type: 'number'
+                  },
+                  maximized: {
+                    type: 'boolean'
+                  },
+                  minHeight: {
+                    type: 'number'
+                  },
+                  minWidth: {
+                    type: 'number'
+                  },
                   resizable: {
                     type: 'boolean'
                   },
                   title: {
                     type: 'string'
                   },
+                  transparent: {
+                    type: 'boolean'
+                  },
+                  url: {
+                    type: 'string'
+                  },
+                  visible: {
+                    type: 'boolean'
+                  },
                   width: {
                     type: 'number'
+                  },
+                  x: {
+                    type: 'number'
+                  },
+                  y: {
+                    type: 'number'
                   }
                 },
-                required: ['title'],
                 type: 'object'
               }
             ]
@@ -505,23 +543,61 @@ export const TauriConfigSchema = {
               additionalProperties: false,
               defaultProperties: [],
               properties: {
+                alwaysOnTop: {
+                  type: 'boolean'
+                },
+                decorations: {
+                  type: 'boolean'
+                },
                 fullscreen: {
                   type: 'boolean'
                 },
                 height: {
                   type: 'number'
                 },
+                label: {
+                  type: 'string'
+                },
+                maxHeight: {
+                  type: 'number'
+                },
+                maxWidth: {
+                  type: 'number'
+                },
+                maximized: {
+                  type: 'boolean'
+                },
+                minHeight: {
+                  type: 'number'
+                },
+                minWidth: {
+                  type: 'number'
+                },
                 resizable: {
                   type: 'boolean'
                 },
                 title: {
                   type: 'string'
                 },
+                transparent: {
+                  type: 'boolean'
+                },
+                url: {
+                  type: 'string'
+                },
+                visible: {
+                  type: 'boolean'
+                },
                 width: {
                   type: 'number'
+                },
+                x: {
+                  type: 'number'
+                },
+                y: {
+                  type: 'number'
                 }
               },
-              required: ['title'],
               type: 'object'
             }
           ],

+ 2 - 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 = "af07c28503e41a0a164cb7256fa0ec938d5daee4" }
+wry = { git = "https://github.com/tauri-apps/wry", rev = "e6cc7f0825220a0117827b6f0a366f60ce7420ea" }
 rand = "0.8"
 
 [target."cfg(target_os = \"windows\")".dependencies]
@@ -69,6 +69,7 @@ event = [ ]
 
 # window
 window = [ ]
+create-window = [ ]
 
 #shell
 execute = [ ]

+ 1 - 3
tauri/build.rs

@@ -24,14 +24,12 @@ fn main() {
 
     // window
     window: { any(all_api, feature = "window") },
+    create_window: { any(all_api, feature = "create-window") },
 
     // shell
     open: { any(all_api, feature = "open") },
     execute: { any(all_api, feature = "execute") },
 
-    // event
-    event: { any(all_api, feature = "event") },
-
     // dialog
     open_dialog: { any(all_api, feature = "open-dialog") },
     save_dialog: { any(all_api, feature = "save-dialog") },

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
tauri/examples/communication/dist/__tauri.js


+ 1 - 1
tauri/examples/communication/src-tauri/Cargo.lock

@@ -3601,7 +3601,7 @@ dependencies = [
 [[package]]
 name = "wry"
 version = "0.4.1"
-source = "git+https://github.com/tauri-apps/wry?rev=af07c28503e41a0a164cb7256fa0ec938d5daee4#af07c28503e41a0a164cb7256fa0ec938d5daee4"
+source = "git+https://github.com/tauri-apps/wry?rev=e6cc7f0825220a0117827b6f0a366f60ce7420ea#e6cc7f0825220a0117827b6f0a366f60ce7420ea"
 dependencies = [
  "cc",
  "cocoa",

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
tauri/examples/multiwindow/dist/__tauri.js


+ 26 - 12
tauri/examples/multiwindow/dist/index.html

@@ -12,10 +12,25 @@
     windowLabelContainer.innerHTML =
       "This is the " + windowLabel + " window.";
 
+    var container = document.getElementById("container");
+
+    function createWindowMessageBtn(label) {
+      var tauriWindow = window.__TAURI__.window.getTauriWindow(label)
+      var button = document.createElement("button");
+      button.innerHTML = "Send message to " + label;
+      button.addEventListener("click", function () {
+        tauriWindow.emit("clicked", "message from " + windowLabel);
+      });
+      container.appendChild(button);
+    }
+
     // global listener
     window.__TAURI__.event.listen("clicked", function (event) {
       responseContainer.innerHTML += "Got " + JSON.stringify(event) + " on global listener<br><br>";
     })
+    window.__TAURI__.event.listen('tauri://window-created', function (event) {
+      createWindowMessageBtn(event.payload.label)
+    })
 
     var responseContainer = document.getElementById("response");
     var thisTauriWindow = window.__TAURI__.window.getTauriWindow()
@@ -24,28 +39,27 @@
       responseContainer.innerHTML += "Got " + JSON.stringify(event) + " on window listener<br><br>";
     });
 
-    var container = document.getElementById("container");
+    var createWindowButton = document.createElement("button");
+    createWindowButton.innerHTML = "Create window";
+    createWindowButton.addEventListener("click", function () {
+      window.__TAURI__.window.createWindow(Math.random().toString());
+    });
+    container.appendChild(createWindowButton);
 
-    var button = document.createElement("button");
-    button.innerHTML = "Send global message";
-    button.addEventListener("click", function () {
+    var globalMessageButton = document.createElement("button");
+    globalMessageButton.innerHTML = "Send global message";
+    globalMessageButton.addEventListener("click", function () {
       // emit to all windows
       window.__TAURI__.event.emit("clicked", "message from " + windowLabel);
     });
-    container.appendChild(button);
+    container.appendChild(globalMessageButton);
 
     for (var index in window.__TAURI__.window.getWindows()) {
       var label = window.__TAURI__.window.getWindows()[index].label;
       if (label === windowLabel) {
         continue;
       }
-      var tauriWindow = window.__TAURI__.window.getTauriWindow(label)
-      var button = document.createElement("button");
-      button.innerHTML = "Send message to " + label;
-      button.addEventListener("click", function () {
-        tauriWindow.emit("clicked", "message from " + windowLabel);
-      });
-      container.appendChild(button);
+      createWindowMessageBtn(label)
     }
   </script>
 </body>

+ 1 - 1
tauri/examples/multiwindow/src-tauri/Cargo.lock

@@ -3513,7 +3513,7 @@ dependencies = [
 [[package]]
 name = "wry"
 version = "0.4.1"
-source = "git+https://github.com/tauri-apps/wry?rev=af07c28503e41a0a164cb7256fa0ec938d5daee4#af07c28503e41a0a164cb7256fa0ec938d5daee4"
+source = "git+https://github.com/tauri-apps/wry?rev=e6cc7f0825220a0117827b6f0a366f60ce7420ea#e6cc7f0825220a0117827b6f0a366f60ce7420ea"
 dependencies = [
  "cc",
  "cocoa",

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

@@ -7,6 +7,8 @@
 #[config_path = "examples/multiwindow/src-tauri/tauri.conf.json"]
 struct Context;
 
+use tauri::WebviewBuilderExt;
+
 fn main() {
   tauri::AppBuilder::<tauri::flavors::Wry, Context>::new()
     .setup(|webview_manager| async move {
@@ -21,6 +23,11 @@ fn main() {
         println!("got 'clicked' event on window '{}'", label)
       });
     })
+    .create_webview("Rust".to_string(), tauri::WindowUrl::App, |mut builder| {
+      builder = builder.title("Tauri - Rust");
+      Ok(builder)
+    })
+    .unwrap()
     .build()
     .unwrap()
     .run();

+ 181 - 19
tauri/src/app.rs

@@ -1,22 +1,26 @@
 use futures::future::BoxFuture;
 use serde_json::Value as JsonValue;
-use std::marker::PhantomData;
 use tauri_api::{config::Config, private::AsTauriContext};
 
+use crate::async_runtime::Mutex;
+
+use std::{collections::HashMap, marker::PhantomData, sync::Arc};
+
 pub(crate) mod event;
-mod runner;
-mod webview;
+mod utils;
+pub(crate) mod webview;
 mod webview_manager;
 
+pub use crate::api::config::WindowUrl;
 pub use webview::{
   wry::WryApplication, ApplicationDispatcherExt, ApplicationExt, Callback, Icon, Message,
   WebviewBuilderExt,
 };
 pub use webview_manager::{WebviewDispatcher, WebviewManager};
 
-type InvokeHandler<D> =
-  dyn Fn(WebviewManager<D>, String) -> BoxFuture<'static, crate::Result<JsonValue>> + Send + Sync;
-type Setup<D> = dyn Fn(WebviewManager<D>) -> BoxFuture<'static, ()> + Send + Sync;
+type InvokeHandler<A> =
+  dyn Fn(WebviewManager<A>, String) -> BoxFuture<'static, crate::Result<JsonValue>> + Send + Sync;
+type Setup<A> = dyn Fn(WebviewManager<A>) -> BoxFuture<'static, ()> + Send + Sync;
 
 /// `App` runtime information.
 pub struct Context {
@@ -35,20 +39,48 @@ impl Context {
   }
 }
 
+pub(crate) struct Webview<A: ApplicationExt> {
+  pub(crate) builder: A::WebviewBuilder,
+  pub(crate) label: String,
+  pub(crate) url: WindowUrl,
+}
+
 /// The application runner.
 pub struct App<A: ApplicationExt> {
   /// The JS message handler.
-  invoke_handler: Option<Box<InvokeHandler<A::Dispatcher>>>,
+  invoke_handler: Option<Box<InvokeHandler<A>>>,
   /// The setup callback, invoked when the webview is ready.
-  setup: Option<Box<Setup<A::Dispatcher>>>,
+  setup: Option<Box<Setup<A>>>,
   /// The context the App was created with
   pub(crate) context: Context,
+  pub(crate) dispatchers: Arc<Mutex<HashMap<String, WebviewDispatcher<A::Dispatcher>>>>,
+  pub(crate) webviews: Option<Vec<Webview<A>>>,
+  url: String,
+  window_labels: Arc<Mutex<Vec<String>>>,
+  plugin_initialization_script: String,
 }
 
 impl<A: ApplicationExt + 'static> App<A> {
   /// Runs the app until it finishes.
-  pub fn run(self) {
-    runner::run(self).expect("Failed to build webview");
+  pub fn run(mut self) {
+    {
+      let mut window_labels = crate::async_runtime::block_on(self.window_labels.lock());
+      for window_config in self.context.config.tauri.windows.clone() {
+        let window_url = window_config.url.clone();
+        let window_label = window_config.label.to_string();
+        window_labels.push(window_label.to_string());
+        let webview = A::WebviewBuilder::from(webview::WindowConfig(window_config));
+        let mut webviews = self.webviews.take().unwrap();
+        webviews.push(Webview {
+          label: window_label,
+          builder: webview,
+          url: window_url,
+        });
+        self.webviews = Some(webviews);
+      }
+    }
+
+    run(self).expect("failed to run application");
   }
 
   /// Runs the invoke handler if defined.
@@ -56,7 +88,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: &WebviewManager<A::Dispatcher>,
+    dispatcher: &WebviewManager<A>,
     arg: &JsonValue,
   ) -> crate::Result<Option<JsonValue>> {
     if let Some(ref invoke_handler) = self.invoke_handler {
@@ -68,7 +100,7 @@ impl<A: ApplicationExt + 'static> App<A> {
   }
 
   /// Runs the setup callback if defined.
-  pub(crate) async fn run_setup(&self, dispatcher: &WebviewManager<A::Dispatcher>) {
+  pub(crate) async fn run_setup(&self, dispatcher: &WebviewManager<A>) {
     if let Some(ref setup) = self.setup {
       let fut = setup(dispatcher.clone());
       fut.await;
@@ -76,15 +108,78 @@ impl<A: ApplicationExt + 'static> App<A> {
   }
 }
 
+#[async_trait::async_trait]
+trait WebviewInitializer<A: ApplicationExt> {
+  async fn init_webview(
+    &self,
+    webview: Webview<A>,
+  ) -> crate::Result<(
+    <A as ApplicationExt>::WebviewBuilder,
+    Vec<Callback<A::Dispatcher>>,
+  )>;
+
+  async fn on_webview_created(
+    &self,
+    webview_label: String,
+    dispatcher: A::Dispatcher,
+    manager: WebviewManager<A>,
+  );
+}
+
+#[async_trait::async_trait]
+impl<A: ApplicationExt + 'static> WebviewInitializer<A> for Arc<App<A>> {
+  async fn init_webview(
+    &self,
+    webview: Webview<A>,
+  ) -> crate::Result<(
+    <A as ApplicationExt>::WebviewBuilder,
+    Vec<Callback<A::Dispatcher>>,
+  )> {
+    let webview_manager = WebviewManager::new(
+      self.clone(),
+      self.dispatchers.clone(),
+      webview.label.to_string(),
+    );
+    utils::build_webview(
+      self.clone(),
+      webview,
+      &webview_manager,
+      &self.url,
+      &self.window_labels.lock().await,
+      &self.plugin_initialization_script,
+      &self.context.tauri_script,
+    )
+  }
+
+  async fn on_webview_created(
+    &self,
+    webview_label: String,
+    dispatcher: A::Dispatcher,
+    manager: WebviewManager<A>,
+  ) {
+    self.dispatchers.lock().await.insert(
+      webview_label.to_string(),
+      WebviewDispatcher::new(dispatcher.clone(), webview_label),
+    );
+
+    crate::async_runtime::spawn_task(async move {
+      crate::plugin::created(A::plugin_store(), &manager).await
+    });
+  }
+}
+
 /// The App builder.
 #[derive(Default)]
 pub struct AppBuilder<A: ApplicationExt, C: AsTauriContext> {
   /// The JS message handler.
-  invoke_handler: Option<Box<InvokeHandler<A::Dispatcher>>>,
+  invoke_handler: Option<Box<InvokeHandler<A>>>,
   /// The setup callback, invoked when the webview is ready.
-  setup: Option<Box<Setup<A::Dispatcher>>>,
-  /// The configuration used
+  setup: Option<Box<Setup<A>>>,
   config: PhantomData<C>,
+  /// The webview dispatchers.
+  dispatchers: Arc<Mutex<HashMap<String, WebviewDispatcher<A::Dispatcher>>>>,
+  /// The created webviews.
+  webviews: Vec<Webview<A>>,
 }
 
 impl<A: ApplicationExt + 'static, C: AsTauriContext> AppBuilder<A, C> {
@@ -94,13 +189,15 @@ impl<A: ApplicationExt + 'static, C: AsTauriContext> AppBuilder<A, C> {
       invoke_handler: None,
       setup: None,
       config: Default::default(),
+      dispatchers: Default::default(),
+      webviews: Default::default(),
     }
   }
 
   /// Defines the JS message handler callback.
   pub fn invoke_handler<
     T: futures::Future<Output = crate::Result<JsonValue>> + Send + Sync + 'static,
-    F: Fn(WebviewManager<A::Dispatcher>, String) -> T + Send + Sync + 'static,
+    F: Fn(WebviewManager<A>, String) -> T + Send + Sync + 'static,
   >(
     mut self,
     invoke_handler: F,
@@ -114,7 +211,7 @@ 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(WebviewManager<A::Dispatcher>) -> T + Send + Sync + 'static,
+    F: Fn(WebviewManager<A>) -> T + Send + Sync + 'static,
   >(
     mut self,
     setup: F,
@@ -128,18 +225,83 @@ impl<A: ApplicationExt + 'static, C: AsTauriContext> AppBuilder<A, C> {
   /// Adds a plugin to the runtime.
   pub fn plugin(
     self,
-    plugin: impl crate::plugin::Plugin<A::Dispatcher> + Send + Sync + Sync + 'static,
+    plugin: impl crate::plugin::Plugin<A> + Send + Sync + Sync + 'static,
   ) -> Self {
     crate::async_runtime::block_on(crate::plugin::register(A::plugin_store(), plugin));
     self
   }
 
+  /// Creates a new webview.
+  pub fn create_webview<F: FnOnce(A::WebviewBuilder) -> crate::Result<A::WebviewBuilder>>(
+    mut self,
+    label: String,
+    url: WindowUrl,
+    f: F,
+  ) -> crate::Result<Self> {
+    let builder = f(A::WebviewBuilder::new())?;
+    self.webviews.push(Webview {
+      label,
+      builder,
+      url,
+    });
+    Ok(self)
+  }
+
   /// Builds the App.
   pub fn build(self) -> crate::Result<App<A>> {
+    let window_labels: Vec<String> = self.webviews.iter().map(|w| w.label.to_string()).collect();
+    let plugin_initialization_script =
+      crate::async_runtime::block_on(crate::plugin::initialization_script(A::plugin_store()));
+
+    let context = Context::new::<C>()?;
+    let url = utils::get_url(&context)?;
+
     Ok(App {
       invoke_handler: self.invoke_handler,
       setup: self.setup,
-      context: Context::new::<C>()?,
+      context,
+      dispatchers: self.dispatchers,
+      webviews: Some(self.webviews),
+      url,
+      window_labels: Arc::new(Mutex::new(window_labels)),
+      plugin_initialization_script,
     })
   }
 }
+
+fn run<A: ApplicationExt + 'static>(mut application: App<A>) -> crate::Result<()> {
+  let plugin_config = application.context.config.plugins.clone();
+  crate::async_runtime::block_on(async move {
+    crate::plugin::initialize(A::plugin_store(), plugin_config).await
+  })?;
+
+  #[cfg(embedded_server)]
+  utils::spawn_server(application.url.to_string(), &application.context);
+
+  let webviews = application.webviews.take().unwrap();
+
+  let application = Arc::new(application);
+  let mut webview_app = A::new()?;
+
+  for webview in webviews {
+    let webview_label = webview.label.to_string();
+    let webview_manager = WebviewManager::new(
+      application.clone(),
+      application.dispatchers.clone(),
+      webview_label.to_string(),
+    );
+    let (webview_builder, callbacks) =
+      crate::async_runtime::block_on(application.init_webview(webview))?;
+
+    let dispatcher = webview_app.create_webview(webview_builder, callbacks)?;
+    crate::async_runtime::block_on(application.on_webview_created(
+      webview_label,
+      dispatcher,
+      webview_manager,
+    ));
+  }
+
+  webview_app.run();
+
+  Ok(())
+}

+ 80 - 195
tauri/src/app/runner.rs → tauri/src/app/utils.rs

@@ -1,6 +1,6 @@
 #[cfg(dev)]
 use std::io::Read;
-use std::{collections::HashMap, sync::Arc};
+use std::sync::Arc;
 
 #[cfg(dev)]
 use crate::api::assets::{AssetFetch, Assets};
@@ -13,20 +13,16 @@ use crate::{
   ApplicationExt, WebviewBuilderExt,
 };
 
-use super::{App, ApplicationDispatcherExt, WebviewDispatcher, WebviewManager};
+use super::{
+  webview::{Callback, WebviewBuilderExtPrivate},
+  App, Context, Webview, WebviewManager,
+};
 #[cfg(embedded_server)]
 use crate::api::tcp::{get_available_port, port_is_available};
-use crate::{app::Context, async_runtime::Mutex};
 
 use serde::{Deserialize, Serialize};
 use serde_json::Value as JsonValue;
 
-#[allow(dead_code)]
-enum Content<T> {
-  Html(T),
-  Url(T),
-}
-
 #[derive(Debug, Deserialize)]
 struct Message {
   #[serde(rename = "__tauriModule")]
@@ -39,46 +35,9 @@ struct Message {
   inner: JsonValue,
 }
 
-/// Main entry point for running the Webview
-pub(crate) fn run<A: ApplicationExt + 'static>(application: App<A>) -> crate::Result<()> {
-  let plugin_config = application.context.config.plugins.clone();
-  crate::async_runtime::block_on(async move {
-    crate::plugin::initialize(A::plugin_store(), plugin_config).await
-  })?;
-
-  // setup the content using the config struct depending on the compile target
-  let main_content = setup_content(&application.context)?;
-
-  #[cfg(embedded_server)]
-  {
-    // setup the server url for the embedded-server
-    let server_url = if let Content::Url(url) = &main_content {
-      String::from(url)
-    } else {
-      String::from("")
-    };
-
-    // spawn the embedded server on our server url
-    #[cfg(embedded_server)]
-    spawn_server(server_url, &application.context);
-  }
-
-  // build the webview
-  let webview_application = build_webview(application, main_content)?;
-
-  // spin up the updater process
-  #[cfg(feature = "updater")]
-  spawn_updater();
-
-  // run the webview
-  webview_application.run();
-
-  Ok(())
-}
-
 // setup content for dev-server
 #[cfg(dev)]
-fn setup_content(context: &Context) -> crate::Result<Content<String>> {
+pub(super) fn get_url(context: &Context) -> crate::Result<String> {
   let config = &context.config;
   if config.build.dev_path.starts_with("http") {
     #[cfg(windows)]
@@ -104,9 +63,9 @@ fn setup_content(context: &Context) -> crate::Result<Content<String>> {
           .expect("failed to run Loopback command");
       }
     }
-    Ok(Content::Url(config.build.dev_path.clone()))
+    Ok(config.build.dev_path.clone())
   } else {
-    Ok(Content::Html(format!(
+    Ok(format!(
       "data:text/html;base64,{}",
       base64::encode(
         context
@@ -121,17 +80,16 @@ fn setup_content(context: &Context) -> crate::Result<Content<String>> {
           })
           .expect("Unable to find `index.html` under your devPath folder")
       )
-    )))
+    ))
   }
 }
 
 // setup content for embedded server
 #[cfg(embedded_server)]
-fn setup_content(context: &Context) -> crate::Result<Content<String>> {
+pub(super) fn get_url(context: &Context) -> crate::Result<String> {
   let (port, valid) = setup_port(&context);
   if valid {
-    let url = setup_server_url(port, &context);
-    Ok(Content::Url(url))
+    Ok(setup_server_url(port, &context))
   } else {
     Err(crate::Error::PortNotAvailable(port))
   }
@@ -168,7 +126,7 @@ fn setup_server_url(port: String, context: &Context) -> String {
 
 // spawn the embedded server
 #[cfg(embedded_server)]
-fn spawn_server(server_url: String, context: &Context) {
+pub(super) fn spawn_server(server_url: String, context: &Context) {
   let assets = context.assets;
   let public_path = context.config.tauri.embedded_server.public_path.clone();
   std::thread::spawn(move || {
@@ -200,7 +158,8 @@ fn spawn_server(server_url: String, context: &Context) {
 
 // spawn an updater process.
 #[cfg(feature = "updater")]
-fn spawn_updater() {
+#[allow(dead_code)]
+pub(super) fn spawn_updater() {
   std::thread::spawn(|| {
     tauri_api::command::spawn_relative_command(
       "updater".to_string(),
@@ -211,10 +170,30 @@ fn spawn_updater() {
   });
 }
 
-pub fn event_initialization_script() -> String {
-  #[cfg(not(event))]
-  return String::from("");
-  #[cfg(event)]
+pub(super) fn initialization_script(
+  plugin_initialization_script: &str,
+  tauri_script: &str,
+) -> String {
+  format!(
+    r#"
+      {tauri_initialization_script}
+      {event_initialization_script}
+      if (window.__TAURI_INVOKE_HANDLER__) {{
+        window.__TAURI__.invoke({{ cmd: "__initialized" }})
+      }} else {{
+        window.addEventListener('DOMContentLoaded', function () {{
+          window.__TAURI__.invoke({{ cmd: "__initialized" }})
+        }})
+      }}
+      {plugin_initialization_script}
+    "#,
+    tauri_initialization_script = tauri_script,
+    event_initialization_script = event_initialization_script(),
+    plugin_initialization_script = plugin_initialization_script
+  )
+}
+
+fn event_initialization_script() -> String {
   return format!(
     "
       window['{queue}'] = [];
@@ -253,82 +232,39 @@ pub fn event_initialization_script() -> String {
   );
 }
 
-// build the webview struct
-fn build_webview<A: ApplicationExt + 'static>(
-  application: App<A>,
-  content: Content<String>,
-) -> crate::Result<A> {
+pub(super) type BuiltWebview<A> = (
+  <A as ApplicationExt>::WebviewBuilder,
+  Vec<Callback<<A as ApplicationExt>::Dispatcher>>,
+);
+
+// build the webview.
+pub(super) fn build_webview<A: ApplicationExt + 'static>(
+  application: Arc<App<A>>,
+  webview: Webview<A>,
+  webview_manager: &WebviewManager<A>,
+  content_url: &str,
+  window_labels: &[String],
+  plugin_initialization_script: &str,
+  tauri_script: &str,
+) -> crate::Result<BuiltWebview<A>> {
   // TODO let debug = cfg!(debug_assertions);
-  let content_url = match content {
-    Content::Html(s) => s,
-    Content::Url(s) => s,
+  let webview_url = match &webview.url {
+    WindowUrl::App => content_url.to_string(),
+    WindowUrl::Custom(url) => url.to_string(),
   };
 
-  let initialization_script = format!(
-    r#"
-      {tauri_initialization_script}
-      {event_initialization_script}
-      if (window.__TAURI_INVOKE_HANDLER__) {{
-        window.__TAURI__.invoke({{ cmd: "__initialized" }})
-      }} else {{
-        window.addEventListener('DOMContentLoaded', function () {{
-          window.__TAURI__.invoke({{ cmd: "__initialized" }})
-        }})
-      }}
-      {plugin_initialization_script}
-    "#,
-    tauri_initialization_script = application.context.tauri_script,
-    event_initialization_script = event_initialization_script(),
-    plugin_initialization_script =
-      crate::async_runtime::block_on(crate::plugin::initialization_script(A::plugin_store()))
-  );
-
-  let application = Arc::new(application);
-
-  let mut webview_application = A::new()?;
-
-  let dispatchers = Arc::new(Mutex::new(HashMap::new()));
-  let mut window_labels = Vec::new();
-
-  for window_config in &application.context.config.tauri.windows {
-    window_labels.push(window_config.label.to_string());
-  }
-
-  for window_config in application.context.config.tauri.windows.clone() {
-    let mut webview = A::WebviewBuilder::new()
-      .title(window_config.title.to_string())
-      .width(window_config.width)
-      .height(window_config.height)
-      .visible(window_config.visible)
-      .resizable(window_config.resizable)
-      .decorations(window_config.decorations)
-      .maximized(window_config.maximized)
-      .fullscreen(window_config.fullscreen)
-      .transparent(window_config.transparent)
-      .always_on_top(window_config.always_on_top);
-    if let Some(min_width) = window_config.min_width {
-      webview = webview.min_width(min_width);
-    }
-    if let Some(min_height) = window_config.min_height {
-      webview = webview.min_height(min_height);
-    }
-    if let Some(max_width) = window_config.max_width {
-      webview = webview.max_width(max_width);
-    }
-    if let Some(max_height) = window_config.max_height {
-      webview = webview.max_height(max_height);
-    }
-    if let Some(x) = window_config.x {
-      webview = webview.x(x);
-    }
-    if let Some(y) = window_config.y {
-      webview = webview.y(y);
-    }
-
-    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_builder, callbacks) = if webview.url == WindowUrl::App {
+    let webview_builder = webview.builder.url(webview_url)
+        .initialization_script(&initialization_script(plugin_initialization_script, tauri_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(&window_labels).unwrap(),
+          current_window_label = webview.label,
+        ));
 
     let webview_manager_ = webview_manager.clone();
     let tauri_invoke_handler = crate::Callback::<A::Dispatcher> {
@@ -379,36 +315,12 @@ fn build_webview<A: ApplicationExt + 'static>(
         0
       }),
     };
+    (webview_builder, vec![tauri_invoke_handler])
+  } else {
+    (webview.builder.url(webview_url), Vec::new())
+  };
 
-    let webview_url = match &window_config.url {
-      WindowUrl::App => content_url.to_string(),
-      WindowUrl::Custom(url) => url.to_string(),
-    };
-
-    webview = webview.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(&window_labels).unwrap(),
-          current_window_label = window_config.label,
-        ));
-
-    let dispatcher = webview_application.create_webview(webview, vec![tauri_invoke_handler])?;
-    crate::async_runtime::block_on(dispatchers.lock()).insert(
-      window_config.label.to_string(),
-      WebviewDispatcher::new(dispatcher, window_config.label.to_string()),
-    );
-
-    crate::async_runtime::spawn(async move {
-      crate::plugin::created(A::plugin_store(), &webview_manager).await
-    });
-  }
-
-  Ok(webview_application)
+  Ok((webview_builder, callbacks))
 }
 
 /// Asynchronously executes the given task
@@ -417,11 +329,11 @@ fn build_webview<A: ApplicationExt + 'static>(
 /// If the Result `is_ok()`, the callback will be the `success_callback` function name and the argument will be the Ok value.
 /// If the Result `is_err()`, the callback will be the `error_callback` function name and the argument will be the Err value.
 async fn execute_promise<
-  D: ApplicationDispatcherExt,
+  A: ApplicationExt + 'static,
   R: Serialize,
   F: futures::Future<Output = crate::Result<R>> + Send + 'static,
 >(
-  webview_manager: &crate::WebviewManager<D>,
+  webview_manager: &crate::WebviewManager<A>,
   task: F,
   success_callback: String,
   error_callback: String,
@@ -441,7 +353,7 @@ async fn execute_promise<
 
 async fn on_message<A: ApplicationExt + 'static>(
   application: Arc<App<A>>,
-  webview_manager: WebviewManager<<A as ApplicationExt>::Dispatcher>,
+  webview_manager: WebviewManager<A>,
   message: Message,
 ) -> crate::Result<JsonValue> {
   if message.inner == serde_json::json!({ "cmd":"__initialized" }) {
@@ -484,24 +396,21 @@ async fn on_message<A: ApplicationExt + 'static>(
 
 #[cfg(test)]
 mod test {
-  use super::Content;
   use crate::{Context, FromTauriContext};
   use proptest::prelude::*;
-  #[cfg(dev)]
-  use std::io::Read;
 
   #[derive(FromTauriContext)]
   #[config_path = "test/fixture/src-tauri/tauri.conf.json"]
   struct TauriContext;
 
   #[test]
-  fn check_setup_content() {
+  fn check_get_url() {
     let context = Context::new::<TauriContext>().unwrap();
-    let res = super::setup_content(&context);
+    let res = super::get_url(&context);
 
     #[cfg(embedded_server)]
     match res {
-      Ok(Content::Url(ref u)) => assert!(u.contains("http://")),
+      Ok(u) => assert!(u.contains("http://")),
       _ => panic!("setup content failed"),
     }
 
@@ -509,31 +418,7 @@ mod test {
     {
       let config = &context.config;
       match res {
-        Ok(Content::Url(dp)) => assert_eq!(dp, config.build.dev_path),
-        Ok(Content::Html(s)) => {
-          assert_eq!(
-            s,
-            format!(
-              "data:text/html;base64,{}",
-              base64::encode(
-                context
-                  .assets
-                  .get(
-                    &crate::api::assets::Assets::format_key("index.html"),
-                    crate::api::assets::AssetFetch::Decompress
-                  )
-                  .ok_or_else(|| crate::Error::AssetNotFound("index.html".to_string()))
-                  .and_then(|(read, _)| {
-                    read
-                      .bytes()
-                      .collect::<Result<Vec<u8>, _>>()
-                      .map_err(Into::into)
-                  })
-                  .expect("Unable to find `index.html` under your dist folder")
-              )
-            )
-          );
-        }
+        Ok(u) => assert_eq!(u, config.build.dev_path),
         _ => panic!("setup content failed"),
       }
     }

+ 28 - 7
tauri/src/app/webview.rs

@@ -80,6 +80,13 @@ pub enum Message {
   SetIcon(Icon),
 }
 
+pub struct WindowConfig(pub crate::api::config::WindowConfig);
+
+pub trait WebviewBuilderExtPrivate: Sized {
+  /// Sets the webview url.
+  fn url(self, url: String) -> Self;
+}
+
 /// The webview builder.
 pub trait WebviewBuilderExt: Sized {
   /// The webview object that this builder creates.
@@ -88,9 +95,6 @@ pub trait WebviewBuilderExt: Sized {
   /// Initializes a new webview builder.
   fn new() -> Self;
 
-  /// Sets the webview url.
-  fn url(self, url: String) -> Self;
-
   /// Sets the init script.
   fn initialization_script(self, init: &str) -> Self;
 
@@ -122,7 +126,7 @@ pub trait WebviewBuilderExt: Sized {
   fn resizable(self, resizable: bool) -> Self;
 
   /// The title of the window in the title bar.
-  fn title(self, title: String) -> Self;
+  fn title<S: Into<String>>(self, title: S) -> Self;
 
   /// Whether to start the window in fullscreen or not.
   fn fullscreen(self, fullscreen: bool) -> Self;
@@ -157,6 +161,19 @@ pub struct Callback<D> {
 
 /// Webview dispatcher. A thread-safe handle to the webview API.
 pub trait ApplicationDispatcherExt: Clone + Send + Sync + Sized {
+  /// The webview builder type.
+  type WebviewBuilder: WebviewBuilderExt
+    + WebviewBuilderExtPrivate
+    + From<WindowConfig>
+    + Send
+    + Sync;
+  /// Creates a webview.
+  fn create_webview(
+    &self,
+    webview_builder: Self::WebviewBuilder,
+    callbacks: Vec<Callback<Self>>,
+  ) -> crate::Result<Self>;
+
   /// Updates the window resizable flag.
   fn set_resizable(&self, resizable: bool) -> crate::Result<()>;
 
@@ -228,12 +245,16 @@ pub trait ApplicationDispatcherExt: Clone + Send + Sync + Sized {
 /// Manages windows and webviews.
 pub trait ApplicationExt: Sized {
   /// The webview builder.
-  type WebviewBuilder: WebviewBuilderExt;
+  type WebviewBuilder: WebviewBuilderExt
+    + WebviewBuilderExtPrivate
+    + From<WindowConfig>
+    + Send
+    + Sync;
   /// The message dispatcher.
-  type Dispatcher: ApplicationDispatcherExt;
+  type Dispatcher: ApplicationDispatcherExt<WebviewBuilder = Self::WebviewBuilder>;
 
   /// Returns the static plugin collection.
-  fn plugin_store() -> &'static PluginStore<Self::Dispatcher>;
+  fn plugin_store() -> &'static PluginStore<Self>;
 
   /// Creates a new application.
   fn new() -> crate::Result<Self>;

+ 105 - 17
tauri/src/app/webview/wry.rs

@@ -1,4 +1,7 @@
-use super::{ApplicationDispatcherExt, ApplicationExt, Callback, Icon, WebviewBuilderExt};
+use super::{
+  ApplicationDispatcherExt, ApplicationExt, Callback, Icon, WebviewBuilderExt,
+  WebviewBuilderExtPrivate, WindowConfig,
+};
 
 use once_cell::sync::Lazy;
 
@@ -24,8 +27,50 @@ impl TryInto<wry::Icon> for Icon {
   }
 }
 
+impl WebviewBuilderExtPrivate for wry::Attributes {
+  fn url(mut self, url: String) -> Self {
+    self.url.replace(url);
+    self
+  }
+}
+
+impl From<WindowConfig> for wry::Attributes {
+  fn from(window_config: WindowConfig) -> Self {
+    let mut webview = wry::Attributes::default()
+      .title(window_config.0.title.to_string())
+      .width(window_config.0.width)
+      .height(window_config.0.height)
+      .visible(window_config.0.visible)
+      .resizable(window_config.0.resizable)
+      .decorations(window_config.0.decorations)
+      .maximized(window_config.0.maximized)
+      .fullscreen(window_config.0.fullscreen)
+      .transparent(window_config.0.transparent)
+      .always_on_top(window_config.0.always_on_top);
+    if let Some(min_width) = window_config.0.min_width {
+      webview = webview.min_width(min_width);
+    }
+    if let Some(min_height) = window_config.0.min_height {
+      webview = webview.min_height(min_height);
+    }
+    if let Some(max_width) = window_config.0.max_width {
+      webview = webview.max_width(max_width);
+    }
+    if let Some(max_height) = window_config.0.max_height {
+      webview = webview.max_height(max_height);
+    }
+    if let Some(x) = window_config.0.x {
+      webview = webview.x(x);
+    }
+    if let Some(y) = window_config.0.y {
+      webview = webview.y(y);
+    }
+    webview
+  }
+}
+
 /// The webview builder.
-impl WebviewBuilderExt for wry::WebViewAttributes {
+impl WebviewBuilderExt for wry::Attributes {
   /// The webview object that this builder creates.
   type Webview = Self;
 
@@ -33,11 +78,6 @@ impl WebviewBuilderExt for wry::WebViewAttributes {
     Default::default()
   }
 
-  fn url(mut self, url: String) -> Self {
-    self.url.replace(url);
-    self
-  }
-
   fn initialization_script(mut self, init: &str) -> Self {
     self.initialization_scripts.push(init.to_string());
     self
@@ -88,8 +128,8 @@ impl WebviewBuilderExt for wry::WebViewAttributes {
     self
   }
 
-  fn title(mut self, title: String) -> Self {
-    self.title = title;
+  fn title<S: Into<String>>(mut self, title: S) -> Self {
+    self.title = title.into();
     self
   }
 
@@ -129,9 +169,48 @@ impl WebviewBuilderExt for wry::WebViewAttributes {
 }
 
 #[derive(Clone)]
-pub struct WryDispatcher(Arc<Mutex<wry::WindowDispatcher>>);
+pub struct WryDispatcher(
+  Arc<Mutex<wry::WindowProxy>>,
+  Arc<Mutex<wry::ApplicationProxy>>,
+);
 
 impl ApplicationDispatcherExt for WryDispatcher {
+  type WebviewBuilder = wry::Attributes;
+
+  fn create_webview(
+    &self,
+    attributes: Self::WebviewBuilder,
+    callbacks: Vec<Callback<Self>>,
+  ) -> crate::Result<Self> {
+    let mut wry_callbacks = Vec::new();
+    let app_dispatcher = self.1.clone();
+    for mut callback in callbacks {
+      let app_dispatcher = app_dispatcher.clone();
+      let callback = wry::Callback {
+        name: callback.name.to_string(),
+        function: Box::new(move |dispatcher, seq, req| {
+          (callback.function)(
+            Self(Arc::new(Mutex::new(dispatcher)), app_dispatcher.clone()),
+            seq,
+            req,
+          )
+        }),
+      };
+      wry_callbacks.push(callback);
+    }
+
+    let window_dispatcher = self
+      .1
+      .lock()
+      .unwrap()
+      .add_window(attributes, Some(wry_callbacks))
+      .map_err(|_| crate::Error::FailedToSendMessage)?;
+    Ok(Self(
+      Arc::new(Mutex::new(window_dispatcher)),
+      self.1.clone(),
+    ))
+  }
+
   fn set_resizable(&self, resizable: bool) -> crate::Result<()> {
     self
       .0
@@ -326,7 +405,7 @@ impl ApplicationDispatcherExt for WryDispatcher {
       .0
       .lock()
       .unwrap()
-      .eval_script(script)
+      .evaluate_script(script)
       .map_err(|_| crate::Error::FailedToSendMessage)
   }
 }
@@ -337,11 +416,11 @@ pub struct WryApplication {
 }
 
 impl ApplicationExt for WryApplication {
-  type WebviewBuilder = wry::WebViewAttributes;
+  type WebviewBuilder = wry::Attributes;
   type Dispatcher = WryDispatcher;
 
-  fn plugin_store() -> &'static PluginStore<Self::Dispatcher> {
-    static PLUGINS: Lazy<PluginStore<WryDispatcher>> = Lazy::new(Default::default);
+  fn plugin_store() -> &'static PluginStore<Self> {
+    static PLUGINS: Lazy<PluginStore<WryApplication>> = Lazy::new(Default::default);
     &PLUGINS
   }
 
@@ -356,11 +435,17 @@ impl ApplicationExt for WryApplication {
     callbacks: Vec<Callback<Self::Dispatcher>>,
   ) -> crate::Result<Self::Dispatcher> {
     let mut wry_callbacks = Vec::new();
+    let app_dispatcher = Arc::new(Mutex::new(self.inner.application_proxy()));
     for mut callback in callbacks {
+      let app_dispatcher = app_dispatcher.clone();
       let callback = wry::Callback {
         name: callback.name.to_string(),
         function: Box::new(move |dispatcher, seq, req| {
-          (callback.function)(WryDispatcher(Arc::new(Mutex::new(dispatcher))), seq, req)
+          (callback.function)(
+            WryDispatcher(Arc::new(Mutex::new(dispatcher)), app_dispatcher.clone()),
+            seq,
+            req,
+          )
         }),
       };
       wry_callbacks.push(callback);
@@ -368,9 +453,12 @@ impl ApplicationExt for WryApplication {
 
     let dispatcher = self
       .inner
-      .create_webview(webview_builder.finish()?, Some(wry_callbacks))
+      .add_window(webview_builder.finish()?, Some(wry_callbacks))
       .map_err(|_| crate::Error::CreateWebview)?;
-    Ok(WryDispatcher(Arc::new(Mutex::new(dispatcher))))
+    Ok(WryDispatcher(
+      Arc::new(Mutex::new(dispatcher)),
+      app_dispatcher,
+    ))
   }
 
   fn run(self) {

+ 83 - 9
tauri/src/app/webview_manager.rs

@@ -1,7 +1,10 @@
 use std::{collections::HashMap, sync::Arc};
 
-use super::{ApplicationDispatcherExt, Icon};
-use crate::async_runtime::Mutex;
+use super::{
+  App, ApplicationDispatcherExt, ApplicationExt, Icon, Webview, WebviewBuilderExt,
+  WebviewInitializer,
+};
+use crate::{api::config::WindowUrl, async_runtime::Mutex};
 
 use serde::Serialize;
 
@@ -172,18 +175,30 @@ impl<A: ApplicationDispatcherExt> WebviewDispatcher<A> {
 }
 
 /// The webview manager.
-#[derive(Clone)]
-pub struct WebviewManager<A: Clone> {
-  dispatchers: Arc<Mutex<HashMap<String, WebviewDispatcher<A>>>>,
+pub struct WebviewManager<A: ApplicationExt> {
+  application: Arc<App<A>>,
+  dispatchers: Arc<Mutex<HashMap<String, WebviewDispatcher<A::Dispatcher>>>>,
   current_webview_window_label: String,
 }
 
-impl<A: ApplicationDispatcherExt> WebviewManager<A> {
+impl<A: ApplicationExt> Clone for WebviewManager<A> {
+  fn clone(&self) -> Self {
+    Self {
+      application: self.application.clone(),
+      dispatchers: self.dispatchers.clone(),
+      current_webview_window_label: self.current_webview_window_label.to_string(),
+    }
+  }
+}
+
+impl<A: ApplicationExt + 'static> WebviewManager<A> {
   pub(crate) fn new(
-    dispatchers: Arc<Mutex<HashMap<String, WebviewDispatcher<A>>>>,
+    application: Arc<App<A>>,
+    dispatchers: Arc<Mutex<HashMap<String, WebviewDispatcher<A::Dispatcher>>>>,
     label: String,
   ) -> Self {
     Self {
+      application,
       dispatchers,
       current_webview_window_label: label,
     }
@@ -195,12 +210,15 @@ impl<A: ApplicationDispatcherExt> WebviewManager<A> {
   }
 
   /// Gets the webview associated with the current context.
-  pub async fn current_webview(&self) -> crate::Result<WebviewDispatcher<A>> {
+  pub async fn current_webview(&self) -> crate::Result<WebviewDispatcher<A::Dispatcher>> {
     self.get_webview(&self.current_webview_window_label).await
   }
 
   /// Gets the webview associated with the given window label.
-  pub async fn get_webview(&self, window_label: &str) -> crate::Result<WebviewDispatcher<A>> {
+  pub async fn get_webview(
+    &self,
+    window_label: &str,
+  ) -> crate::Result<WebviewDispatcher<A::Dispatcher>> {
     self
       .dispatchers
       .lock()
@@ -210,6 +228,48 @@ impl<A: ApplicationDispatcherExt> WebviewManager<A> {
       .map(|d| d.clone())
   }
 
+  /// Creates a new webview.
+  pub async fn create_webview<F: FnOnce(A::WebviewBuilder) -> crate::Result<A::WebviewBuilder>>(
+    &self,
+    label: String,
+    url: WindowUrl,
+    f: F,
+  ) -> crate::Result<WebviewDispatcher<A::Dispatcher>> {
+    let builder = f(A::WebviewBuilder::new())?;
+    let webview = Webview {
+      url,
+      label: label.to_string(),
+      builder,
+    };
+    self
+      .application
+      .window_labels
+      .lock()
+      .await
+      .push(label.to_string());
+    let (webview_builder, callbacks) = self.application.init_webview(webview).await?;
+
+    let window_dispatcher = self
+      .current_webview()
+      .await?
+      .dispatcher
+      .create_webview(webview_builder, callbacks)?;
+    let webview_manager = Self::new(
+      self.application.clone(),
+      self.dispatchers.clone(),
+      label.to_string(),
+    );
+    self
+      .application
+      .on_webview_created(
+        label.to_string(),
+        window_dispatcher.clone(),
+        webview_manager,
+      )
+      .await;
+    Ok(WebviewDispatcher::new(window_dispatcher, label))
+  }
+
   /// Listen to a global event.
   /// An event from any webview will trigger the handler.
   pub fn listen<F: FnMut(Option<String>) + Send + 'static>(
@@ -232,6 +292,20 @@ impl<A: ApplicationDispatcherExt> WebviewManager<A> {
     Ok(())
   }
 
+  pub(crate) async fn emit_except<S: Serialize + Clone>(
+    &self,
+    except_label: String,
+    event: impl AsRef<str>,
+    payload: Option<S>,
+  ) -> crate::Result<()> {
+    for dispatcher in self.dispatchers.lock().await.values() {
+      if dispatcher.window_label != except_label {
+        super::event::emit(&dispatcher, event.as_ref(), payload.clone())?;
+      }
+    }
+    Ok(())
+  }
+
   /// Emits a global event from the webview.
   pub(crate) fn on_event(&self, event: String, data: Option<String>) {
     super::event::on_event(event, None, data)

+ 12 - 4
tauri/src/async_runtime.rs

@@ -7,16 +7,24 @@ pub use tokio::sync::Mutex;
 
 static RUNTIME: OnceCell<StdMutex<Runtime>> = OnceCell::new();
 
-pub fn block_on<F: futures::Future>(future: F) -> F::Output {
+pub fn block_on<F: futures::Future>(task: F) -> F::Output {
   let runtime = RUNTIME.get_or_init(|| StdMutex::new(Runtime::new().unwrap()));
-  runtime.lock().unwrap().block_on(future)
+  runtime.lock().unwrap().block_on(task)
 }
 
-pub fn spawn<F>(future: F)
+pub fn spawn<F>(task: F)
 where
   F: futures::Future + Send + 'static,
   F::Output: Send + 'static,
 {
   let runtime = RUNTIME.get_or_init(|| StdMutex::new(Runtime::new().unwrap()));
-  runtime.lock().unwrap().spawn(future);
+  runtime.lock().unwrap().spawn(task);
+}
+
+pub fn spawn_task<F>(task: F)
+where
+  F: futures::Future + Send + 'static,
+  F::Output: Send + 'static,
+{
+  tokio::spawn(task);
 }

+ 5 - 6
tauri/src/endpoints.rs

@@ -1,6 +1,5 @@
 mod cli;
 mod dialog;
-#[cfg(event)]
 mod event;
 #[allow(unused_imports)]
 mod file_system;
@@ -14,7 +13,7 @@ mod notification;
 mod shell;
 mod window;
 
-use crate::{app::Context, ApplicationDispatcherExt};
+use crate::{app::Context, ApplicationExt};
 
 use serde::{Deserialize, Serialize};
 use serde_json::Value as JsonValue;
@@ -35,9 +34,9 @@ enum Module {
 }
 
 impl Module {
-  async fn run<D: ApplicationDispatcherExt + 'static>(
+  async fn run<A: ApplicationExt + 'static>(
     self,
-    webview_manager: &crate::WebviewManager<D>,
+    webview_manager: &crate::WebviewManager<A>,
     context: &Context,
   ) -> crate::Result<JsonValue> {
     match self {
@@ -55,8 +54,8 @@ impl Module {
   }
 }
 
-pub(crate) async fn handle<D: ApplicationDispatcherExt + 'static>(
-  webview_manager: &crate::WebviewManager<D>,
+pub(crate) async fn handle<A: ApplicationExt + 'static>(
+  webview_manager: &crate::WebviewManager<A>,
   module: String,
   mut arg: JsonValue,
   context: &Context,

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

@@ -9,6 +9,7 @@ pub enum Cmd {
   Listen {
     event: String,
     handler: String,
+    #[serde(default)]
     once: bool,
   },
   /// Emit an event to the webview associated with the given window.
@@ -22,13 +23,10 @@ pub enum Cmd {
 }
 
 impl Cmd {
-  pub async fn run<D: crate::ApplicationDispatcherExt + 'static>(
+  pub async fn run<A: crate::ApplicationExt + 'static>(
     self,
-    webview_manager: &crate::WebviewManager<D>,
+    webview_manager: &crate::WebviewManager<A>,
   ) -> crate::Result<JsonValue> {
-    #[cfg(not(event))]
-    return Err(crate::Error::ApiNotAllowlisted("event".to_string()));
-    #[cfg(event)]
     match self {
       Self::Listen {
         event,
@@ -62,7 +60,6 @@ impl Cmd {
   }
 }
 
-#[cfg(event)]
 pub fn listen_fn(event: String, handler: String, once: bool) -> crate::Result<String> {
   Ok(format!(
     "if (window['{listeners}'] === void 0) {{
@@ -96,7 +93,6 @@ mod test {
 
   // check the listen_fn for various usecases.
   proptest! {
-    #[cfg(event)]
     #[test]
     fn check_listen_fn(event in "", handler in "", once in proptest::bool::ANY) {
       super::listen_fn(event, handler, once).expect("listen_fn failed");

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

@@ -23,9 +23,9 @@ pub enum Cmd {
 }
 
 impl Cmd {
-  pub async fn run<D: crate::ApplicationDispatcherExt + 'static>(
+  pub async fn run<A: crate::ApplicationExt + 'static>(
     self,
-    webview_manager: &crate::WebviewManager<D>,
+    webview_manager: &crate::WebviewManager<A>,
   ) -> crate::Result<JsonValue> {
     #[cfg(not(global_shortcut))]
     return Err(crate::Error::ApiNotAllowlisted(

+ 32 - 3
tauri/src/endpoints/window.rs

@@ -1,4 +1,4 @@
-use crate::app::{ApplicationDispatcherExt, Icon};
+use crate::app::{ApplicationExt, Icon};
 use serde::Deserialize;
 use serde_json::Value as JsonValue;
 
@@ -22,6 +22,9 @@ impl Into<Icon> for IconDto {
 #[derive(Deserialize)]
 #[serde(tag = "cmd", rename_all = "camelCase")]
 pub enum Cmd {
+  CreateWebview {
+    options: crate::api::config::WindowConfig,
+  },
   SetResizable {
     resizable: bool,
   },
@@ -82,16 +85,42 @@ pub enum Cmd {
   },
 }
 
+#[cfg(create_window)]
+#[derive(Clone, serde::Serialize)]
+struct WindowCreatedEvent {
+  label: String,
+}
+
 impl Cmd {
-  pub async fn run<D: ApplicationDispatcherExt + 'static>(
+  pub async fn run<A: ApplicationExt + 'static>(
     self,
-    webview_manager: &crate::WebviewManager<D>,
+    webview_manager: &crate::WebviewManager<A>,
   ) -> crate::Result<JsonValue> {
     if cfg!(not(window)) {
       Err(crate::Error::ApiNotAllowlisted("setTitle".to_string()))
     } else {
       let current_webview = webview_manager.current_webview().await?;
       match self {
+        Self::CreateWebview { options } => {
+          #[cfg(not(create_window))]
+          return Err(crate::Error::ApiNotAllowlisted("createWindow".to_string()));
+          #[cfg(create_window)]
+          {
+            let label = options.label.to_string();
+            webview_manager
+              .create_webview(label.to_string(), options.url.clone(), |_| {
+                Ok(crate::app::webview::WindowConfig(options).into())
+              })
+              .await?;
+            webview_manager
+              .emit_except(
+                label.to_string(),
+                "tauri://window-created",
+                Some(WindowCreatedEvent { label }),
+              )
+              .await?;
+          }
+        }
         Self::SetResizable { resizable } => current_webview.set_resizable(resizable)?,
         Self::SetTitle { title } => current_webview.set_title(&title)?,
         Self::Maximize => current_webview.maximize()?,

+ 22 - 24
tauri/src/plugin.rs

@@ -1,6 +1,4 @@
-use crate::{
-  api::config::PluginConfig, async_runtime::Mutex, ApplicationDispatcherExt, WebviewManager,
-};
+use crate::{api::config::PluginConfig, async_runtime::Mutex, ApplicationExt, WebviewManager};
 
 use futures::future::join_all;
 use serde_json::Value as JsonValue;
@@ -9,7 +7,7 @@ use std::sync::Arc;
 
 /// The plugin interface.
 #[async_trait::async_trait]
-pub trait Plugin<D: ApplicationDispatcherExt + 'static>: Send + Sync {
+pub trait Plugin<A: ApplicationExt + 'static>: Send + Sync {
   /// The plugin name. Used as key on the plugin config object.
   fn name(&self) -> &'static str;
 
@@ -30,17 +28,17 @@ pub trait Plugin<D: ApplicationDispatcherExt + 'static>: Send + Sync {
 
   /// Callback invoked when the webview is created.
   #[allow(unused_variables)]
-  async fn created(&mut self, webview_manager: WebviewManager<D>) {}
+  async fn created(&mut self, webview_manager: WebviewManager<A>) {}
 
   /// Callback invoked when the webview is ready.
   #[allow(unused_variables)]
-  async fn ready(&mut self, webview_manager: WebviewManager<D>) {}
+  async fn ready(&mut self, webview_manager: WebviewManager<A>) {}
 
   /// Add invoke_handler API extension commands.
   #[allow(unused_variables)]
   async fn extend_api(
     &mut self,
-    webview_manager: WebviewManager<D>,
+    webview_manager: WebviewManager<A>,
     payload: &JsonValue,
   ) -> crate::Result<JsonValue> {
     Err(crate::Error::UnknownApi(None))
@@ -48,19 +46,19 @@ pub trait Plugin<D: ApplicationDispatcherExt + 'static>: Send + Sync {
 }
 
 /// Plugin collection type.
-pub type PluginStore<D> = Arc<Mutex<Vec<Box<dyn Plugin<D> + Sync + Send>>>>;
+pub type PluginStore<A> = Arc<Mutex<Vec<Box<dyn Plugin<A> + Sync + Send>>>>;
 
 /// Registers a plugin.
-pub async fn register<D: ApplicationDispatcherExt + 'static>(
-  store: &PluginStore<D>,
-  plugin: impl Plugin<D> + Sync + Send + 'static,
+pub async fn register<A: ApplicationExt + 'static>(
+  store: &PluginStore<A>,
+  plugin: impl Plugin<A> + Sync + Send + 'static,
 ) {
   let mut plugins = store.lock().await;
   plugins.push(Box::new(plugin));
 }
 
-pub(crate) async fn initialize<D: ApplicationDispatcherExt + 'static>(
-  store: &PluginStore<D>,
+pub(crate) async fn initialize<A: ApplicationExt + 'static>(
+  store: &PluginStore<A>,
   plugins_config: PluginConfig,
 ) -> crate::Result<()> {
   let mut plugins = store.lock().await;
@@ -77,8 +75,8 @@ pub(crate) async fn initialize<D: ApplicationDispatcherExt + 'static>(
   Ok(())
 }
 
-pub(crate) async fn initialization_script<D: ApplicationDispatcherExt + 'static>(
-  store: &PluginStore<D>,
+pub(crate) async fn initialization_script<A: ApplicationExt + 'static>(
+  store: &PluginStore<A>,
 ) -> String {
   let mut plugins = store.lock().await;
   let mut futures = Vec::new();
@@ -98,9 +96,9 @@ pub(crate) async fn initialization_script<D: ApplicationDispatcherExt + 'static>
   initialization_script
 }
 
-pub(crate) async fn created<D: ApplicationDispatcherExt + 'static>(
-  store: &PluginStore<D>,
-  webview_manager: &crate::WebviewManager<D>,
+pub(crate) async fn created<A: ApplicationExt + 'static>(
+  store: &PluginStore<A>,
+  webview_manager: &crate::WebviewManager<A>,
 ) {
   let mut plugins = store.lock().await;
   let mut futures = Vec::new();
@@ -110,9 +108,9 @@ pub(crate) async fn created<D: ApplicationDispatcherExt + 'static>(
   join_all(futures).await;
 }
 
-pub(crate) async fn ready<D: ApplicationDispatcherExt + 'static>(
-  store: &PluginStore<D>,
-  webview_manager: &crate::WebviewManager<D>,
+pub(crate) async fn ready<A: ApplicationExt + 'static>(
+  store: &PluginStore<A>,
+  webview_manager: &crate::WebviewManager<A>,
 ) {
   let mut plugins = store.lock().await;
   let mut futures = Vec::new();
@@ -122,9 +120,9 @@ pub(crate) async fn ready<D: ApplicationDispatcherExt + 'static>(
   join_all(futures).await;
 }
 
-pub(crate) async fn extend_api<D: ApplicationDispatcherExt + 'static>(
-  store: &PluginStore<D>,
-  webview_manager: &crate::WebviewManager<D>,
+pub(crate) async fn extend_api<A: ApplicationExt + 'static>(
+  store: &PluginStore<A>,
+  webview_manager: &crate::WebviewManager<A>,
   arg: &JsonValue,
 ) -> crate::Result<Option<JsonValue>> {
   let mut plugins = store.lock().await;

Vissa filer visades inte eftersom för många filer har ändrats