diff --git a/.changes/event.md b/.changes/event.md new file mode 100644 index 000000000..4318942a1 --- /dev/null +++ b/.changes/event.md @@ -0,0 +1,5 @@ +--- +"tauri": minor +--- + +The `tauri::event` module has been moved to a Webview manager API. diff --git a/.changes/multiwindow.md b/.changes/multiwindow.md new file mode 100644 index 000000000..6177d2e83 --- /dev/null +++ b/.changes/multiwindow.md @@ -0,0 +1,5 @@ +--- +"tauri": minor +--- + +Added support to multiple windows. diff --git a/README.md b/README.md index 21875339c..3128a24e3 100644 --- a/README.md +++ b/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 diff --git a/cli/core/src/helpers/config.rs b/cli/core/src/helpers/config.rs index 42d8c60f9..9bb235e09 100644 --- a/cli/core/src/helpers/config.rs +++ b/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(), diff --git a/cli/tauri.js/src/types/config.schema.json b/cli/tauri.js/src/types/config.schema.json index 9572f4781..5610d83a9 100644 --- a/cli/tauri.js/src/types/config.schema.json +++ b/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": { diff --git a/cli/tauri.js/src/types/config.ts b/cli/tauri.js/src/types/config.ts index 2255ebaec..50a54108e 100644 --- a/cli/tauri.js/src/types/config.ts +++ b/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 } diff --git a/cli/tauri.js/src/types/config.validator.ts b/cli/tauri.js/src/types/config.validator.ts index 8250d9ea0..676f64d7a 100644 --- a/cli/tauri.js/src/types/config.validator.ts +++ b/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: { diff --git a/cli/tauri.js/test/jest/fixtures/app/src-tauri/src/main.rs b/cli/tauri.js/test/jest/fixtures/app/src-tauri/src/main.rs index 3f7bf429b..65567f408 100644 --- a/cli/tauri.js/test/jest/fixtures/app/src-tauri/src/main.rs +++ b/cli/tauri.js/test/jest/fixtures/app/src-tauri/src/main.rs @@ -7,19 +7,21 @@ struct Context; fn main() { tauri::AppBuilder::::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()), diff --git a/cli/tauri.js/test/jest/fixtures/app/src-tauri/tauri.conf.json b/cli/tauri.js/test/jest/fixtures/app/src-tauri/tauri.conf.json index 1595fc58c..298aa0e37 100644 --- a/cli/tauri.js/test/jest/fixtures/app/src-tauri/tauri.conf.json +++ b/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" + } + ] } } diff --git a/tauri-utils/src/config.rs b/tauri-utils/src/config.rs index 325f3c5d4..9732be21e 100644 --- a/tauri-utils/src/config.rs +++ b/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(deserializer: D) -> Result + 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(self, v: &str) -> Result + 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, + /// The vertical position of the window's top left corner + pub y: Option, /// 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, + /// The min window height. + pub min_height: Option, + /// The max window width. + pub max_width: Option, + /// The max window height. + pub max_height: Option, /// 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_width() -> i32 { - 800 +fn default_window_label() -> String { + "main".to_string() } -fn default_height() -> i32 { - 600 +fn default_width() -> f64 { + 800f64 +} + +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 { + 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, /// 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); } } diff --git a/tauri/Cargo.toml b/tauri/Cargo.toml index 996e3c012..f5fee70bc 100644 --- a/tauri/Cargo.toml +++ b/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" diff --git a/tauri/examples/api/public/index.html b/tauri/examples/api/public/index.html index 0f9945386..a3b0fa0f9 100644 --- a/tauri/examples/api/public/index.html +++ b/tauri/examples/api/public/index.html @@ -9,7 +9,6 @@ - diff --git a/tauri/examples/api/src-tauri/Cargo.lock b/tauri/examples/api/src-tauri/Cargo.lock index 4170460e7..f43b421c8 100644 --- a/tauri/examples/api/src-tauri/Cargo.lock +++ b/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", diff --git a/tauri/examples/api/src-tauri/src/main.rs b/tauri/examples/api/src-tauri/src/main.rs index 2c4aa046a..3ac4a8914 100644 --- a/tauri/examples/api/src-tauri/src/main.rs +++ b/tauri/examples/api/src-tauri/src/main.rs @@ -17,19 +17,21 @@ struct Context; fn main() { tauri::AppBuilder::::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 diff --git a/tauri/examples/api/src-tauri/tauri.conf.json b/tauri/examples/api/src-tauri/tauri.conf.json index f0e9add29..41d39d9d2 100644 --- a/tauri/examples/api/src-tauri/tauri.conf.json +++ b/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'" } diff --git a/tauri/examples/api/src/components/Window.svelte b/tauri/examples/api/src/components/Window.svelte index 8b4923fd6..d7550b801 100644 --- a/tauri/examples/api/src/components/Window.svelte +++ b/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); }
- +
diff --git a/tauri/examples/communication/dist/communication.js b/tauri/examples/communication/dist/communication.js index 71077fabf..8d299fb81 100644 --- a/tauri/examples/communication/dist/communication.js +++ b/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", diff --git a/tauri/examples/communication/src-tauri/src/main.rs b/tauri/examples/communication/src-tauri/src/main.rs index 16d8a6d6e..834e65fc2 100644 --- a/tauri/examples/communication/src-tauri/src/main.rs +++ b/tauri/examples/communication/src-tauri/src/main.rs @@ -18,19 +18,21 @@ struct Context; fn main() { tauri::AppBuilder::::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 diff --git a/tauri/examples/communication/src-tauri/tauri.conf.json b/tauri/examples/communication/src-tauri/tauri.conf.json index 083cd7e31..c1848b501 100644 --- a/tauri/examples/communication/src-tauri/tauri.conf.json +++ b/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'" } diff --git a/tauri/examples/multiwindow/dist/__tauri.js b/tauri/examples/multiwindow/dist/__tauri.js new file mode 100644 index 000000000..5ce41abbf --- /dev/null +++ b/tauri/examples/multiwindow/dist/__tauri.js @@ -0,0 +1,287 @@ +function ownKeys(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,n)}return r}function _objectSpread(e){for(var t=1;t=0;--a){var i=this.tryEntries[a],u=i.completion;if("root"===i.tryLoc)return o("end");if(i.tryLoc<=this.prev){var c=n.call(i,"catchLoc"),s=n.call(i,"finallyLoc");if(c&&s){if(this.prev=0;--r){var o=this.tryEntries[r];if(o.tryLoc<=this.prev&&n.call(o,"finallyLoc")&&this.prev=0;--t){var r=this.tryEntries[t];if(r.finallyLoc===e)return this.complete(r.completion,r.afterLoc),j(r),m}},catch:function(e){for(var t=this.tryEntries.length-1;t>=0;--t){var r=this.tryEntries[t];if(r.tryLoc===e){var n=r.completion;if("throw"===n.type){var o=n.arg;j(r)}return o}}throw new Error("illegal catch attempt")},delegateYield:function(e,r,n){return this.delegate={iterator:O(e),resultName:r,nextLoc:n},"next"===this.method&&(this.arg=t),m}},e}("object"===("undefined"==typeof module?"undefined":_typeof(module))?module.exports:{});try{regeneratorRuntime=t}catch(e){Function("r","regeneratorRuntime = r")(t)}function r(e){for(var t=void 0,r=e[0],n=1;n1&&void 0!==arguments[1]&&arguments[1],n=o();return Object.defineProperty(window,n,{value:function(o){return t&&Reflect.deleteProperty(window,n),r([e,"optionalCall",function(e){return e(o)}])},writable:!1,configurable:!0}),n}function u(e){return c.apply(this,arguments)}function c(){return(c=_asyncToGenerator(regeneratorRuntime.mark((function e(t){return regeneratorRuntime.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,new Promise((function(e,r){var n=i((function(t){e(t),Reflect.deleteProperty(window,o)}),!0),o=i((function(e){r(e),Reflect.deleteProperty(window,n)}),!0);a(_objectSpread({callback:n,error:o},t))}));case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}}),e)})))).apply(this,arguments)}var s=Object.freeze({__proto__:null,invoke:a,transformCallback:i,promisified:u});function p(){return(p=_asyncToGenerator(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,u({cmd:"cliMatches"});case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}}),e)})))).apply(this,arguments)}var f=Object.freeze({__proto__:null,getMatches:function(){return p.apply(this,arguments)}});function l(){return(l=_asyncToGenerator(regeneratorRuntime.mark((function e(){var t,r=arguments;return regeneratorRuntime.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return"object"===_typeof(t=r.length>0&&void 0!==r[0]?r[0]:{})&&Object.freeze(t),e.next=4,u({cmd:"openDialog",options:t});case 4:return e.abrupt("return",e.sent);case 5:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function h(){return(h=_asyncToGenerator(regeneratorRuntime.mark((function e(){var t,r=arguments;return regeneratorRuntime.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return"object"===_typeof(t=r.length>0&&void 0!==r[0]?r[0]:{})&&Object.freeze(t),e.next=4,u({cmd:"saveDialog",options:t});case 4:return e.abrupt("return",e.sent);case 5:case"end":return e.stop()}}),e)})))).apply(this,arguments)}var y=Object.freeze({__proto__:null,open:function(){return l.apply(this,arguments)},save:function(){return h.apply(this,arguments)}});var m,d=Object.freeze({__proto__:null,listen:function(e,t){var r=arguments.length>2&&void 0!==arguments[2]&&arguments[2];a({cmd:"listen",event:e,handler:i(t,r),once:r})},emit:function(e,t){a({cmd:"emit",event:e,payload:t})}});function v(){return(v=_asyncToGenerator(regeneratorRuntime.mark((function e(t){var r,n=arguments;return regeneratorRuntime.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return r=n.length>1&&void 0!==n[1]?n[1]:{},e.next=3,u({cmd:"readTextFile",path:t,options:r});case 3:return e.abrupt("return",e.sent);case 4:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function g(){return(g=_asyncToGenerator(regeneratorRuntime.mark((function e(t){var r,n=arguments;return regeneratorRuntime.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return r=n.length>1&&void 0!==n[1]?n[1]:{},e.next=3,u({cmd:"readBinaryFile",path:t,options:r});case 3:return e.abrupt("return",e.sent);case 4:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function w(){return(w=_asyncToGenerator(regeneratorRuntime.mark((function e(t){var r,n=arguments;return regeneratorRuntime.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return"object"===_typeof(r=n.length>1&&void 0!==n[1]?n[1]:{})&&Object.freeze(r),"object"===_typeof(t)&&Object.freeze(t),e.next=5,u({cmd:"writeFile",path:t.path,contents:t.contents,options:r});case 5:return e.abrupt("return",e.sent);case 6:case"end":return e.stop()}}),e)})))).apply(this,arguments)}!function(e){e[e.Audio=1]="Audio";e[e.Cache=2]="Cache";e[e.Config=3]="Config";e[e.Data=4]="Data";e[e.LocalData=5]="LocalData";e[e.Desktop=6]="Desktop";e[e.Document=7]="Document";e[e.Download=8]="Download";e[e.Executable=9]="Executable";e[e.Font=10]="Font";e[e.Home=11]="Home";e[e.Picture=12]="Picture";e[e.Public=13]="Public";e[e.Runtime=14]="Runtime";e[e.Template=15]="Template";e[e.Video=16]="Video";e[e.Resource=17]="Resource";e[e.App=18]="App"}(m||(m={}));var b=65536;function x(e){var t=function(e){if(e.length1&&void 0!==n[1]?n[1]:{})&&Object.freeze(r),"object"===_typeof(t)&&Object.freeze(t),e.next=5,u({cmd:"writeBinaryFile",path:t.path,contents:x(t.contents),options:r});case 5:return e.abrupt("return",e.sent);case 6:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function R(){return(R=_asyncToGenerator(regeneratorRuntime.mark((function e(t){var r,n=arguments;return regeneratorRuntime.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return r=n.length>1&&void 0!==n[1]?n[1]:{},e.next=3,u({cmd:"readDir",path:t,options:r});case 3:return e.abrupt("return",e.sent);case 4:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function T(){return(T=_asyncToGenerator(regeneratorRuntime.mark((function e(t){var r,n=arguments;return regeneratorRuntime.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return r=n.length>1&&void 0!==n[1]?n[1]:{},e.next=3,u({cmd:"createDir",path:t,options:r});case 3:return e.abrupt("return",e.sent);case 4:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function P(){return(P=_asyncToGenerator(regeneratorRuntime.mark((function e(t){var r,n=arguments;return regeneratorRuntime.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return r=n.length>1&&void 0!==n[1]?n[1]:{},e.next=3,u({cmd:"removeDir",path:t,options:r});case 3:return e.abrupt("return",e.sent);case 4:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function k(){return(k=_asyncToGenerator(regeneratorRuntime.mark((function e(t,r){var n,o=arguments;return regeneratorRuntime.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return n=o.length>2&&void 0!==o[2]?o[2]:{},e.next=3,u({cmd:"copyFile",source:t,destination:r,options:n});case 3:return e.abrupt("return",e.sent);case 4:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function j(){return(j=_asyncToGenerator(regeneratorRuntime.mark((function e(t){var r,n=arguments;return regeneratorRuntime.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return r=n.length>1&&void 0!==n[1]?n[1]:{},e.next=3,u({cmd:"removeFile",path:t,options:r});case 3:return e.abrupt("return",e.sent);case 4:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function G(){return(G=_asyncToGenerator(regeneratorRuntime.mark((function e(t,r){var n,o=arguments;return regeneratorRuntime.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return n=o.length>2&&void 0!==o[2]?o[2]:{},e.next=3,u({cmd:"renameFile",oldPath:t,newPath:r,options:n});case 3:return e.abrupt("return",e.sent);case 4:case"end":return e.stop()}}),e)})))).apply(this,arguments)}var O=Object.freeze({__proto__:null,get BaseDirectory(){return m},get Dir(){return m},readTextFile:function(e){return v.apply(this,arguments)},readBinaryFile:function(e){return g.apply(this,arguments)},writeFile:function(e){return w.apply(this,arguments)},writeBinaryFile:function(e){return _.apply(this,arguments)},readDir:function(e){return R.apply(this,arguments)},createDir:function(e){return T.apply(this,arguments)},removeDir:function(e){return P.apply(this,arguments)},copyFile:function(e,t){return k.apply(this,arguments)},removeFile:function(e){return j.apply(this,arguments)},renameFile:function(e,t){return G.apply(this,arguments)}});function D(){return(D=_asyncToGenerator(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,u({cmd:"resolvePath",path:"",directory:m.App});case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function E(){return(E=_asyncToGenerator(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,u({cmd:"resolvePath",path:"",directory:m.Audio});case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function L(){return(L=_asyncToGenerator(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,u({cmd:"resolvePath",path:"",directory:m.Cache});case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function S(){return(S=_asyncToGenerator(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,u({cmd:"resolvePath",path:"",directory:m.Config});case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function F(){return(F=_asyncToGenerator(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,u({cmd:"resolvePath",path:"",directory:m.Data});case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function A(){return(A=_asyncToGenerator(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,u({cmd:"resolvePath",path:"",directory:m.Desktop});case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function N(){return(N=_asyncToGenerator(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,u({cmd:"resolvePath",path:"",directory:m.Document});case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function z(){return(z=_asyncToGenerator(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,u({cmd:"resolvePath",path:"",directory:m.Download});case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function C(){return(C=_asyncToGenerator(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,u({cmd:"resolvePath",path:"",directory:m.Executable});case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function B(){return(B=_asyncToGenerator(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,u({cmd:"resolvePath",path:"",directory:m.Font});case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function I(){return(I=_asyncToGenerator(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,u({cmd:"resolvePath",path:"",directory:m.Home});case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function H(){return(H=_asyncToGenerator(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,u({cmd:"resolvePath",path:"",directory:m.LocalData});case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function M(){return(M=_asyncToGenerator(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,u({cmd:"resolvePath",path:"",directory:m.Picture});case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function q(){return(q=_asyncToGenerator(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,u({cmd:"resolvePath",path:"",directory:m.Public});case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function K(){return(K=_asyncToGenerator(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,u({cmd:"resolvePath",path:"",directory:m.Resource});case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function U(){return(U=_asyncToGenerator(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,u({cmd:"resolvePath",path:"",directory:m.Runtime});case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function V(){return(V=_asyncToGenerator(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,u({cmd:"resolvePath",path:"",directory:m.Template});case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function J(){return(J=_asyncToGenerator(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,u({cmd:"resolvePath",path:"",directory:m.Video});case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function Y(){return(Y=_asyncToGenerator(regeneratorRuntime.mark((function e(t,r){return regeneratorRuntime.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,u({cmd:"resolvePath",path:t,directory:r});case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}}),e)})))).apply(this,arguments)}var Q,W,X=Object.freeze({__proto__:null,appDir:function(){return D.apply(this,arguments)},audioDir:function(){return E.apply(this,arguments)},cacheDir:function(){return L.apply(this,arguments)},configDir:function(){return S.apply(this,arguments)},dataDir:function(){return F.apply(this,arguments)},desktopDir:function(){return A.apply(this,arguments)},documentDir:function(){return N.apply(this,arguments)},downloadDir:function(){return z.apply(this,arguments)},executableDir:function(){return C.apply(this,arguments)},fontDir:function(){return B.apply(this,arguments)},homeDir:function(){return I.apply(this,arguments)},localDataDir:function(){return H.apply(this,arguments)},pictureDir:function(){return M.apply(this,arguments)},publicDir:function(){return q.apply(this,arguments)},resourceDir:function(){return K.apply(this,arguments)},runtimeDir:function(){return U.apply(this,arguments)},templateDir:function(){return V.apply(this,arguments)},videoDir:function(){return J.apply(this,arguments)},resolvePath:function(e,t){return Y.apply(this,arguments)}});function Z(e){return $.apply(this,arguments)}function $(){return($=_asyncToGenerator(regeneratorRuntime.mark((function e(t){return regeneratorRuntime.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,u({cmd:"httpRequest",options:t});case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function ee(){return(ee=_asyncToGenerator(regeneratorRuntime.mark((function e(t,r){return regeneratorRuntime.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,Z(_objectSpread({method:"GET",url:t},r));case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function te(){return(te=_asyncToGenerator(regeneratorRuntime.mark((function e(t,r,n){return regeneratorRuntime.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,Z(_objectSpread({method:"POST",url:t,body:r},n));case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function re(){return(re=_asyncToGenerator(regeneratorRuntime.mark((function e(t,r,n){return regeneratorRuntime.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,Z(_objectSpread({method:"PUT",url:t,body:r},n));case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function ne(){return(ne=_asyncToGenerator(regeneratorRuntime.mark((function e(t,r){return regeneratorRuntime.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,Z(_objectSpread({method:"PATCH",url:t},r));case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function oe(){return(oe=_asyncToGenerator(regeneratorRuntime.mark((function e(t,r){return regeneratorRuntime.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,Z(_objectSpread({method:"DELETE",url:t},r));case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}}),e)})))).apply(this,arguments)}!function(e){e[e.JSON=1]="JSON";e[e.Text=2]="Text";e[e.Binary=3]="Binary"}(Q||(Q={})),function(e){e[e.Form=1]="Form";e[e.File=2]="File";e[e.Auto=3]="Auto"}(W||(W={}));var ae={request:Z,get:function(e,t){return ee.apply(this,arguments)},post:function(e,t,r){return te.apply(this,arguments)},put:function(e,t,r){return re.apply(this,arguments)},patch:function(e,t){return ne.apply(this,arguments)},delete:function(e,t){return oe.apply(this,arguments)},ResponseType:Q,BodyType:W};function ie(){return(ie=_asyncToGenerator(regeneratorRuntime.mark((function e(t,r){return regeneratorRuntime.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return"object"===_typeof(r)&&Object.freeze(r),e.next=3,u({cmd:"execute",command:t,args:"string"==typeof r?[r]:r});case 3:return e.abrupt("return",e.sent);case 4:case"end":return e.stop()}}),e)})))).apply(this,arguments)}var ue=Object.freeze({__proto__:null,execute:function(e,t){return ie.apply(this,arguments)}});var ce=Object.freeze({__proto__:null,setTitle:function(e){a({cmd:"setTitle",title:e})},open:function(e){a({cmd:"open",uri:e})}});function se(){return(se=_asyncToGenerator(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:if("default"===window.Notification.permission){e.next=4;break}return e.next=3,Promise.resolve("granted"===window.Notification.permission);case 3:return e.abrupt("return",e.sent);case 4:return e.next=6,u({cmd:"isNotificationPermissionGranted"});case 6:return e.abrupt("return",e.sent);case 7:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function pe(){return(pe=_asyncToGenerator(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,window.Notification.requestPermission();case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}}),e)})))).apply(this,arguments)}var fe=Object.freeze({__proto__:null,sendNotification:function(e){"string"==typeof e?new window.Notification(e):new window.Notification(e.title,e)},requestPermission:function(){return pe.apply(this,arguments)},isPermissionGranted:function(){return se.apply(this,arguments)}});e.cli=f,e.dialog=y,e.event=d,e.fs=O,e.http=ae,e.notification=fe,e.path=X,e.process=ue,e.tauri=s,e.window=ce,Object.defineProperty(e,"__esModule",{value:!0})})); + + +// polyfills +if (!String.prototype.startsWith) { + String.prototype.startsWith = function (searchString, position) { + position = position || 0 + return this.substr(position, searchString.length) === searchString + } +} + +;(function () { + function s4() { + return Math.floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1) + } + + var uid = function () { + return ( + s4() + + s4() + + '-' + + s4() + + '-' + + s4() + + '-' + + s4() + + '-' + + s4() + + s4() + + s4() + ) + } + + function ownKeys(object, enumerableOnly) { + var keys = Object.keys(object) + if (Object.getOwnPropertySymbols) { + var symbols = Object.getOwnPropertySymbols(object) + if (enumerableOnly) + symbols = symbols.filter(function (sym) { + return Object.getOwnPropertyDescriptor(object, sym).enumerable + }) + keys.push.apply(keys, symbols) + } + return keys + } + + function _objectSpread(target) { + for (var i = 1; i < arguments.length; i++) { + var source = arguments[i] != null ? arguments[i] : {} + if (i % 2) { + ownKeys(source, true).forEach(function (key) { + _defineProperty(target, key, source[key]) + }) + } else if (Object.getOwnPropertyDescriptors) { + Object.defineProperties( + target, + Object.getOwnPropertyDescriptors(source) + ) + } else { + ownKeys(source).forEach(function (key) { + Object.defineProperty( + target, + key, + Object.getOwnPropertyDescriptor(source, key) + ) + }) + } + } + return target + } + + function _defineProperty(obj, key, value) { + if (key in obj) { + Object.defineProperty(obj, key, { + value: value, + enumerable: true, + configurable: true, + writable: true + }) + } else { + obj[key] = value + } + return obj + } + + if (!window.__TAURI__) { + window.__TAURI__ = {} + } + + window.__TAURI__.transformCallback = function transformCallback( + callback, + once + ) { + var identifier = uid() + + window[identifier] = function (result) { + if (once) { + delete window[identifier] + } + + return callback && callback(result) + } + + return identifier + } + + window.__TAURI__.promisified = function promisified(args) { + var _this = this + + return new Promise(function (resolve, reject) { + var callback = _this.transformCallback(function (r) { + resolve(r) + delete window[error] + }, true) + var error = _this.transformCallback(function (e) { + reject(e) + delete window[callback] + }, true) + + if (window.__TAURI_INVOKE_HANDLER__) { + window.__TAURI_INVOKE_HANDLER__( + JSON.stringify(_objectSpread( + { + callback: callback, + error: error + }, + args + )) + ) + } else { + window.addEventListener('DOMContentLoaded', function () { + window.__TAURI_INVOKE_HANDLER__( + JSON.stringify(_objectSpread( + { + callback: callback, + error: error + }, + args + )) + ) + }) + } + }) + } + + // open links with the Tauri API + function __openLinks() { + document.querySelector('body').addEventListener( + 'click', + function (e) { + var target = e.target + while (target != null) { + if ( + target.matches ? target.matches('a') : target.msMatchesSelector('a') + ) { + if ( + target.href && + target.href.startsWith('http') && + target.target === '_blank' + ) { + window.__TAURI_INVOKE_HANDLER__(JSON.stringify({ + cmd: 'open', + uri: target.href + })) + e.preventDefault() + } + break + } + target = target.parentElement + } + }, + true + ) + } + + if ( + document.readyState === 'complete' || + document.readyState === 'interactive' + ) { + __openLinks() + } else { + window.addEventListener( + 'DOMContentLoaded', + function () { + __openLinks() + }, + true + ) + } + + let permissionSettable = false + let permissionValue = 'default' + + function isPermissionGranted() { + if (window.Notification.permission !== 'default') { + return Promise.resolve(window.Notification.permission === 'granted') + } + return window.__TAURI__.promisified({ + cmd: 'isNotificationPermissionGranted' + }) + } + + function setNotificationPermission(value) { + permissionSettable = true + window.Notification.permission = value + permissionSettable = false + } + + function requestPermission() { + return window.__TAURI__ + .promisified({ + cmd: 'requestNotificationPermission' + }) + .then(function (permission) { + setNotificationPermission(permission) + return permission + }) + } + + function sendNotification(options) { + if (typeof options === 'object') { + Object.freeze(options) + } + + isPermissionGranted().then(function (permission) { + if (permission) { + return window.__TAURI__.promisified({ + cmd: 'notification', + options: + typeof options === 'string' + ? { + title: options + } + : options + }) + } + }) + } + + window.Notification = function (title, options) { + var opts = options || {} + sendNotification( + Object.assign(opts, { + title: title + }) + ) + } + + window.Notification.requestPermission = requestPermission + + Object.defineProperty(window.Notification, 'permission', { + enumerable: true, + get: function () { + return permissionValue + }, + set: function (v) { + if (!permissionSettable) { + throw new Error('Readonly property') + } + permissionValue = v + } + }) + + isPermissionGranted().then(function (response) { + if (response === null) { + setNotificationPermission('default') + } else { + setNotificationPermission(response ? 'granted' : 'denied') + } + }) + + window.alert = function (message) { + window.__TAURI_INVOKE_HANDLER__(JSON.stringify({ + cmd: 'messageDialog', + message: message + })) + } + + window.confirm = function (message) { + return window.__TAURI__.promisified({ + cmd: 'askDialog', + message: message + }) + } +})() \ No newline at end of file diff --git a/tauri/examples/multiwindow/dist/index.html b/tauri/examples/multiwindow/dist/index.html new file mode 100644 index 000000000..02a48f815 --- /dev/null +++ b/tauri/examples/multiwindow/dist/index.html @@ -0,0 +1,36 @@ + + + + +
+
+
+ + + + + \ No newline at end of file diff --git a/tauri/examples/multiwindow/package.json b/tauri/examples/multiwindow/package.json new file mode 100644 index 000000000..fd692a576 --- /dev/null +++ b/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 +} diff --git a/tauri/examples/multiwindow/src-tauri/.gitignore b/tauri/examples/multiwindow/src-tauri/.gitignore new file mode 100644 index 000000000..270a92d27 --- /dev/null +++ b/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 diff --git a/tauri/examples/multiwindow/src-tauri/Cargo.toml b/tauri/examples/multiwindow/src-tauri/Cargo.toml new file mode 100644 index 000000000..39797f829 --- /dev/null +++ b/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" diff --git a/tauri/examples/multiwindow/src-tauri/icons/128x128.png b/tauri/examples/multiwindow/src-tauri/icons/128x128.png new file mode 100644 index 000000000..f8d9962cc Binary files /dev/null and b/tauri/examples/multiwindow/src-tauri/icons/128x128.png differ diff --git a/tauri/examples/multiwindow/src-tauri/icons/128x128@2x.png b/tauri/examples/multiwindow/src-tauri/icons/128x128@2x.png new file mode 100644 index 000000000..44d26d04b Binary files /dev/null and b/tauri/examples/multiwindow/src-tauri/icons/128x128@2x.png differ diff --git a/tauri/examples/multiwindow/src-tauri/icons/32x32.png b/tauri/examples/multiwindow/src-tauri/icons/32x32.png new file mode 100644 index 000000000..b6bf7d610 Binary files /dev/null and b/tauri/examples/multiwindow/src-tauri/icons/32x32.png differ diff --git a/tauri/examples/multiwindow/src-tauri/icons/Square107x107Logo.png b/tauri/examples/multiwindow/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 000000000..6146c3354 Binary files /dev/null and b/tauri/examples/multiwindow/src-tauri/icons/Square107x107Logo.png differ diff --git a/tauri/examples/multiwindow/src-tauri/icons/Square142x142Logo.png b/tauri/examples/multiwindow/src-tauri/icons/Square142x142Logo.png new file mode 100644 index 000000000..e24cb2a75 Binary files /dev/null and b/tauri/examples/multiwindow/src-tauri/icons/Square142x142Logo.png differ diff --git a/tauri/examples/multiwindow/src-tauri/icons/Square150x150Logo.png b/tauri/examples/multiwindow/src-tauri/icons/Square150x150Logo.png new file mode 100644 index 000000000..a389fb4a6 Binary files /dev/null and b/tauri/examples/multiwindow/src-tauri/icons/Square150x150Logo.png differ diff --git a/tauri/examples/multiwindow/src-tauri/icons/Square284x284Logo.png b/tauri/examples/multiwindow/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 000000000..c43c42ffd Binary files /dev/null and b/tauri/examples/multiwindow/src-tauri/icons/Square284x284Logo.png differ diff --git a/tauri/examples/multiwindow/src-tauri/icons/Square30x30Logo.png b/tauri/examples/multiwindow/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 000000000..493f155ac Binary files /dev/null and b/tauri/examples/multiwindow/src-tauri/icons/Square30x30Logo.png differ diff --git a/tauri/examples/multiwindow/src-tauri/icons/Square310x310Logo.png b/tauri/examples/multiwindow/src-tauri/icons/Square310x310Logo.png new file mode 100644 index 000000000..6380a98dd Binary files /dev/null and b/tauri/examples/multiwindow/src-tauri/icons/Square310x310Logo.png differ diff --git a/tauri/examples/multiwindow/src-tauri/icons/Square44x44Logo.png b/tauri/examples/multiwindow/src-tauri/icons/Square44x44Logo.png new file mode 100644 index 000000000..61e4eff35 Binary files /dev/null and b/tauri/examples/multiwindow/src-tauri/icons/Square44x44Logo.png differ diff --git a/tauri/examples/multiwindow/src-tauri/icons/Square71x71Logo.png b/tauri/examples/multiwindow/src-tauri/icons/Square71x71Logo.png new file mode 100644 index 000000000..668945bab Binary files /dev/null and b/tauri/examples/multiwindow/src-tauri/icons/Square71x71Logo.png differ diff --git a/tauri/examples/multiwindow/src-tauri/icons/Square89x89Logo.png b/tauri/examples/multiwindow/src-tauri/icons/Square89x89Logo.png new file mode 100644 index 000000000..1e14926fd Binary files /dev/null and b/tauri/examples/multiwindow/src-tauri/icons/Square89x89Logo.png differ diff --git a/tauri/examples/multiwindow/src-tauri/icons/StoreLogo.png b/tauri/examples/multiwindow/src-tauri/icons/StoreLogo.png new file mode 100644 index 000000000..1f20ed7b0 Binary files /dev/null and b/tauri/examples/multiwindow/src-tauri/icons/StoreLogo.png differ diff --git a/tauri/examples/multiwindow/src-tauri/icons/icon.icns b/tauri/examples/multiwindow/src-tauri/icons/icon.icns new file mode 100644 index 000000000..43c73bda9 Binary files /dev/null and b/tauri/examples/multiwindow/src-tauri/icons/icon.icns differ diff --git a/tauri/examples/multiwindow/src-tauri/icons/icon.ico b/tauri/examples/multiwindow/src-tauri/icons/icon.ico new file mode 100644 index 000000000..db7fd9820 Binary files /dev/null and b/tauri/examples/multiwindow/src-tauri/icons/icon.ico differ diff --git a/tauri/examples/multiwindow/src-tauri/icons/icon.png b/tauri/examples/multiwindow/src-tauri/icons/icon.png new file mode 100644 index 000000000..e65ea7e86 Binary files /dev/null and b/tauri/examples/multiwindow/src-tauri/icons/icon.png differ diff --git a/tauri/examples/multiwindow/src-tauri/rustfmt.toml b/tauri/examples/multiwindow/src-tauri/rustfmt.toml new file mode 100644 index 000000000..136f5f330 --- /dev/null +++ b/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" diff --git a/tauri/examples/multiwindow/src-tauri/src/build.rs b/tauri/examples/multiwindow/src-tauri/src/build.rs new file mode 100644 index 000000000..83fb8f4d6 --- /dev/null +++ b/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() {} diff --git a/tauri/examples/multiwindow/src-tauri/src/main.rs b/tauri/examples/multiwindow/src-tauri/src/main.rs new file mode 100644 index 000000000..ee00e1cb5 --- /dev/null +++ b/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::::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(); +} diff --git a/tauri/examples/multiwindow/src-tauri/tauri.conf.json b/tauri/examples/multiwindow/src-tauri/tauri.conf.json new file mode 100644 index 000000000..846515968 --- /dev/null +++ b/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'" + } + } +} \ No newline at end of file diff --git a/tauri/examples/multiwindow/yarn.lock b/tauri/examples/multiwindow/yarn.lock new file mode 100644 index 000000000..fb57ccd13 --- /dev/null +++ b/tauri/examples/multiwindow/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + diff --git a/tauri/src/app.rs b/tauri/src/app.rs index bea2a934c..210f49c8a 100644 --- a/tauri/src/app.rs +++ b/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 = dyn Fn(W, String) -> BoxFuture<'static, Result<(), String>> + Send + Sync; -type Setup = dyn Fn(W, String) -> BoxFuture<'static, ()> + Send + Sync; +pub use webview_manager::{WebviewDispatcher, WebviewManager}; + +type InvokeHandler = + dyn Fn(WebviewManager, String) -> BoxFuture<'static, Result<(), String>> + Send + Sync; +type Setup = dyn Fn(WebviewManager) -> BoxFuture<'static, ()> + Send + Sync; /// `App` runtime information. pub struct Context { @@ -31,8 +36,6 @@ pub struct App { invoke_handler: Option>>, /// The setup callback, invoked when the webview is ready. setup: Option>>, - /// The HTML of the splashscreen to render. - splashscreen_html: Option, /// The context the App was created with pub(crate) context: Context, } @@ -48,7 +51,7 @@ impl App
{ /// 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, arg: &str, ) -> Result { if let Some(ref invoke_handler) = self.invoke_handler { @@ -60,17 +63,12 @@ impl App { } /// 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) { 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 { invoke_handler: Option>>, /// The setup callback, invoked when the webview is ready. setup: Option>>, - /// The HTML of the splashscreen to render. - splashscreen_html: Option, /// The configuration used config: PhantomData, } @@ -92,7 +88,6 @@ impl AppBuilder { Self { invoke_handler: None, setup: None, - splashscreen_html: None, config: Default::default(), } } @@ -100,13 +95,13 @@ impl AppBuilder { /// Defines the JS message handler callback. pub fn invoke_handler< T: futures::Future> + Send + Sync + 'static, - F: Fn(A::Dispatcher, String) -> T + Send + Sync + 'static, + F: Fn(WebviewManager, 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 AppBuilder { /// Defines the setup callback. pub fn setup< T: futures::Future + Send + Sync + 'static, - F: Fn(A::Dispatcher, String) -> T + Send + Sync + 'static, + F: Fn(WebviewManager) -> 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 AppBuilder { Ok(App { invoke_handler: self.invoke_handler, setup: self.setup, - splashscreen_html: self.splashscreen_html, context: Context::new::()?, }) } diff --git a/tauri/src/event.rs b/tauri/src/app/event.rs similarity index 86% rename from tauri/src/event.rs rename to tauri/src/app/event.rs index 4abdbd8f7..c097f1f40 100644 --- a/tauri/src/event.rs +++ b/tauri/src/app/event.rs @@ -16,7 +16,7 @@ struct EventHandler { on_event: Box) + Send>, } -type Listeners = Arc>>; +type Listeners = Arc>>>; 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) + Send + 'static>(id: impl Into, handler: F) { +pub fn listen) + Send + 'static>(id: impl AsRef, 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( - dispatcher: &mut D, - event: impl AsRef + Send + 'static, + webview_dispatcher: &crate::WebviewDispatcher, + event: impl AsRef, payload: Option, ) -> crate::Result<()> { let salt = crate::salt::generate(); @@ -72,7 +74,7 @@ pub fn emit( 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) { .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 diff --git a/tauri/src/app/runner.rs b/tauri/src/app/runner.rs index b637bd7a0..b89cb2d12 100644 --- a/tauri/src/app/runner.rs +++ b/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(application: App) -> 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( application: App, content: Content, - splashscreen_content: Option>, -) -> crate::Result<(A, A::Dispatcher)> { - let config = &application.context.config; +) -> crate::Result { // 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( let mut webview_application = A::new()?; - let main_window = - webview_application.create_window(A::WindowBuilder::new().resizable(resizable).title(title))?; + let mut window_refs = Vec::new(); + let mut dispatchers = HashMap::new(); - let dispatcher = webview_application.dispatcher(&main_window); + 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)); + } - let tauri_invoke_handler = crate::Callback:: { - 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(); + for (window_config, window) in window_refs { + let webview_manager = WebviewManager::new(dispatchers.clone(), window_config.label.to_string()); - 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 application = application.clone(); + let content_url = content_url.to_string(); + + let webview_manager_ = webview_manager.clone(); + let tauri_invoke_handler = crate::Callback:: { + 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(crate::Error::UnknownApi) = endpoint_handle { + endpoint_handle = + crate::plugin::extend_api(A::plugin_store(), &webview_manager, &arg) + .await + .map(|_| ()); + } + 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)); + } + } } } - 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); - } - } - 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)); - } - } - } - }); - 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::>()).unwrap(), + current_window_label = window_config.label, + )), + window, + vec![tauri_invoke_handler], + )?; - Ok((webview_application, dispatcher)) + crate::async_runtime::spawn(async move { + crate::plugin::created(A::plugin_store(), &webview_manager).await + }); + } + + Ok(webview_application) } // Formats an invoke handler error message to print to console.error diff --git a/tauri/src/app/webview_manager.rs b/tauri/src/app/webview_manager.rs new file mode 100644 index 000000000..bdc043d9a --- /dev/null +++ b/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); + +impl WebviewDispatcher { + 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) + Send + 'static>( + &self, + event: impl AsRef, + handler: F, + ) { + super::event::listen(event, handler) + } + + /// Emits an event. + pub fn emit( + &self, + event: impl AsRef, + payload: Option, + ) -> crate::Result<()> { + super::event::emit(&self, event, payload) + } + + pub(crate) fn on_event(&self, event: String, data: Option) { + 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 { + dispatchers: HashMap>, + current_webview_window_label: String, +} + +impl WebviewManager { + pub(crate) fn new(dispatchers: HashMap>, 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> { + 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> { + self + .dispatchers + .get(window_label) + .ok_or(crate::Error::WebviewNotFound) + } +} diff --git a/tauri/src/endpoints.rs b/tauri/src/endpoints.rs index 71dbdc719..dd9e2cf10 100644 --- a/tauri/src/endpoints.rs +++ b/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( - dispatcher: &mut D, + webview_manager: &crate::WebviewManager, arg: &str, context: &Context, ) -> crate::Result<()> { @@ -34,9 +34,9 @@ pub(crate) async fn handle( 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( 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( 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( 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( 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( 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( 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( 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( 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( 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( 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( 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( #[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( 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( 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( .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( } => { 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( 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( 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( } #[allow(dead_code)] -fn api_error(dispatcher: &mut D, error_fn: String, message: &str) { +fn api_error( + webview_manager: &crate::WebviewManager, + 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( - dispatcher: &mut D, + webview_manager: &crate::WebviewManager, 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( } #[allow(dead_code)] -fn throw_allowlist_error(dispatcher: &mut D, allowlist_key: &str) { +fn throw_allowlist_error( + webview_manager: &crate::WebviewManager, + 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)] diff --git a/tauri/src/endpoints/dialog.rs b/tauri/src/endpoints/dialog.rs index edeb25ac7..95febe6be 100644 --- a/tauri/src/endpoints/dialog.rs +++ b/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( - dispatcher: &mut D, + webview_manager: &crate::WebviewManager, 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( /// Shows a save dialog. #[cfg(save_dialog)] pub fn save( - dispatcher: &mut D, + webview_manager: &crate::WebviewManager, 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( - dispatcher: &mut D, + webview_manager: &crate::WebviewManager, 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), diff --git a/tauri/src/endpoints/event.rs b/tauri/src/endpoints/event.rs index 1661bcfb0..7506123cf 100644 --- a/tauri/src/endpoints/event.rs +++ b/tauri/src/endpoints/event.rs @@ -17,9 +17,9 @@ pub fn listen_fn(event: String, handler: String, once: bool) -> crate::Result( - dispatcher: &mut D, + webview_manager: &crate::WebviewManager, path: PathBuf, options: Option, 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( /// Copies a file. #[cfg(copy_file)] pub async fn copy_file( - dispatcher: &mut D, + webview_manager: &crate::WebviewManager, source: PathBuf, destination: PathBuf, options: Option, @@ -42,7 +42,7 @@ pub async fn copy_file( 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( /// Creates a directory. #[cfg(create_dir)] pub async fn create_dir( - dispatcher: &mut D, + webview_manager: &crate::WebviewManager, path: PathBuf, options: Option, 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( /// Removes a directory. #[cfg(remove_dir)] pub async fn remove_dir( - dispatcher: &mut D, + webview_manager: &crate::WebviewManager, path: PathBuf, options: Option, 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( /// Removes a file #[cfg(remove_file)] pub async fn remove_file( - dispatcher: &mut D, + webview_manager: &crate::WebviewManager, path: PathBuf, options: Option, 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( /// Renames a file. #[cfg(rename_file)] pub async fn rename_file( - dispatcher: &mut D, + webview_manager: &crate::WebviewManager, old_path: PathBuf, new_path: PathBuf, options: Option, @@ -157,7 +157,7 @@ pub async fn rename_file( 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( /// Writes a text file. #[cfg(write_file)] pub async fn write_file( - dispatcher: &mut D, + webview_manager: &crate::WebviewManager, path: PathBuf, contents: String, options: Option, @@ -185,7 +185,7 @@ pub async fn write_file( 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( /// Writes a binary file. #[cfg(write_binary_file)] pub async fn write_binary_file( - dispatcher: &mut D, + webview_manager: &crate::WebviewManager, path: PathBuf, contents: String, options: Option, @@ -209,7 +209,7 @@ pub async fn write_binary_file( 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( /// Reads a text file. #[cfg(read_text_file)] pub async fn read_text_file( - dispatcher: &mut D, + webview_manager: &crate::WebviewManager, path: PathBuf, options: Option, 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( /// Reads a binary file. #[cfg(read_binary_file)] pub async fn read_binary_file( - dispatcher: &mut D, + webview_manager: &crate::WebviewManager, path: PathBuf, options: Option, 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(""), diff --git a/tauri/src/endpoints/http.rs b/tauri/src/endpoints/http.rs index 1cdb585db..a194c39dc 100644 --- a/tauri/src/endpoints/http.rs +++ b/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( - dispatcher: &mut D, + webview_manager: &crate::WebviewManager, options: HttpRequestOptions, callback: String, error: String, ) { crate::execute_promise( - dispatcher, + webview_manager, async move { request(options).map_err(|e| e.into()) }, callback, error, diff --git a/tauri/src/endpoints/notification.rs b/tauri/src/endpoints/notification.rs index d51462de8..b3c3f45a1 100644 --- a/tauri/src/endpoints/notification.rs +++ b/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( - dispatcher: &mut D, + webview_manager: &crate::WebviewManager, options: NotificationOptions, callback: String, error: String, @@ -13,7 +13,7 @@ pub async fn send( 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( } pub async fn is_permission_granted( - dispatcher: &mut D, + webview_manager: &crate::WebviewManager, 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( } pub fn request_permission( - dispatcher: &mut D, + webview_manager: &crate::WebviewManager, 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(); diff --git a/tauri/src/endpoints/path.rs b/tauri/src/endpoints/path.rs index 424718106..d9ca10236 100644 --- a/tauri/src/endpoints/path.rs +++ b/tauri/src/endpoints/path.rs @@ -3,14 +3,14 @@ use crate::ApplicationDispatcherExt; use tauri_api::{path, path::BaseDirectory}; pub async fn resolve_path( - dispatcher: &mut D, + webview_manager: &crate::WebviewManager, path: String, directory: Option, callback: String, error: String, ) { crate::execute_promise( - dispatcher, + webview_manager, async move { path::resolve_path(path, directory).map_err(|e| e.into()) }, callback, error, diff --git a/tauri/src/endpoints/salt.rs b/tauri/src/endpoints/salt.rs index 5c097b21b..eb078b262 100644 --- a/tauri/src/endpoints/salt.rs +++ b/tauri/src/endpoints/salt.rs @@ -2,7 +2,7 @@ use crate::ApplicationDispatcherExt; /// Validates a salt. pub fn validate( - dispatcher: &mut D, + webview_manager: &crate::WebviewManager, salt: String, callback: String, error: String, @@ -13,6 +13,8 @@ pub fn validate( 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(()) } diff --git a/tauri/src/error.rs b/tauri/src/error.rs index 595e7f017..d3e3d8d3d 100644 --- a/tauri/src/error.rs +++ b/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), diff --git a/tauri/src/lib.rs b/tauri/src/lib.rs index af5ab4aea..c8e021106 100644 --- a/tauri/src/lib.rs +++ b/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 + Send + 'static, >( - dispatcher: &mut D, + webview_manager: &crate::WebviewManager, 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> + Send + 'static, >( - dispatcher: &mut D, + webview_manager: &crate::WebviewManager, 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( - dispatcher: &mut D, + webview_manager: &crate::WebviewManager, command: String, args: Vec, 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( .await; } -/// Closes the splashscreen. -pub fn close_splashscreen(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::*; diff --git a/tauri/src/plugin.rs b/tauri/src/plugin.rs index 98a3f02be..bc146e3bc 100644 --- a/tauri/src/plugin.rs +++ b/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: 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) {} /// 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) {} /// 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, + payload: &str, + ) -> crate::Result<()> { Err(crate::Error::UnknownApi) } } @@ -93,36 +99,36 @@ pub(crate) async fn initialization_script pub(crate) async fn created( store: &PluginStore, - dispatcher: &mut D, + webview_manager: &crate::WebviewManager, ) { 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( store: &PluginStore, - dispatcher: &mut D, + webview_manager: &crate::WebviewManager, ) { 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( store: &PluginStore, - dispatcher: &mut D, + webview_manager: &crate::WebviewManager, arg: &str, ) -> crate::Result { 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); } diff --git a/tauri/src/webview.rs b/tauri/src/webview.rs index dab9f3722..c534dd4ce 100644 --- a/tauri/src/webview.rs +++ b/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; } +pub struct WindowBuilder(T); + +impl WindowBuilder { + pub fn get(self) -> T { + self.0 + } +} + +impl From<&WindowConfig> for WindowBuilder { + 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 { /// 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. diff --git a/tauri/src/webview/wry.rs b/tauri/src/webview/wry.rs index a1b4cdf83..2f83de13f 100644 --- a/tauri/src/webview/wry.rs +++ b/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>>, current_window: wry::WindowId, - windows: Arc>>, +} + +struct WryMessage(wry::Message); + +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) + } } 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(); - + fn send_message(&self, message: Message) { + let message: WryMessage = (self.current_window, message).into(); 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(); - } - } - - fn send_event(&self, event: Event) { - 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, - windows: Arc>>, dispatcher_handle: Arc>>, } @@ -154,11 +179,9 @@ impl ApplicationExt for WryApplication { fn new() -> crate::Result { 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, diff --git a/tauri/test/fixture/src-tauri/tauri.conf.json b/tauri/test/fixture/src-tauri/tauri.conf.json index 66b7e9993..43994c43c 100644 --- a/tauri/test/fixture/src-tauri/tauri.conf.json +++ b/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'" }