From 2158a68d7d6e1db7596dc6288dbcefa96f7b3cb9 Mon Sep 17 00:00:00 2001 From: chip Date: Sat, 3 Apr 2021 17:41:04 -0700 Subject: [PATCH] wip: big refactor, removing application cycle + allowing generic parameters to the application (#1400) Co-authored-by: Lucas Nogueira --- .scripts/cargo-check.ps1 | 2 +- .scripts/cargo-check.sh | 2 +- cli/core/Cargo.lock | 4 +- core/tauri-build/src/codegen/context.rs | 22 +- core/tauri-codegen/src/context.rs | 45 +- core/tauri-codegen/src/embedded_assets.rs | 16 +- examples/api/src-tauri/src/main.rs | 14 +- examples/helloworld/src-tauri/src/main.rs | 3 +- examples/multiwindow/dist/index.html | 2 +- examples/multiwindow/src-tauri/src/main.rs | 25 +- .../multiwindow/src-tauri/tauri.conf.json | 2 - tauri-macros/src/command.rs | 45 +- tauri-macros/src/context/mod.rs | 115 ++-- tauri-macros/src/lib.rs | 12 +- tauri-utils/src/assets.rs | 2 +- tauri-utils/src/config.rs | 11 - tauri/Cargo.toml | 3 +- tauri/src/app.rs | 494 ---------------- tauri/src/app/event.rs | 244 -------- tauri/src/app/utils.rs | 303 ---------- tauri/src/app/webview.rs | 303 ---------- tauri/src/app/webview_manager.rs | 347 ------------ tauri/src/endpoints.rs | 38 +- tauri/src/endpoints/event.rs | 73 ++- tauri/src/endpoints/file_system.rs | 2 +- tauri/src/endpoints/global_shortcut.rs | 39 +- tauri/src/endpoints/shell.rs | 15 +- tauri/src/endpoints/window.rs | 86 +-- tauri/src/error.rs | 3 + tauri/src/event.rs | 219 ++++++++ tauri/src/hooks.rs | 164 ++++++ tauri/src/lib.rs | 34 +- tauri/src/plugin.rs | 154 +++-- tauri/src/runtime/app.rs | 185 ++++++ tauri/src/runtime/flavor/mod.rs | 3 + .../{app/webview => runtime/flavor}/wry.rs | 301 +++++----- tauri/src/runtime/manager.rs | 527 ++++++++++++++++++ tauri/src/runtime/mod.rs | 328 +++++++++++ tauri/src/runtime/tag.rs | 48 ++ tauri/src/runtime/webview.rs | 127 +++++ tauri/src/runtime/window.rs | 364 ++++++++++++ 41 files changed, 2469 insertions(+), 2257 deletions(-) delete mode 100644 tauri/src/app.rs delete mode 100644 tauri/src/app/event.rs delete mode 100644 tauri/src/app/utils.rs delete mode 100644 tauri/src/app/webview.rs delete mode 100644 tauri/src/app/webview_manager.rs create mode 100644 tauri/src/event.rs create mode 100644 tauri/src/hooks.rs create mode 100644 tauri/src/runtime/app.rs create mode 100644 tauri/src/runtime/flavor/mod.rs rename tauri/src/{app/webview => runtime/flavor}/wry.rs (65%) create mode 100644 tauri/src/runtime/manager.rs create mode 100644 tauri/src/runtime/mod.rs create mode 100644 tauri/src/runtime/tag.rs create mode 100644 tauri/src/runtime/webview.rs create mode 100644 tauri/src/runtime/window.rs diff --git a/.scripts/cargo-check.ps1 b/.scripts/cargo-check.ps1 index e39793bf8..318cc060d 100755 --- a/.scripts/cargo-check.ps1 +++ b/.scripts/cargo-check.ps1 @@ -39,7 +39,7 @@ foreach ($command in $args) { } "fmt" { Write-Output "[$command] checking formatting" - cargo fmt "--" --check + cargo +nightly fmt "--" --check check_error } default { diff --git a/.scripts/cargo-check.sh b/.scripts/cargo-check.sh index 78a42e028..d59b951c4 100755 --- a/.scripts/cargo-check.sh +++ b/.scripts/cargo-check.sh @@ -30,7 +30,7 @@ for command in "$@"; do ;; fmt) echo "[$command] checking formatting" - cargo fmt -- --check + cargo +nightly fmt -- --check ;; *) echo "[cargo-check.sh] Unknown cargo sub-command: $command" diff --git a/cli/core/Cargo.lock b/cli/core/Cargo.lock index 2cfba4a40..3fbc02264 100755 --- a/cli/core/Cargo.lock +++ b/cli/core/Cargo.lock @@ -2420,9 +2420,9 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "winreg" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" +checksum = "d107f8c6e916235c4c01cabb3e8acf7bea8ef6a63ca2e7fa0527c049badfc48c" dependencies = [ "winapi 0.3.9", ] diff --git a/core/tauri-build/src/codegen/context.rs b/core/tauri-build/src/codegen/context.rs index 34d248ca3..37f086708 100644 --- a/core/tauri-build/src/codegen/context.rs +++ b/core/tauri-build/src/codegen/context.rs @@ -1,6 +1,4 @@ use anyhow::{Context, Result}; -use proc_macro2::Ident; -use quote::format_ident; use std::{ env::var, fs::{create_dir_all, File}, @@ -18,7 +16,6 @@ use tauri_codegen::{context_codegen, ContextData}; #[derive(Debug)] pub struct CodegenContext { config_path: PathBuf, - struct_ident: Ident, out_file: PathBuf, } @@ -26,7 +23,6 @@ impl Default for CodegenContext { fn default() -> Self { Self { config_path: PathBuf::from("tauri.conf.json"), - struct_ident: format_ident!("TauriBuildCodegenContext"), out_file: PathBuf::from("tauri-build-context.rs"), } } @@ -48,20 +44,6 @@ impl CodegenContext { self } - /// Set the name of the generated struct. - /// - /// Don't set this if you are using [`tauri::include_codegen_context!`] as that helper macro - /// expects the default value. This option can be useful if you are not using the helper and - /// instead using [`std::include!`] on the generated code yourself. - /// - /// Defaults to `TauriBuildCodegenContext`. - /// - /// [`tauri::include_codegen_context!`]: https://docs.rs/tauri/0.12/tauri/macro.include_codegen_context.html - pub fn struct_ident(mut self, ident: impl AsRef) -> Self { - self.struct_ident = format_ident!("{}", ident.as_ref()); - self - } - /// Sets the output file's path. /// /// **Note:** This path should be relative to the `OUT_DIR`. @@ -100,7 +82,9 @@ impl CodegenContext { let code = context_codegen(ContextData { config, config_parent, - struct_ident: self.struct_ident.clone(), + // it's very hard to have a build script for unit tests, so assume this is always called from + // outside the tauri crate, making the ::tauri root valid. + context_path: quote::quote!(::tauri::Context), })?; // get the full output file path diff --git a/core/tauri-codegen/src/context.rs b/core/tauri-codegen/src/context.rs index cbaf22e66..95a2232f7 100644 --- a/core/tauri-codegen/src/context.rs +++ b/core/tauri-codegen/src/context.rs @@ -1,5 +1,5 @@ use crate::embedded_assets::{EmbeddedAssets, EmbeddedAssetsError}; -use proc_macro2::{Ident, TokenStream}; +use proc_macro2::TokenStream; use quote::quote; use std::path::PathBuf; use tauri_api::config::Config; @@ -8,7 +8,7 @@ use tauri_api::config::Config; pub struct ContextData { pub config: Config, pub config_parent: PathBuf, - pub struct_ident: Ident, + pub context_path: TokenStream, } /// Build an `AsTauriContext` implementation for including in application code. @@ -16,7 +16,7 @@ pub fn context_codegen(data: ContextData) -> Result Result= OnceCell::new(); - - /// Generated by `tauri-codegen`. - struct #struct_ident; - - impl AsTauriContext for #struct_ident { - /// Return a static reference to the config we parsed at build time - fn config() -> &'static ::tauri::api::config::Config { - CONFIG.get_or_init(|| #config) - } - - /// Inject assets we generated during build time - fn assets() -> &'static ::tauri::api::assets::EmbeddedAssets { - #assets - } - - /// Default window icon to set automatically if exists - fn default_window_icon() -> Option<&'static [u8]> { - #default_window_icon - } - } - - #struct_ident {} - }}) + Ok(quote!(#context_path { + config: #config, + assets: #assets, + default_window_icon: #default_window_icon, + })) } diff --git a/core/tauri-codegen/src/embedded_assets.rs b/core/tauri-codegen/src/embedded_assets.rs index 99c19b702..43fc7b84d 100644 --- a/core/tauri-codegen/src/embedded_assets.rs +++ b/core/tauri-codegen/src/embedded_assets.rs @@ -51,7 +51,6 @@ pub enum EmbeddedAssetsError { pub struct EmbeddedAssets(HashMap)>); impl EmbeddedAssets { - #[cfg(not(debug_assertions))] /// Compress a directory of assets, ready to be generated into a [`tauri_api::assets::Assets`]. pub fn new(path: &Path) -> Result { WalkDir::new(&path) @@ -74,14 +73,6 @@ impl EmbeddedAssets { .map(Self) } - #[cfg(debug_assertions)] - /// A dummy EmbeddedAssets for use during development builds. - /// Compressing + including the bytes of assets during development takes a long time. - /// On development builds, assets will simply be resolved & fetched from the configured dist folder. - pub fn new(_: &Path) -> Result { - Ok(EmbeddedAssets(HashMap::new())) - } - /// Use highest compression level for release, the fastest one for everything else fn compression_level() -> i32 { match var("PROFILE").as_ref().map(String::as_str) { @@ -135,10 +126,9 @@ impl ToTokens for EmbeddedAssets { } // we expect phf related items to be in path when generating the path code - tokens.append_all(quote! { + tokens.append_all(quote! {{ use ::tauri::api::assets::{EmbeddedAssets, phf, phf::phf_map}; - static ASSETS: EmbeddedAssets = EmbeddedAssets::from_zstd(phf_map! { #map }); - &ASSETS - }); + EmbeddedAssets::from_zstd(phf_map! { #map }) + }}); } } diff --git a/examples/api/src-tauri/src/main.rs b/examples/api/src-tauri/src/main.rs index 7f7250256..f6a61b22a 100644 --- a/examples/api/src-tauri/src/main.rs +++ b/examples/api/src-tauri/src/main.rs @@ -14,17 +14,16 @@ struct Reply { fn main() { tauri::AppBuilder::default() - .setup(move |webview_manager| { - let dispatcher = webview_manager.current_webview().unwrap(); - let dispatcher_ = dispatcher.clone(); - dispatcher.listen("js-event", move |event| { + .on_page_load(|window, _| { + let window_ = window.clone(); + window.listen("js-event".into(), move |event| { println!("got js-event with message '{:?}'", event.payload()); let reply = Reply { data: "something else".to_string(), }; - dispatcher_ - .emit("rust-event", Some(reply)) + window_ + .emit(&"rust-event".into(), Some(reply)) .expect("failed to emit"); }); }) @@ -33,5 +32,6 @@ fn main() { cmd::perform_request ]) .build(tauri::generate_context!()) - .run(); + .run() + .expect("error while running tauri application"); } diff --git a/examples/helloworld/src-tauri/src/main.rs b/examples/helloworld/src-tauri/src/main.rs index 5ec396a8a..50571a001 100644 --- a/examples/helloworld/src-tauri/src/main.rs +++ b/examples/helloworld/src-tauri/src/main.rs @@ -12,5 +12,6 @@ fn main() { tauri::AppBuilder::default() .invoke_handler(tauri::generate_handler![my_custom_command]) .build(tauri::generate_context!()) - .run(); + .run() + .expect("error while running tauri application"); } diff --git a/examples/multiwindow/dist/index.html b/examples/multiwindow/dist/index.html index 195ab7ae7..3b8e2d3db 100644 --- a/examples/multiwindow/dist/index.html +++ b/examples/multiwindow/dist/index.html @@ -72,4 +72,4 @@ - \ No newline at end of file + diff --git a/examples/multiwindow/src-tauri/src/main.rs b/examples/multiwindow/src-tauri/src/main.rs index c90342973..85822295d 100644 --- a/examples/multiwindow/src-tauri/src/main.rs +++ b/examples/multiwindow/src-tauri/src/main.rs @@ -3,27 +3,20 @@ windows_subsystem = "windows" )] -use tauri::WebviewBuilderExt; +use tauri::Attributes; fn main() { tauri::AppBuilder::default() - .setup(move |webview_manager| { - if webview_manager.current_window_label() == "Main" { - webview_manager.listen("clicked", move |_| { - println!("got 'clicked' event on global channel"); - }); - } - let current_webview = webview_manager.current_webview().unwrap(); - let label = webview_manager.current_window_label().to_string(); - current_webview.listen("clicked", move |_| { - println!("got 'clicked' event on window '{}'", label) + .on_page_load(|window, _payload| { + let label = window.label().to_string(); + window.listen("clicked".to_string(), move |_payload| { + println!("got 'clicked' event on window '{}'", label); }); }) - .create_webview("Rust".to_string(), tauri::WindowUrl::App, |mut builder| { - builder = builder.title("Tauri - Rust"); - Ok(builder) + .create_window("Rust".to_string(), tauri::WindowUrl::App, |attributes| { + attributes.title("Tauri - Rust") }) - .unwrap() .build(tauri::generate_context!()) - .run(); + .run() + .expect("failed to run tauri application"); } diff --git a/examples/multiwindow/src-tauri/tauri.conf.json b/examples/multiwindow/src-tauri/tauri.conf.json index 6de968f74..ae2ce6ae2 100644 --- a/examples/multiwindow/src-tauri/tauri.conf.json +++ b/examples/multiwindow/src-tauri/tauri.conf.json @@ -27,14 +27,12 @@ "windows": [ { "label": "Main", - "url": "app", "title": "Tauri - Main", "width": 800, "height": 600 }, { "label": "Secondary", - "url": "app", "title": "Tauri - Secondary", "width": 600, "height": 400 diff --git a/tauri-macros/src/command.rs b/tauri-macros/src/command.rs index 2eaf7cb61..59ae62043 100644 --- a/tauri-macros/src/command.rs +++ b/tauri-macros/src/command.rs @@ -1,23 +1,10 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; use syn::{ - parse::Parser, punctuated::Punctuated, FnArg, Ident, ItemFn, Meta, NestedMeta, Pat, Path, - ReturnType, Token, Type, + parse::Parser, punctuated::Punctuated, FnArg, Ident, ItemFn, Pat, Path, ReturnType, Token, Type, }; -pub fn generate_command(attrs: Vec, function: ItemFn) -> TokenStream { - // Check if "with_manager" attr was passed to macro - let uses_manager = attrs.iter().any(|a| { - if let NestedMeta::Meta(Meta::Path(path)) = a { - path - .get_ident() - .map(|i| *i == "with_manager") - .unwrap_or(false) - } else { - false - } - }); - +pub fn generate_command(function: ItemFn) -> TokenStream { let fn_name = function.sig.ident.clone(); let fn_name_str = fn_name.to_string(); let fn_wrapper = format_ident!("{}_wrapper", fn_name); @@ -37,7 +24,7 @@ pub fn generate_command(attrs: Vec, function: ItemFn) -> TokenStream }; // Split function args into names and types - let (mut names, mut types): (Vec, Vec) = function + let (names, types): (Vec, Vec) = function .sig .inputs .iter() @@ -59,22 +46,6 @@ pub fn generate_command(attrs: Vec, function: ItemFn) -> TokenStream }) .unzip(); - // If function doesn't take the webview manager, wrapper just takes webview manager generically and ignores it - // Otherwise the wrapper uses the specific type from the original function declaration - let mut manager_arg_type = quote!(::tauri::WebviewManager); - let manager_arg_maybe = match types.first() { - Some(first_type) if uses_manager => { - // Give wrapper specific type - manager_arg_type = quote!(#first_type); - // Remove webview manager arg from list so it isn't expected as arg from JS - types.drain(0..1); - names.drain(0..1); - // Tell wrapper to pass webview manager to original function - quote!(_manager,) - } - // Tell wrapper not to pass webview manager to original function - _ => quote!(), - }; let await_maybe = if function.sig.asyncness.is_some() { quote!(.await) } else { @@ -87,18 +58,18 @@ pub fn generate_command(attrs: Vec, function: ItemFn) -> TokenStream // note that all types must implement `serde::Serialize`. let return_value = if returns_result { quote! { - match #fn_name(#manager_arg_maybe #(parsed_args.#names),*)#await_maybe { + match #fn_name(#(parsed_args.#names),*)#await_maybe { Ok(value) => ::core::result::Result::Ok(value), Err(e) => ::core::result::Result::Err(e), } } } else { - quote! { ::core::result::Result::<_, ()>::Ok(#fn_name(#manager_arg_maybe #(parsed_args.#names),*)#await_maybe) } + quote! { ::core::result::Result::<_, ()>::Ok(#fn_name(#(parsed_args.#names),*)#await_maybe) } }; quote! { #function - pub fn #fn_wrapper(_manager: #manager_arg_type, message: ::tauri::InvokeMessage) { + pub fn #fn_wrapper(message: ::tauri::InvokeMessage

) { #[derive(::serde::Deserialize)] #[serde(rename_all = "camelCase")] struct ParsedArgs { @@ -134,9 +105,9 @@ pub fn generate_handler(item: proc_macro::TokenStream) -> TokenStream { }); quote! { - move |webview_manager, message| { + move |message| { match message.command() { - #(stringify!(#fn_names) => #fn_wrappers(webview_manager, message),)* + #(stringify!(#fn_names) => #fn_wrappers(message),)* _ => {}, } } diff --git a/tauri-macros/src/context/mod.rs b/tauri-macros/src/context/mod.rs index 675aa628e..7b15ed4da 100644 --- a/tauri-macros/src/context/mod.rs +++ b/tauri-macros/src/context/mod.rs @@ -1,67 +1,80 @@ -use proc_macro2::TokenStream; -use quote::{format_ident, quote}; -use std::path::PathBuf; +use proc_macro2::{Ident, Span, TokenStream}; +use quote::{quote, ToTokens}; +use std::{env::VarError, path::PathBuf}; +use syn::{ + parse::{Parse, ParseBuffer}, + punctuated::Punctuated, + LitStr, PathArguments, PathSegment, Token, +}; use tauri_codegen::{context_codegen, get_config, ContextData}; -/// Parse the passed [`proc_macro::TokenStream`] and make sure the config file exists. -/// -/// The [`proc_macro::TokenStream`] is expected to be empty for the default config path, or passed -/// a [`syn::LitStr`] a custom config path. The custom path can be either relative or absolute, with -/// relative paths being searched from the current working directory of the compiling crate. -/// -/// This is a macro so that we can use [`syn::parse_macro_input!`] and easily return a -/// [`std::compile_error!`] when the path input is bad. -#[macro_use] -macro_rules! parse_config_path { - ($path:ident) => { - { - use ::std::{env::{var, VarError}, path::PathBuf}; - use ::syn::{parse_macro_input, LitStr}; - use ::quote::quote; +pub(crate) struct ContextItems { + config_file: PathBuf, + context_path: syn::Path, +} - let path = if $path.is_empty() { - var("CARGO_MANIFEST_DIR").map(|m| PathBuf::from(m).join("tauri.conf.json")) +impl Parse for ContextItems { + fn parse(input: &ParseBuffer) -> syn::parse::Result { + let config_file = if input.is_empty() { + std::env::var("CARGO_MANIFEST_DIR").map(|m| PathBuf::from(m).join("tauri.conf.json")) + } else { + let raw: LitStr = input.parse()?; + let path = PathBuf::from(raw.value()); + if path.is_relative() { + std::env::var("CARGO_MANIFEST_DIR").map(|m| PathBuf::from(m).join(path)) } else { - let raw = parse_macro_input!($path as LitStr); - let path = PathBuf::from(raw.value()); - if path.is_relative() { - var("CARGO_MANIFEST_DIR").map(|m| PathBuf::from(m).join(path)) - } else { - Ok(path) - } - }; - - let path = path - .map_err(|error| match error { - VarError::NotPresent => "no CARGO_MANIFEST_DIR env var, this should be set by cargo".into(), - VarError::NotUnicode(_) => "CARGO_MANIFEST_DIR env var contained invalid utf8".into() - }) - .and_then(|path| { - if path.exists() { - Ok(path) - } else { - Err(format!( - "no file at path {} exists, expected tauri config file", - path.display() - )) - } - }); - - match path { - Ok(path) => path, - Err(error_string) => return quote!(compile_error!(#error_string)).into(), + Ok(path) } } + .map_err(|error| match error { + VarError::NotPresent => "no CARGO_MANIFEST_DIR env var, this should be set by cargo".into(), + VarError::NotUnicode(_) => "CARGO_MANIFEST_DIR env var contained invalid utf8".into(), + }) + .and_then(|path| { + if path.exists() { + Ok(path) + } else { + Err(format!( + "no file at path {} exists, expected tauri config file", + path.display() + )) + } + }) + .map_err(|e| input.error(e))?; + + let context_path = if input.is_empty() { + let mut segments = Punctuated::new(); + segments.push(PathSegment { + ident: Ident::new("tauri", Span::call_site()), + arguments: PathArguments::None, + }); + segments.push(PathSegment { + ident: Ident::new("Context", Span::call_site()), + arguments: PathArguments::None, + }); + syn::Path { + leading_colon: Some(Token![::](Span::call_site())), + segments, + } + } else { + let _: Token![,] = input.parse()?; + input.call(syn::Path::parse_mod_style)? + }; + + Ok(Self { + config_file, + context_path, + }) } } -pub(crate) fn generate_context(path: PathBuf) -> TokenStream { - let context = get_config(&path) +pub(crate) fn generate_context(context: ContextItems) -> TokenStream { + let context = get_config(&context.config_file) .map_err(|e| e.to_string()) .map(|(config, config_parent)| ContextData { config, config_parent, - struct_ident: format_ident!("AutoTauriContext"), + context_path: context.context_path.to_token_stream(), }) .and_then(|data| context_codegen(data).map_err(|e| e.to_string())); diff --git a/tauri-macros/src/lib.rs b/tauri-macros/src/lib.rs index 613c9d2e7..3e8ee5512 100644 --- a/tauri-macros/src/lib.rs +++ b/tauri-macros/src/lib.rs @@ -1,6 +1,7 @@ extern crate proc_macro; +use crate::context::ContextItems; use proc_macro::TokenStream; -use syn::{parse_macro_input, AttributeArgs, ItemFn}; +use syn::{parse_macro_input, ItemFn}; mod command; @@ -8,10 +9,9 @@ mod command; mod context; #[proc_macro_attribute] -pub fn command(attrs: TokenStream, item: TokenStream) -> TokenStream { +pub fn command(_: TokenStream, item: TokenStream) -> TokenStream { let function = parse_macro_input!(item as ItemFn); - let attrs = parse_macro_input!(attrs as AttributeArgs); - let gen = command::generate_command(attrs, function); + let gen = command::generate_command(function); gen.into() } @@ -34,8 +34,8 @@ pub fn generate_handler(item: TokenStream) -> TokenStream { /// /// todo: link the [`AsTauriContext`] docs #[proc_macro] -pub fn generate_context(item: TokenStream) -> TokenStream { +pub fn generate_context(items: TokenStream) -> TokenStream { // this macro is exported from the context module - let path = parse_config_path!(item); + let path = parse_macro_input!(items as ContextItems); context::generate_context(path).into() } diff --git a/tauri-utils/src/assets.rs b/tauri-utils/src/assets.rs index ee02d1156..af9ca93a6 100644 --- a/tauri-utils/src/assets.rs +++ b/tauri-utils/src/assets.rs @@ -69,7 +69,7 @@ impl> From

for AssetKey { } /// Represents a container of file assets that are retrievable during runtime. -pub trait Assets { +pub trait Assets: Send + Sync + 'static { /// Get the content of the passed [`AssetKey`]. fn get>(&self, key: Key) -> Option>; } diff --git a/tauri-utils/src/config.rs b/tauri-utils/src/config.rs index cad8eb459..7d111e383 100644 --- a/tauri-utils/src/config.rs +++ b/tauri-utils/src/config.rs @@ -388,17 +388,6 @@ pub struct Config { #[derive(Debug, Clone, Default, PartialEq, Deserialize)] pub struct PluginConfig(pub HashMap); -impl PluginConfig { - /// Gets a plugin configuration. - pub fn get>(&self, plugin_name: S) -> String { - self - .0 - .get(plugin_name.as_ref()) - .map(|config| config.to_string()) - .unwrap_or_else(|| "{}".to_string()) - } -} - /// Implement `ToTokens` for all config structs, allowing a literal `Config` to be built. /// /// This allows for a build script to output the values in a `Config` to a `TokenStream`, which can diff --git a/tauri/Cargo.toml b/tauri/Cargo.toml index e3635763d..5281ccd23 100644 --- a/tauri/Cargo.toml +++ b/tauri/Cargo.toml @@ -23,13 +23,12 @@ serde = { version = "1.0", features = [ "derive" ] } base64 = "0.13.0" tokio = { version = "1.4", features = ["rt", "rt-multi-thread", "sync"] } futures = "0.3" -async-trait = "0.1" uuid = { version = "0.8.2", features = [ "v4" ] } thiserror = "1.0.24" once_cell = "1.7.2" tauri-api = { version = "0.7.5", path = "../tauri-api" } tauri-macros = { version = "0.1", path = "../tauri-macros" } -wry = "0.6" +wry = "0.7" rand = "0.8" [build-dependencies] diff --git a/tauri/src/app.rs b/tauri/src/app.rs deleted file mode 100644 index 6b7cf178c..000000000 --- a/tauri/src/app.rs +++ /dev/null @@ -1,494 +0,0 @@ -use serde::{Deserialize, Serialize}; -use serde_json::Value as JsonValue; -use tauri_api::{ - config::Config, - private::AsTauriContext, - rpc::{format_callback, format_callback_result}, -}; - -use std::{ - collections::HashMap, - sync::{Arc, Mutex}, -}; - -pub(crate) mod event; -mod utils; -pub(crate) mod webview; -mod webview_manager; - -pub use crate::api::config::WindowUrl; -use crate::flavors::Wry; -pub use webview::{ - wry::WryApplication, ApplicationDispatcherExt, ApplicationExt, CustomProtocol, FileDropEvent, - FileDropHandler, Icon, Message, RpcRequest, WebviewBuilderExt, WebviewRpcHandler, -}; -pub use webview_manager::{WebviewDispatcher, WebviewManager}; - -type InvokeHandler = dyn Fn(WebviewManager, InvokeMessage) + Send; -type ManagerHook = dyn Fn(WebviewManager) + Send; -type PageLoadHook = dyn Fn(WebviewManager, PageLoadPayload) + Send; - -/// Payload from an invoke call. -#[derive(Debug, Deserialize)] -pub(crate) struct InvokePayload { - #[serde(rename = "__tauriModule")] - tauri_module: Option, - callback: String, - error: String, - #[serde(rename = "mainThread", default)] - pub(crate) main_thread: bool, - #[serde(flatten)] - inner: JsonValue, -} - -/// An invoke message. -pub struct InvokeMessage { - webview_manager: WebviewManager, - command: String, - payload: InvokePayload, -} - -impl InvokeMessage { - pub(crate) fn new( - webview_manager: WebviewManager, - command: String, - payload: InvokePayload, - ) -> Self { - Self { - webview_manager, - command, - payload, - } - } - - /// The invoke command. - pub fn command(&self) -> &str { - &self.command - } - - /// The invoke payload. - pub fn payload(&self) -> JsonValue { - self.payload.inner.clone() - } - - /// Reply to the invoke promise with a async task. - pub fn respond_async< - T: Serialize, - E: Serialize, - F: std::future::Future> + Send + 'static, - >( - self, - task: F, - ) { - if self.payload.main_thread { - crate::async_runtime::block_on(async move { - return_task( - &self.webview_manager, - task, - self.payload.callback, - self.payload.error, - ) - .await; - }); - } else { - crate::async_runtime::spawn(async move { - return_task( - &self.webview_manager, - task, - self.payload.callback, - self.payload.error, - ) - .await; - }); - } - } - - /// Reply to the invoke promise running the given closure. - pub fn respond_closure Result>(self, f: F) { - return_closure( - &self.webview_manager, - f, - self.payload.callback, - self.payload.error, - ) - } - - /// Resolve the invoke promise with a value. - pub fn resolve(self, value: S) { - return_result( - &self.webview_manager, - Result::::Ok(value), - self.payload.callback, - self.payload.error, - ) - } - - /// Reject the invoke promise with a value. - pub fn reject(self, value: S) { - return_result( - &self.webview_manager, - Result::<(), S>::Err(value), - self.payload.callback, - self.payload.error, - ) - } -} - -/// Asynchronously executes the given task -/// and evaluates its Result to the JS promise described by the `success_callback` and `error_callback` function names. -/// -/// If the Result `is_ok()`, the callback will be the `success_callback` function name and the argument will be the Ok value. -/// If the Result `is_err()`, the callback will be the `error_callback` function name and the argument will be the Err value. -async fn return_task< - A: ApplicationExt + 'static, - T: Serialize, - E: Serialize, - F: std::future::Future> + Send + 'static, ->( - webview_manager: &crate::WebviewManager, - task: F, - success_callback: String, - error_callback: String, -) { - let result = task.await; - return_closure(webview_manager, || result, success_callback, error_callback) -} - -fn return_closure< - A: ApplicationExt + 'static, - T: Serialize, - E: Serialize, - F: FnOnce() -> Result, ->( - webview_manager: &crate::WebviewManager, - f: F, - success_callback: String, - error_callback: String, -) { - return_result(webview_manager, f(), success_callback, error_callback) -} - -fn return_result( - webview_manager: &crate::WebviewManager, - result: Result, - success_callback: String, - error_callback: String, -) { - let callback_string = - match format_callback_result(result, success_callback, error_callback.clone()) { - Ok(callback_string) => callback_string, - Err(e) => format_callback(error_callback, e.to_string()), - }; - if let Ok(dispatcher) = webview_manager.current_webview() { - let _ = dispatcher.eval(callback_string.as_str()); - } -} - -/// `App` runtime information. -pub struct Context { - pub(crate) config: &'static Config, - pub(crate) default_window_icon: Option<&'static [u8]>, - pub(crate) assets: &'static tauri_api::assets::EmbeddedAssets, -} - -impl Context { - pub(crate) fn new(_: Context) -> Self { - Self { - config: Context::config(), - default_window_icon: Context::default_window_icon(), - assets: Context::assets(), - } - } -} - -pub(crate) struct Webview { - pub(crate) builder: A::WebviewBuilder, - pub(crate) label: String, - pub(crate) url: WindowUrl, -} - -/// The payload for the "page_load" hook. -#[derive(Debug, Clone, Deserialize)] -pub struct PageLoadPayload { - url: String, -} - -impl PageLoadPayload { - /// The page URL. - pub fn url(&self) -> &str { - &self.url - } -} - -/// The application runner. -pub struct App { - /// The JS message handler. - invoke_handler: Option>>, - /// The page load hook, invoked when the webview performs a navigation. - on_page_load: Option>>, - /// The setup hook, invoked when the webviews have been created. - setup: Option>>, - /// The context the App was created with - pub(crate) context: Context, - pub(crate) dispatchers: Arc>>>, - pub(crate) webviews: Option>>, - url: String, - window_labels: Arc>>, - plugin_initialization_script: String, -} - -impl App { - /// Runs the app until it finishes. - pub fn run(mut self) { - { - let mut window_labels = self.window_labels.lock().unwrap(); - for window_config in self.context.config.tauri.windows.clone() { - let window_url = window_config.url.clone(); - let window_label = window_config.label.to_string(); - window_labels.push(window_label.to_string()); - let webview = A::WebviewBuilder::from(webview::WindowConfig(window_config)); - let mut webviews = self.webviews.take().unwrap(); - webviews.push(Webview { - label: window_label, - builder: webview, - url: window_url, - }); - self.webviews = Some(webviews); - } - } - - run(self).expect("failed to run application"); - } - - /// Runs the invoke handler if defined. - /// Returns whether the message was consumed or not. - /// The message is considered consumed if the handler exists and returns an Ok Result. - pub(crate) fn run_invoke_handler( - &self, - dispatcher: &WebviewManager, - message: InvokeMessage, - ) { - if let Some(ref invoke_handler) = self.invoke_handler { - invoke_handler(dispatcher.clone(), message); - } - } - - /// Runs the setup hook if defined. - pub(crate) fn run_setup(&self, dispatcher: WebviewManager) { - if let Some(ref setup) = self.setup { - setup(dispatcher); - } - } - - /// Runs the on page load hook if defined. - pub(crate) fn run_on_page_load(&self, dispatcher: &WebviewManager, payload: PageLoadPayload) { - if let Some(ref on_page_load) = self.on_page_load { - on_page_load(dispatcher.clone(), payload); - } - } -} - -type WebviewContext = ( - ::WebviewBuilder, - Option::Dispatcher>>, - Option, - Option, -); - -trait WebviewInitializer { - fn init_webview(&self, webview: Webview) -> crate::Result>; - - fn on_webview_created(&self, webview_label: String, dispatcher: A::Dispatcher); -} - -impl WebviewInitializer for Arc>> { - fn init_webview(&self, webview: Webview) -> crate::Result> { - let application = self.lock().unwrap(); - let webview_manager = WebviewManager::new( - self.clone(), - application.dispatchers.clone(), - webview.label.to_string(), - ); - let (webview_builder, rpc_handler, custom_protocol) = utils::build_webview( - self.clone(), - webview, - &webview_manager, - &application.url, - &application.window_labels.lock().unwrap(), - &application.plugin_initialization_script, - &application.context, - )?; - let file_drop_handler: Box bool + Send> = Box::new(move |event| { - let webview_manager = webview_manager.clone(); - crate::async_runtime::block_on(async move { - let webview = webview_manager.current_webview().unwrap(); - let _ = match event { - FileDropEvent::Hovered(paths) => webview.emit("tauri://file-drop-hover", Some(paths)), - FileDropEvent::Dropped(paths) => webview.emit("tauri://file-drop", Some(paths)), - FileDropEvent::Cancelled => webview.emit("tauri://file-drop-cancelled", Some(())), - }; - }); - true - }); - Ok(( - webview_builder, - rpc_handler, - custom_protocol, - Some(file_drop_handler), - )) - } - - fn on_webview_created(&self, webview_label: String, dispatcher: A::Dispatcher) { - self.lock().unwrap().dispatchers.lock().unwrap().insert( - webview_label.to_string(), - WebviewDispatcher::new(dispatcher, webview_label), - ); - } -} - -/// The App builder. -pub struct AppBuilder -where - A: ApplicationExt, -{ - /// The JS message handler. - invoke_handler: Option>>, - /// The setup hook. - setup: Option>>, - /// Page load hook. - on_page_load: Option>>, - /// The webview dispatchers. - dispatchers: Arc>>>, - /// The created webviews. - webviews: Vec>, -} - -impl AppBuilder { - /// Creates a new App builder. - pub fn new() -> Self { - Self { - invoke_handler: None, - setup: None, - on_page_load: None, - dispatchers: Default::default(), - webviews: Default::default(), - } - } - - /// Defines the JS message handler callback. - pub fn invoke_handler, InvokeMessage) + Send + 'static>( - mut self, - invoke_handler: F, - ) -> Self { - self.invoke_handler = Some(Box::new(invoke_handler)); - self - } - - /// Defines the setup hook. - pub fn setup) + Send + 'static>(mut self, setup: F) -> Self { - self.setup = Some(Box::new(setup)); - self - } - - /// Defines the page load hook. - pub fn on_page_load, PageLoadPayload) + Send + 'static>( - mut self, - on_page_load: F, - ) -> Self { - self.on_page_load = Some(Box::new(on_page_load)); - self - } - - /// Adds a plugin to the runtime. - pub fn plugin(self, plugin: impl crate::plugin::Plugin + Send + 'static) -> Self { - crate::plugin::register(A::plugin_store(), plugin); - self - } - - /// Creates a new webview. - pub fn create_webview crate::Result>( - mut self, - label: String, - url: WindowUrl, - f: F, - ) -> crate::Result { - let builder = f(A::WebviewBuilder::new())?; - self.webviews.push(Webview { - label, - builder, - url, - }); - Ok(self) - } - - /// Builds the App. - pub fn build(self, context: impl AsTauriContext) -> App { - let window_labels: Vec = self.webviews.iter().map(|w| w.label.to_string()).collect(); - let plugin_initialization_script = crate::plugin::initialization_script(A::plugin_store()); - - let context = Context::new(context); - let url = utils::get_url(&context); - - App { - invoke_handler: self.invoke_handler, - setup: self.setup, - on_page_load: self.on_page_load, - context, - dispatchers: self.dispatchers, - webviews: Some(self.webviews), - url, - window_labels: Arc::new(Mutex::new(window_labels)), - plugin_initialization_script, - } - } -} - -/// Make `Wry` the default `ApplicationExt` for `AppBuilder` -impl Default for AppBuilder { - fn default() -> Self { - Self::new() - } -} - -fn run(mut application: App) -> crate::Result<()> { - let plugin_config = application.context.config.plugins.clone(); - crate::plugin::initialize(A::plugin_store(), plugin_config)?; - - let webviews = application.webviews.take().unwrap(); - - let dispatchers = application.dispatchers.clone(); - let application = Arc::new(Mutex::new(application)); - let mut webview_app = A::new()?; - let mut main_webview_manager = None; - - for webview in webviews { - let webview_label = webview.label.to_string(); - let webview_manager = WebviewManager::new( - application.clone(), - dispatchers.clone(), - webview_label.to_string(), - ); - if main_webview_manager.is_none() { - main_webview_manager = Some(webview_manager.clone()); - } - let (webview_builder, rpc_handler, custom_protocol, file_drop_handler) = - application.init_webview(webview)?; - - let dispatcher = webview_app.create_webview( - webview_builder, - rpc_handler, - custom_protocol, - file_drop_handler, - )?; - application.on_webview_created(webview_label, dispatcher); - crate::plugin::created(A::plugin_store(), &webview_manager); - } - - if let Some(main_webview_manager) = main_webview_manager { - application.lock().unwrap().run_setup(main_webview_manager); - } - - webview_app.run(); - - Ok(()) -} diff --git a/tauri/src/app/event.rs b/tauri/src/app/event.rs deleted file mode 100644 index 939545ed9..000000000 --- a/tauri/src/app/event.rs +++ /dev/null @@ -1,244 +0,0 @@ -use std::{ - boxed::Box, - collections::HashMap, - sync::{Arc, Mutex}, -}; - -use crate::ApplicationDispatcherExt; -use once_cell::sync::Lazy; -use serde::Serialize; -use serde_json::Value as JsonValue; -use uuid::Uuid; - -/// Event identifier. -pub type EventId = u64; - -/// An event handler. -struct EventHandler { - /// Event identifier. - id: EventId, - /// A event handler might be global or tied to a window. - window_label: Option, - /// The on event callback. - on_event: Box, -} - -type Listeners = Arc>>>; - -static EMIT_FUNCTION_NAME: Lazy = Lazy::new(|| Uuid::new_v4().to_string()); -static EVENT_LISTENERS_OBJECT_NAME: Lazy = Lazy::new(|| Uuid::new_v4().to_string()); -static EVENT_QUEUE_OBJECT_NAME: Lazy = Lazy::new(|| Uuid::new_v4().to_string()); - -/// Gets the listeners map. -fn listeners() -> &'static Listeners { - static LISTENERS: Lazy = Lazy::new(Default::default); - &LISTENERS -} - -/// the emit JS function name -pub fn emit_function_name() -> String { - EMIT_FUNCTION_NAME.to_string() -} - -/// the event listeners JS object name -pub fn event_listeners_object_name() -> String { - EVENT_LISTENERS_OBJECT_NAME.to_string() -} - -/// the event queue JS object name -pub fn event_queue_object_name() -> String { - EVENT_QUEUE_OBJECT_NAME.to_string() -} - -#[derive(Debug, Clone)] -pub struct EventPayload { - id: EventId, - payload: Option, -} - -impl EventPayload { - /// The event identifier. - pub fn id(&self) -> EventId { - self.id - } - - /// The event payload. - pub fn payload(&self) -> Option<&String> { - self.payload.as_ref() - } -} - -/// Adds an event listener for JS events. -pub fn listen( - event_name: impl AsRef, - window_label: Option, - handler: F, -) -> EventId { - let mut l = listeners() - .lock() - .expect("Failed to lock listeners: listen()"); - let id = rand::random(); - let handler = EventHandler { - id, - window_label, - on_event: Box::new(handler), - }; - if let Some(listeners) = l.get_mut(event_name.as_ref()) { - listeners.push(handler); - } else { - l.insert(event_name.as_ref().to_string(), vec![handler]); - } - id -} - -/// Listen to an JS event and immediately unlisten. -pub fn once( - event_name: impl AsRef, - window_label: Option, - handler: F, -) { - listen(event_name, window_label, move |event| { - unlisten(event.id); - handler(event); - }); -} - -/// Removes an event listener. -pub fn unlisten(event_id: EventId) { - crate::async_runtime::spawn(async move { - let mut event_listeners = listeners() - .lock() - .expect("Failed to lock listeners: listen()"); - for listeners in event_listeners.values_mut() { - if let Some(index) = listeners.iter().position(|l| l.id == event_id) { - listeners.remove(index); - } - } - }) -} - -/// Emits an event to JS. -pub fn emit( - webview_dispatcher: &crate::WebviewDispatcher, - event: impl AsRef, - payload: Option, -) -> crate::Result<()> { - let salt = crate::salt::generate(); - - let js_payload = if let Some(payload_value) = payload { - serde_json::to_value(payload_value)? - } else { - JsonValue::Null - }; - - webview_dispatcher.eval(&format!( - "window['{}']({{event: '{}', payload: {}}}, '{}')", - emit_function_name(), - event.as_ref(), - js_payload, - salt - ))?; - - Ok(()) -} - -/// Triggers the given event with its payload. -pub(crate) fn on_event(event: String, window_label: Option<&str>, data: Option) { - let mut l = listeners() - .lock() - .expect("Failed to lock listeners: on_event()"); - - if l.contains_key(&event) { - let listeners = l.get_mut(&event).expect("Failed to get mutable handler"); - for handler in listeners { - if let Some(target_window_label) = window_label { - // if the emitted event targets a specifid window, only triggers the listeners associated to that window - if handler.window_label.as_deref() == Some(target_window_label) { - let payload = data.clone(); - (handler.on_event)(EventPayload { - id: handler.id, - payload, - }); - } - } else { - // otherwise triggers all listeners - let payload = data.clone(); - (handler.on_event)(EventPayload { - id: handler.id, - payload, - }); - } - } - } -} - -#[cfg(test)] -mod test { - use super::*; - use proptest::prelude::*; - - // dummy event handler function - fn event_fn(s: EventPayload) { - println!("{:?}", s); - } - - proptest! { - #![proptest_config(ProptestConfig::with_cases(10000))] - #[test] - // check to see if listen() is properly passing keys into the LISTENERS map - fn listeners_check_key(e in "[a-z]+") { - // clone e as the key - let key = e.clone(); - // pass e and an dummy func into listen - listen(e, None, event_fn); - - // lock mutex - let l = listeners().lock().unwrap(); - - // check if the generated key is in the map - assert_eq!(l.contains_key(&key), true); - } - - #[test] - // check to see if listen inputs a handler function properly into the LISTENERS map. - fn listeners_check_fn(e in "[a-z]+") { - // clone e as the key - let key = e.clone(); - // pass e and an dummy func into listen - listen(e, None, event_fn); - - // lock mutex - let mut l = listeners().lock().unwrap(); - - // check if l contains key - if l.contains_key(&key) { - // grab key if it exists - let handler = l.get_mut(&key); - // check to see if we get back a handler or not - match handler { - // pass on Some(handler) - Some(_) => {}, - // Fail on None - None => panic!("handler is None") - } - } - } - - #[test] - // check to see if on_event properly grabs the stored function from listen. - fn check_on_event(e in "[a-z]+", d in "[a-z]+") { - // clone e as the key - let key = e.clone(); - // call listen with e and the event_fn dummy func - listen(e.clone(), None, event_fn); - // call on event with e and d. - on_event(e, None, Some(d)); - - // lock the mutex - let l = listeners().lock().unwrap(); - - // assert that the key is contained in the listeners map - assert!(l.contains_key(&key)); - } - } -} diff --git a/tauri/src/app/utils.rs b/tauri/src/app/utils.rs deleted file mode 100644 index b4ff06486..000000000 --- a/tauri/src/app/utils.rs +++ /dev/null @@ -1,303 +0,0 @@ -use crate::{ - api::{assets::Assets, config::WindowUrl}, - app::Icon, - ApplicationExt, WebviewBuilderExt, -}; - -use super::{ - webview::{CustomProtocol, WebviewBuilderExtPrivate, WebviewRpcHandler}, - App, Context, InvokeMessage, InvokePayload, PageLoadPayload, RpcRequest, Webview, WebviewManager, -}; - -use serde_json::Value as JsonValue; -use std::{ - borrow::Cow, - sync::{Arc, Mutex}, -}; - -// setup content for dev-server -#[cfg(dev)] -pub(super) fn get_url(context: &Context) -> String { - let config = &context.config; - if config.build.dev_path.starts_with("http") { - config.build.dev_path.clone() - } else { - let path = "index.html"; - format!( - "data:text/html;base64,{}", - base64::encode( - context - .assets - .get(&path) - .ok_or_else(|| crate::Error::AssetNotFound(path.to_string())) - .map(Cow::into_owned) - .expect("Unable to find `index.html` under your devPath folder") - ) - ) - } -} - -#[cfg(custom_protocol)] -pub(super) fn get_url(context: &Context) -> String { - // Custom protocol doesn't require any setup, so just return URL - format!("tauri://{}", context.config.tauri.bundle.identifier) -} - -pub(super) fn initialization_script( - plugin_initialization_script: &str, - with_global_tauri: bool, -) -> String { - format!( - r#" - {bundle_script} - {core_script} - {event_initialization_script} - if (window.rpc) {{ - window.__TAURI__.invoke("__initialized", {{ url: window.location.href }}) - }} else {{ - window.addEventListener('DOMContentLoaded', function () {{ - window.__TAURI__.invoke("__initialized", {{ url: window.location.href }}) - }}) - }} - {plugin_initialization_script} - "#, - core_script = include_str!("../../scripts/core.js"), - bundle_script = if with_global_tauri { - include_str!("../../scripts/bundle.js") - } else { - "" - }, - event_initialization_script = event_initialization_script(), - plugin_initialization_script = plugin_initialization_script - ) -} - -fn event_initialization_script() -> String { - return format!( - " - window['{queue}'] = []; - window['{fn}'] = function (eventData, salt, ignoreQueue) {{ - const listeners = (window['{listeners}'] && window['{listeners}'][eventData.event]) || [] - if (!ignoreQueue && listeners.length === 0) {{ - window['{queue}'].push({{ - eventData: eventData, - salt: salt - }}) - }} - - if (listeners.length > 0) {{ - window.__TAURI__.invoke('tauri', {{ - __tauriModule: 'Internal', - message: {{ - cmd: 'validateSalt', - salt: salt - }} - }}).then(function (flag) {{ - if (flag) {{ - for (let i = listeners.length - 1; i >= 0; i--) {{ - const listener = listeners[i] - eventData.id = listener.id - listener.handler(eventData) - }} - }} - }}) - }} - }} - ", - fn = crate::event::emit_function_name(), - queue = crate::event::event_queue_object_name(), - listeners = crate::event::event_listeners_object_name() - ); -} - -pub(super) type BuiltWebview = ( - ::WebviewBuilder, - Option::Dispatcher>>, - Option, -); - -// build the webview. -pub(super) fn build_webview( - application: Arc>>, - webview: Webview, - webview_manager: &WebviewManager, - content_url: &str, - window_labels: &[String], - plugin_initialization_script: &str, - context: &Context, -) -> crate::Result> { - let webview_url = match &webview.url { - WindowUrl::App => content_url.to_string(), - WindowUrl::Custom(url) => url.to_string(), - }; - - let is_local = match webview.url { - WindowUrl::App => true, - WindowUrl::Custom(url) => &url[0..8] == "tauri://", - }; - let (webview_builder, rpc_handler, custom_protocol) = if is_local { - let mut webview_builder = webview.builder.url(webview_url) - .initialization_script(&initialization_script(plugin_initialization_script, context.config.build.with_global_tauri)) - .initialization_script(&format!( - r#" - window.__TAURI__.__windows = {window_labels_array}.map(function (label) {{ return {{ label: label }} }}); - window.__TAURI__.__currentWindow = {{ label: "{current_window_label}" }} - "#, - window_labels_array = - serde_json::to_string(&window_labels).unwrap(), - current_window_label = webview.label, - )); - - if !webview_builder.has_icon() { - if let Some(default_window_icon) = &context.default_window_icon { - webview_builder = webview_builder.icon(Icon::Raw(default_window_icon.to_vec()))?; - } - } - - let webview_manager_ = webview_manager.clone(); - let rpc_handler: Box::Dispatcher, RpcRequest) + Send> = - Box::new(move |_, request: RpcRequest| { - let command = request.command.clone(); - let arg = request - .params - .unwrap() - .as_array_mut() - .unwrap() - .first_mut() - .unwrap_or(&mut JsonValue::Null) - .take(); - let webview_manager = webview_manager_.clone(); - match serde_json::from_value::(arg) { - Ok(message) => { - let _ = on_message(application.clone(), webview_manager, command, message); - } - Err(e) => { - if let Ok(dispatcher) = webview_manager.current_webview() { - let error: crate::Error = e.into(); - let _ = dispatcher.eval(&format!( - r#"console.error({})"#, - JsonValue::String(error.to_string()) - )); - } - } - } - }); - let bundle_identifier = context.config.tauri.bundle.identifier.clone(); - #[cfg(debug_assertions)] - let dist_dir = std::path::PathBuf::from(context.config.build.dist_dir.clone()); - #[cfg(not(debug_assertions))] - let assets = context.assets; - let custom_protocol = CustomProtocol { - name: "tauri".into(), - handler: Box::new(move |path| { - let mut path = path - .split('?') - // ignore query string - .next() - .unwrap() - .to_string() - .replace(&format!("tauri://{}", bundle_identifier), ""); - if path.ends_with('/') { - path.pop(); - } - let path = if path.is_empty() { - // if the url is `tauri://${appId}`, we should load `index.html` - "index.html".to_string() - } else { - // skip leading `/` - path.chars().skip(1).collect::() - }; - - // In development builds, resolve, read and directly serve assets in the configured dist folder. - #[cfg(debug_assertions)] - { - dist_dir - .canonicalize() - .or_else(|_| Err(crate::Error::AssetNotFound(path.clone()))) - .and_then(|pathbuf| { - pathbuf - .join(path.clone()) - .canonicalize() - .or_else(|_| Err(crate::Error::AssetNotFound(path.clone()))) - .and_then(|pathbuf| { - match std::fs::read(pathbuf) { - Ok(asset) => Ok(asset), - Err(e) => { - eprintln!("Error reading asset from dist: {:?}", e); // TODO log::error! - Err(crate::Error::AssetNotFound(path.clone())) - } - } - }) - }) - } - - // In release builds, fetch + serve decompressed embedded assets. - #[cfg(not(debug_assertions))] - { - assets - .get(&path) - .ok_or(crate::Error::AssetNotFound(path)) - .map(Cow::into_owned) - } - }), - }; - (webview_builder, Some(rpc_handler), Some(custom_protocol)) - } else { - (webview.builder.url(webview_url), None, None) - }; - - Ok((webview_builder, rpc_handler, custom_protocol)) -} - -fn on_message( - application: Arc>>, - webview_manager: WebviewManager, - command: String, - payload: InvokePayload, -) -> crate::Result<()> { - let message = InvokeMessage::new(webview_manager.clone(), command.to_string(), payload); - if &command == "__initialized" { - let payload: PageLoadPayload = serde_json::from_value(message.payload())?; - application - .lock() - .unwrap() - .run_on_page_load(&webview_manager, payload.clone()); - crate::plugin::on_page_load(A::plugin_store(), &webview_manager, payload); - } else if let Some(module) = &message.payload.tauri_module { - let module = module.to_string(); - crate::endpoints::handle( - &webview_manager, - module, - message, - &application.lock().unwrap().context, - ); - } else if command.starts_with("plugin:") { - crate::plugin::extend_api(A::plugin_store(), &webview_manager, command, message); - } else { - application - .lock() - .unwrap() - .run_invoke_handler(&webview_manager, message); - } - Ok(()) -} - -#[cfg(test)] -mod test { - use crate::{generate_context, Context}; - - #[test] - fn check_get_url() { - let context = generate_context!("test/fixture/src-tauri/tauri.conf.json"); - let context = Context::new(context); - let res = super::get_url(&context); - #[cfg(custom_protocol)] - assert!(res == "tauri://studio.tauri.example"); - - #[cfg(dev)] - { - let config = &context.config; - assert_eq!(res, config.build.dev_path); - } - } -} diff --git a/tauri/src/app/webview.rs b/tauri/src/app/webview.rs deleted file mode 100644 index e6852f6fb..000000000 --- a/tauri/src/app/webview.rs +++ /dev/null @@ -1,303 +0,0 @@ -pub mod wry; - -use crate::plugin::PluginStore; -use std::path::PathBuf; - -use serde_json::Value as JsonValue; - -/// A icon definition. -pub enum Icon { - /// Icon from file path. - File(String), - /// Icon from raw bytes. - Raw(Vec), -} - -/// Messages to dispatch to the application. -pub enum Message { - // webview messages - /// Eval a script on the webview. - EvalScript(String), - // window messages - /// Updates the window resizable flag. - SetResizable(bool), - /// Updates the window title. - SetTitle(String), - /// Maximizes the window. - Maximize, - /// Unmaximizes the window. - Unmaximize, - /// Minimizes the window. - Minimize, - /// Unminimizes the window. - Unminimize, - /// Shows the window. - Show, - /// Hides the window. - Hide, - /// Updates the hasDecorations flag. - SetDecorations(bool), - /// Updates the window alwaysOnTop flag. - SetAlwaysOnTop(bool), - /// Updates the window width. - SetWidth(f64), - /// Updates the window height. - SetHeight(f64), - /// Resizes the window. - Resize { - /// New width. - width: f64, - /// New height. - height: f64, - }, - /// Updates the window min size. - SetMinSize { - /// New value for the window min width. - min_width: f64, - /// New value for the window min height. - min_height: f64, - }, - /// Updates the window max size. - SetMaxSize { - /// New value for the window max width. - max_width: f64, - /// New value for the window max height. - max_height: f64, - }, - /// Updates the X position. - SetX(f64), - /// Updates the Y position. - SetY(f64), - /// Updates the window position. - SetPosition { - /// New value for the window X coordinate. - x: f64, - /// New value for the window Y coordinate. - y: f64, - }, - /// Updates the window fullscreen state. - SetFullscreen(bool), - /// Updates the window icon. - SetIcon(Icon), -} - -pub struct WindowConfig(pub crate::api::config::WindowConfig); - -pub trait WebviewBuilderExtPrivate: Sized { - /// Sets the webview url. - fn url(self, url: String) -> Self; -} - -/// The webview builder. -pub trait WebviewBuilderExt: Sized { - /// The webview object that this builder creates. - type Webview; - - /// Initializes a new webview builder. - fn new() -> Self; - - /// Sets the init script. - fn initialization_script(self, init: &str) -> 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: S) -> 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; - - /// Whether the window should be immediately visible upon creation. - fn visible(self, visible: bool) -> Self; - - /// Whether the the window should be transparent. If this is true, writing colors - /// with alpha values different than `1.0` will produce a transparent window. - fn transparent(self, transparent: bool) -> Self; - - /// Whether the window should have borders and bars. - fn decorations(self, decorations: bool) -> Self; - - /// Whether the window should always be on top of other windows. - fn always_on_top(self, always_on_top: bool) -> Self; - - /// Sets the window icon. - fn icon(self, icon: Icon) -> crate::Result; - - /// Whether the icon was set or not. - fn has_icon(&self) -> bool; - - /// User data path for the webview. Actually only supported on Windows. - fn user_data_path(self, user_data_path: Option) -> Self; - - /// Builds the webview instance. - fn finish(self) -> crate::Result; -} - -/// Rpc request. -pub struct RpcRequest { - /// RPC command. - pub command: String, - /// Params. - pub params: Option, -} - -/// Rpc handler. -pub type WebviewRpcHandler = Box; - -/// Uses a custom handler to resolve file requests -pub struct CustomProtocol { - /// Name of the protocol - pub name: String, - /// Handler for protocol - pub handler: Box crate::Result> + Send>, -} - -/// The file drop event payload. -#[derive(Debug, Clone)] -pub enum FileDropEvent { - /// The file(s) have been dragged onto the window, but have not been dropped yet. - Hovered(Vec), - /// The file(s) have been dropped onto the window. - Dropped(Vec), - /// The file drop was aborted. - Cancelled, -} - -/// File drop handler callback -/// Return `true` in the callback to block the OS' default behavior of handling a file drop.. -pub type FileDropHandler = Box bool + Send>; - -/// Webview dispatcher. A thread-safe handle to the webview API. -pub trait ApplicationDispatcherExt: Clone + Send + Sized { - /// The webview builder type. - type WebviewBuilder: WebviewBuilderExt + WebviewBuilderExtPrivate + From + Send; - /// Creates a webview. - fn create_webview( - &self, - webview_builder: Self::WebviewBuilder, - rpc_handler: Option>, - custom_protocol: Option, - file_drop_handler: Option, - ) -> crate::Result; - - /// Updates the window resizable flag. - fn set_resizable(&self, resizable: bool) -> crate::Result<()>; - - /// Updates the window title. - fn set_title>(&self, title: S) -> crate::Result<()>; - - /// Maximizes the window. - fn maximize(&self) -> crate::Result<()>; - - /// Unmaximizes the window. - fn unmaximize(&self) -> crate::Result<()>; - - /// Minimizes the window. - fn minimize(&self) -> crate::Result<()>; - - /// Unminimizes the window. - fn unminimize(&self) -> crate::Result<()>; - - /// Shows the window. - fn show(&self) -> crate::Result<()>; - - /// Hides the window. - fn hide(&self) -> crate::Result<()>; - - /// Closes the window. - fn close(&self) -> crate::Result<()>; - - /// Updates the hasDecorations flag. - fn set_decorations(&self, decorations: bool) -> crate::Result<()>; - - /// Updates the window alwaysOnTop flag. - fn set_always_on_top(&self, always_on_top: bool) -> crate::Result<()>; - - /// Updates the window width. - fn set_width(&self, width: f64) -> crate::Result<()>; - - /// Updates the window height. - fn set_height(&self, height: f64) -> crate::Result<()>; - - /// Resizes the window. - fn resize(&self, width: f64, height: f64) -> crate::Result<()>; - - /// Updates the window min size. - fn set_min_size(&self, min_width: f64, min_height: f64) -> crate::Result<()>; - - /// Updates the window max size. - fn set_max_size(&self, max_width: f64, max_height: f64) -> crate::Result<()>; - - /// Updates the X position. - fn set_x(&self, x: f64) -> crate::Result<()>; - - /// Updates the Y position. - fn set_y(&self, y: f64) -> crate::Result<()>; - - /// Updates the window position. - fn set_position(&self, x: f64, y: f64) -> crate::Result<()>; - - /// Updates the window fullscreen state. - fn set_fullscreen(&self, fullscreen: bool) -> crate::Result<()>; - - /// Updates the window icon. - fn set_icon(&self, icon: Icon) -> crate::Result<()>; - - /// Evals a script on the webview. - fn eval_script>(&self, script: S) -> crate::Result<()>; -} - -/// The application interface. -/// Manages windows and webviews. -pub trait ApplicationExt: Sized { - /// The webview builder. - type WebviewBuilder: WebviewBuilderExt + WebviewBuilderExtPrivate + From + Send; - /// The message dispatcher. - type Dispatcher: ApplicationDispatcherExt; - - /// Returns the static plugin collection. - fn plugin_store() -> &'static PluginStore; - - /// Creates a new application. - fn new() -> crate::Result; - - /// Creates a new webview. - fn create_webview( - &mut self, - webview_builder: Self::WebviewBuilder, - rpc_handler: Option>, - custom_protocol: Option, - file_drop_handler: Option, - ) -> crate::Result; - - /// Run the application. - fn run(self); -} diff --git a/tauri/src/app/webview_manager.rs b/tauri/src/app/webview_manager.rs deleted file mode 100644 index 175483c6b..000000000 --- a/tauri/src/app/webview_manager.rs +++ /dev/null @@ -1,347 +0,0 @@ -use std::{ - collections::HashMap, - future::Future, - sync::{Arc, Mutex}, -}; - -use super::{ - event::{EventId, EventPayload}, - App, ApplicationDispatcherExt, ApplicationExt, Icon, Webview, WebviewBuilderExt, - WebviewInitializer, -}; -use crate::{api::config::WindowUrl, flavors::Wry}; - -use serde::Serialize; - -/// The webview dispatcher. -#[derive(Clone)] -pub struct WebviewDispatcher { - dispatcher: A, - window_label: String, -} - -impl WebviewDispatcher { - pub(crate) fn new(dispatcher: A, window_label: String) -> Self { - Self { - dispatcher, - window_label, - } - } - - /// The label of the window tied to this dispatcher. - pub fn window_label(&self) -> &str { - &self.window_label - } - - /// Listen to a webview event. - pub fn listen( - &self, - event: impl AsRef, - handler: F, - ) -> EventId { - super::event::listen(event, Some(self.window_label.to_string()), handler) - } - - /// Listen to a webview event and unlisten after the first event. - pub fn once(&self, event: impl AsRef, handler: F) { - super::event::once(event, Some(self.window_label.to_string()), handler) - } - - /// Unregister the event listener with the given id. - pub fn unlisten(&self, event_id: EventId) { - super::event::unlisten(event_id) - } - - /// Emits an event to the webview. - pub fn emit( - &self, - event: impl AsRef, - payload: Option, - ) -> crate::Result<()> { - super::event::emit(&self, event, payload) - } - - /// Emits an event from the webview. - pub(crate) fn on_event(&self, event: String, data: Option) { - super::event::on_event(event, Some(&self.window_label), data) - } - - /// Evaluates a JS script. - pub fn eval(&self, js: &str) -> crate::Result<()> { - self.dispatcher.eval_script(js) - } - - /// Updates the window resizable flag. - pub fn set_resizable(&self, resizable: bool) -> crate::Result<()> { - self.dispatcher.set_resizable(resizable) - } - - /// Updates the window title. - pub fn set_title(&self, title: &str) -> crate::Result<()> { - self.dispatcher.set_title(title.to_string()) - } - - /// Maximizes the window. - pub fn maximize(&self) -> crate::Result<()> { - self.dispatcher.maximize() - } - - /// Unmaximizes the window. - pub fn unmaximize(&self) -> crate::Result<()> { - self.dispatcher.unmaximize() - } - - /// Minimizes the window. - pub fn minimize(&self) -> crate::Result<()> { - self.dispatcher.minimize() - } - - /// Unminimizes the window. - pub fn unminimize(&self) -> crate::Result<()> { - self.dispatcher.unminimize() - } - - /// Sets the window visibility to true. - pub fn show(&self) -> crate::Result<()> { - self.dispatcher.show() - } - - /// Sets the window visibility to false. - pub fn hide(&self) -> crate::Result<()> { - self.dispatcher.hide() - } - - /// Closes the window. - pub fn close(&self) -> crate::Result<()> { - self.dispatcher.close() - } - - /// Whether the window should have borders and bars. - pub fn set_decorations(&self, decorations: bool) -> crate::Result<()> { - self.dispatcher.set_decorations(decorations) - } - - /// Whether the window should always be on top of other windows. - pub fn set_always_on_top(&self, always_on_top: bool) -> crate::Result<()> { - self.dispatcher.set_always_on_top(always_on_top) - } - - /// Sets the window width. - pub fn set_width(&self, width: impl Into) -> crate::Result<()> { - self.dispatcher.set_width(width.into()) - } - - /// Sets the window height. - pub fn set_height(&self, height: impl Into) -> crate::Result<()> { - self.dispatcher.set_height(height.into()) - } - - /// Resizes the window. - pub fn resize(&self, width: impl Into, height: impl Into) -> crate::Result<()> { - self.dispatcher.resize(width.into(), height.into()) - } - - /// Sets the window min size. - pub fn set_min_size( - &self, - min_width: impl Into, - min_height: impl Into, - ) -> crate::Result<()> { - self - .dispatcher - .set_min_size(min_width.into(), min_height.into()) - } - - /// Sets the window max size. - pub fn set_max_size( - &self, - max_width: impl Into, - max_height: impl Into, - ) -> crate::Result<()> { - self - .dispatcher - .set_max_size(max_width.into(), max_height.into()) - } - - /// Sets the window x position. - pub fn set_x(&self, x: impl Into) -> crate::Result<()> { - self.dispatcher.set_x(x.into()) - } - - /// Sets the window y position. - pub fn set_y(&self, y: impl Into) -> crate::Result<()> { - self.dispatcher.set_y(y.into()) - } - - /// Sets the window position. - pub fn set_position(&self, x: impl Into, y: impl Into) -> crate::Result<()> { - self.dispatcher.set_position(x.into(), y.into()) - } - - /// Sets the window fullscreen state. - pub fn set_fullscreen(&self, fullscreen: bool) -> crate::Result<()> { - self.dispatcher.set_fullscreen(fullscreen) - } - - /// Sets the window icon. - pub fn set_icon(&self, icon: Icon) -> crate::Result<()> { - self.dispatcher.set_icon(icon) - } -} - -/// The webview manager. -pub struct WebviewManager -where - A: ApplicationExt, -{ - application: Arc>>, - dispatchers: Arc>>>, - current_webview_window_label: String, -} - -impl Clone for WebviewManager { - fn clone(&self) -> Self { - Self { - application: self.application.clone(), - dispatchers: self.dispatchers.clone(), - current_webview_window_label: self.current_webview_window_label.to_string(), - } - } -} - -impl WebviewManager { - pub(crate) fn new( - application: Arc>>, - dispatchers: Arc>>>, - label: String, - ) -> Self { - Self { - application, - dispatchers, - current_webview_window_label: label, - } - } - - /// Spawns an asynchronous task - pub fn spawn(task: F) - where - F: Future + Send + 'static, - F::Output: Send + 'static, - { - crate::async_runtime::spawn(task) - } - - /// 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> { - 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> { - self - .dispatchers - .lock() - .unwrap() - .get(window_label) - .ok_or(crate::Error::WebviewNotFound) - .map(|d| d.clone()) - } - - /// Creates a new webview. - pub async fn create_webview crate::Result>( - &self, - label: String, - url: WindowUrl, - f: F, - ) -> crate::Result> { - let builder = f(A::WebviewBuilder::new())?; - let webview = Webview { - url, - label: label.to_string(), - builder, - }; - self - .application - .lock() - .unwrap() - .window_labels - .lock() - .unwrap() - .push(label.to_string()); - let (webview_builder, rpc_handler, custom_protocol, file_drop_handler) = - self.application.init_webview(webview)?; - - let window_dispatcher = self.current_webview()?.dispatcher.create_webview( - webview_builder, - rpc_handler, - custom_protocol, - file_drop_handler, - )?; - let webview_manager = Self::new( - self.application.clone(), - self.dispatchers.clone(), - label.to_string(), - ); - self - .application - .on_webview_created(label.to_string(), window_dispatcher.clone()); - crate::plugin::created(A::plugin_store(), &webview_manager); - Ok(WebviewDispatcher::new(window_dispatcher, label)) - } - - /// Listen to a global event. - /// An event from any webview will trigger the handler. - pub fn listen( - &self, - event: impl AsRef, - handler: F, - ) -> EventId { - super::event::listen(event, None, handler) - } - - /// Listen to a global event and unlisten after the first event. - pub fn once(&self, event: impl AsRef, handler: F) { - super::event::once(event, None, handler) - } - - /// Unregister the global event listener with the given id. - pub fn unlisten(&self, event_id: EventId) { - super::event::unlisten(event_id) - } - - /// Emits an event to all webviews. - pub fn emit( - &self, - event: impl AsRef, - payload: Option, - ) -> crate::Result<()> { - for dispatcher in self.dispatchers.lock().unwrap().values() { - super::event::emit(&dispatcher, event.as_ref(), payload.clone())?; - } - Ok(()) - } - - pub(crate) fn emit_except( - &self, - except_label: String, - event: impl AsRef, - payload: Option, - ) -> crate::Result<()> { - for dispatcher in self.dispatchers.lock().unwrap().values() { - if dispatcher.window_label != except_label { - super::event::emit(&dispatcher, event.as_ref(), payload.clone())?; - } - } - Ok(()) - } - - /// Emits a global event from the webview. - pub(crate) fn on_event(&self, event: String, data: Option) { - super::event::on_event(event, None, data) - } -} diff --git a/tauri/src/endpoints.rs b/tauri/src/endpoints.rs index 9978dcc3d..f9aee0884 100644 --- a/tauri/src/endpoints.rs +++ b/tauri/src/endpoints.rs @@ -1,3 +1,7 @@ +use crate::{api::config::Config, hooks::InvokeMessage, runtime::Params}; +use serde::{Deserialize, Serialize}; +use serde_json::Value as JsonValue; + mod cli; mod dialog; mod event; @@ -10,11 +14,6 @@ mod notification; mod shell; mod window; -use crate::{app::Context, ApplicationExt, InvokeMessage}; - -use serde::{Deserialize, Serialize}; -use serde_json::Value as JsonValue; - /// The response for a JS `invoke` call. pub struct InvokeResponse { json: crate::Result, @@ -44,31 +43,27 @@ enum Module { } impl Module { - fn run( - self, - webview_manager: crate::WebviewManager, - message: InvokeMessage, - context: &Context, - ) { + fn run(self, message: InvokeMessage, config: &Config) { + let window = message.window(); match self { Self::Fs(cmd) => message .respond_async(async move { cmd.run().and_then(|r| r.json).map_err(|e| e.to_string()) }), Self::Window(cmd) => message.respond_async(async move { cmd - .run(&webview_manager) + .run(window) .await .and_then(|r| r.json) .map_err(|e| e.to_string()) }), Self::Shell(cmd) => message.respond_async(async move { cmd - .run(webview_manager) + .run(window) .and_then(|r| r.json) .map_err(|e| e.to_string()) }), Self::Event(cmd) => message.respond_async(async move { cmd - .run(&webview_manager) + .run(window) .and_then(|r| r.json) .map_err(|e| e.to_string()) }), @@ -77,7 +72,7 @@ impl Module { Self::Dialog(cmd) => message .respond_async(async move { cmd.run().and_then(|r| r.json).map_err(|e| e.to_string()) }), Self::Cli(cmd) => { - if let Some(cli_config) = context.config.tauri.cli.clone() { + if let Some(cli_config) = config.tauri.cli.clone() { message.respond_async(async move { cmd .run(&cli_config) @@ -87,7 +82,7 @@ impl Module { } } Self::Notification(cmd) => { - let identifier = context.config.tauri.bundle.identifier.clone(); + let identifier = config.tauri.bundle.identifier.clone(); message.respond_async(async move { cmd .run(identifier) @@ -104,7 +99,7 @@ impl Module { }), Self::GlobalShortcut(cmd) => message.respond_async(async move { cmd - .run(&webview_manager) + .run(window) .and_then(|r| r.json) .map_err(|e| e.to_string()) }), @@ -112,18 +107,13 @@ impl Module { } } -pub(crate) fn handle( - webview_manager: &crate::WebviewManager, - module: String, - message: InvokeMessage, - context: &Context, -) { +pub(crate) fn handle(module: String, message: InvokeMessage, config: &Config) { let mut payload = message.payload(); if let JsonValue::Object(ref mut obj) = payload { obj.insert("module".to_string(), JsonValue::String(module)); } match serde_json::from_value::(payload) { - Ok(module) => module.run(webview_manager.clone(), message, context), + Ok(module) => module.run(message, config), Err(e) => message.reject(e.to_string()), } } diff --git a/tauri/src/endpoints/event.rs b/tauri/src/endpoints/event.rs index a3a3144ed..c860f14fb 100644 --- a/tauri/src/endpoints/event.rs +++ b/tauri/src/endpoints/event.rs @@ -1,4 +1,7 @@ -use super::InvokeResponse; +use crate::{ + endpoints::InvokeResponse, + runtime::{sealed::ManagerPrivate, window::Window, Manager, Params}, +}; use serde::Deserialize; /// The API descriptor. @@ -21,22 +24,15 @@ pub enum Cmd { } impl Cmd { - pub fn run( - self, - webview_manager: &crate::WebviewManager, - ) -> crate::Result { + pub fn run(self, window: Window) -> crate::Result { match self { Self::Listen { event, handler } => { let event_id = rand::random(); - webview_manager - .current_webview()? - .eval(&listen_js(event, event_id, handler))?; + window.eval(&listen_js(&window, event, event_id, handler))?; Ok(event_id.into()) } Self::Unlisten { event_id } => { - webview_manager - .current_webview()? - .eval(&unlisten_js(event_id))?; + window.eval(&unlisten_js(&window, event_id))?; Ok(().into()) } Self::Emit { @@ -44,17 +40,22 @@ impl Cmd { window_label, payload, } => { - if let Some(label) = window_label { - let dispatcher = webview_manager.get_webview(&label)?; - // dispatch the event to Rust listeners - dispatcher.on_event(event.to_string(), payload.clone()); - // dispatch the event to JS listeners - dispatcher.emit(event, payload)?; + let e: M::Event = event + .parse() + .unwrap_or_else(|_| panic!("todo: invalid event str")); + + let window_label: Option = window_label.map(|l| { + l.parse() + .unwrap_or_else(|_| panic!("todo: invalid window label")) + }); + + // dispatch the event to Rust listeners + window.trigger(e.clone(), payload.clone()); + + if let Some(target) = window_label { + window.emit_to(&target, e, payload)?; } else { - // dispatch the event to Rust listeners - webview_manager.on_event(event.to_string(), payload.clone()); - // dispatch the event to JS listeners - webview_manager.emit(event, payload)?; + window.emit_all(e, payload)?; } Ok(().into()) } @@ -62,7 +63,7 @@ impl Cmd { } } -pub fn unlisten_js(event_id: u64) -> String { +pub fn unlisten_js(window: &Window, event_id: u64) -> String { format!( " for (var event in (window['{listeners}'] || {{}})) {{ @@ -72,12 +73,17 @@ pub fn unlisten_js(event_id: u64) -> String { }} }} ", - listeners = crate::app::event::event_listeners_object_name(), + listeners = window.manager().event_listeners_object_name(), event_id = event_id, ) } -pub fn listen_js(event: String, event_id: u64, handler: String) -> String { +pub fn listen_js( + window: &Window, + event: String, + event_id: u64, + handler: String, +) -> String { format!( "if (window['{listeners}'] === void 0) {{ window['{listeners}'] = {{}} @@ -95,24 +101,11 @@ pub fn listen_js(event: String, event_id: u64, handler: String) -> String { window['{emit}'](e.eventData, e.salt, true) }} ", - listeners = crate::app::event::event_listeners_object_name(), - queue = crate::app::event::event_queue_object_name(), - emit = crate::app::event::emit_function_name(), + listeners = window.manager().event_listeners_object_name(), + queue = window.manager().event_queue_object_name(), + emit = window.manager().event_emit_function_name(), event = event, event_id = event_id, handler = handler ) } - -#[cfg(test)] -mod test { - use proptest::prelude::*; - - // check the listen_js for various usecases. - proptest! { - #[test] - fn check_listen_js(event in "", id in proptest::bits::u64::ANY, handler in "") { - super::listen_js(event, id, handler); - } - } -} diff --git a/tauri/src/endpoints/file_system.rs b/tauri/src/endpoints/file_system.rs index 43106b881..b6e0b83bb 100644 --- a/tauri/src/endpoints/file_system.rs +++ b/tauri/src/endpoints/file_system.rs @@ -1,5 +1,5 @@ use super::InvokeResponse; -use crate::{api::path::BaseDirectory, ApplicationDispatcherExt}; +use crate::api::path::BaseDirectory; use serde::{Deserialize, Serialize}; use tauri_api::{dir, file, path::resolve_path}; diff --git a/tauri/src/endpoints/global_shortcut.rs b/tauri/src/endpoints/global_shortcut.rs index 635b5c844..f35a5cdab 100644 --- a/tauri/src/endpoints/global_shortcut.rs +++ b/tauri/src/endpoints/global_shortcut.rs @@ -1,12 +1,12 @@ use super::InvokeResponse; -#[cfg(global_shortcut_all)] -use crate::api::shortcuts::ShortcutManager; -use crate::app::WebviewDispatcher; +use crate::runtime::{window::Window, Dispatch, Params}; use once_cell::sync::Lazy; use serde::Deserialize; - use std::sync::{Arc, Mutex}; +#[cfg(global_shortcut_all)] +use crate::api::shortcuts::ShortcutManager; + #[cfg(global_shortcut_all)] type ShortcutManagerHandle = Arc>; @@ -36,8 +36,8 @@ pub enum Cmd { } #[cfg(global_shortcut_all)] -fn register_shortcut( - dispatcher: WebviewDispatcher, +fn register_shortcut( + dispatcher: D, manager: &mut ShortcutManager, shortcut: String, handler: String, @@ -47,33 +47,36 @@ fn register_shortcut( handler.to_string(), serde_json::Value::String(shortcut.clone()), ); - let _ = dispatcher.eval(callback_string.as_str()); + let _ = dispatcher.eval_script(callback_string.as_str()); })?; Ok(()) } +#[cfg(not(global_shortcut_all))] impl Cmd { - pub fn run( - self, - webview_manager: &crate::WebviewManager, - ) -> crate::Result { - #[cfg(not(global_shortcut_all))] - return Err(crate::Error::ApiNotAllowlisted( + pub fn run(self, _window: Window) -> crate::Result { + Err(crate::Error::ApiNotAllowlisted( "globalShortcut > all".to_string(), - )); - #[cfg(global_shortcut_all)] + )) + } +} + +#[cfg(global_shortcut_all)] +impl Cmd { + pub fn run(self, window: Window) -> crate::Result { match self { Self::Register { shortcut, handler } => { - let dispatcher = webview_manager.current_webview()?; + let dispatcher = window.dispatcher(); let mut manager = manager_handle().lock().unwrap(); register_shortcut(dispatcher, &mut manager, shortcut, handler)?; Ok(().into()) } Self::RegisterAll { shortcuts, handler } => { - let dispatcher = webview_manager.current_webview()?; + let dispatcher = window.dispatcher(); let mut manager = manager_handle().lock().unwrap(); for shortcut in shortcuts { - register_shortcut(dispatcher.clone(), &mut manager, shortcut, handler.clone())?; + let dispatch = dispatcher.clone(); + register_shortcut(dispatch, &mut manager, shortcut, handler.clone())?; } Ok(().into()) } diff --git a/tauri/src/endpoints/shell.rs b/tauri/src/endpoints/shell.rs index f8ae32e2b..5c4194a18 100644 --- a/tauri/src/endpoints/shell.rs +++ b/tauri/src/endpoints/shell.rs @@ -1,15 +1,13 @@ -use super::InvokeResponse; use crate::{ api::{ command::{Command, CommandChild, CommandEvent}, rpc::format_callback, }, - app::ApplicationExt, + endpoints::InvokeResponse, + runtime::{window::Window, Params}, }; - use once_cell::sync::Lazy; use serde::Deserialize; - use std::{ collections::HashMap, sync::{Arc, Mutex}, @@ -57,10 +55,7 @@ pub enum Cmd { } impl Cmd { - pub fn run( - self, - webview_manager: crate::WebviewManager, - ) -> crate::Result { + pub fn run(self, window: Window) -> crate::Result { match self { Self::Execute { program, @@ -87,9 +82,7 @@ impl Cmd { command_childs().lock().unwrap().remove(&pid); } let js = format_callback(on_event_fn.clone(), serde_json::to_value(event).unwrap()); - if let Ok(dispatcher) = webview_manager.current_webview() { - let _ = dispatcher.eval(js.as_str()); - } + let _ = window.eval(js.as_str()); } }); diff --git a/tauri/src/endpoints/window.rs b/tauri/src/endpoints/window.rs index c9ab70c96..420648900 100644 --- a/tauri/src/endpoints/window.rs +++ b/tauri/src/endpoints/window.rs @@ -1,11 +1,19 @@ -use super::InvokeResponse; -use crate::app::{ApplicationExt, Icon}; +use crate::{ + endpoints::InvokeResponse, + runtime::{ + webview::{Icon, WindowConfig}, + window::{PendingWindow, Window}, + Manager, Params, + }, +}; use serde::Deserialize; +use std::path::PathBuf; + #[derive(Deserialize)] #[serde(untagged)] pub enum IconDto { - File(String), + File(PathBuf), Raw(Vec), } @@ -90,14 +98,10 @@ struct WindowCreatedEvent { } impl Cmd { - pub async fn run( - self, - webview_manager: &crate::WebviewManager, - ) -> crate::Result { + pub async fn run(self, mut window: Window) -> crate::Result { if cfg!(not(window_all)) { Err(crate::Error::ApiNotAllowlisted("window > all".to_string())) } else { - let current_webview = webview_manager.current_webview()?; match self { Self::CreateWebview { options } => { #[cfg(not(window_create))] @@ -106,48 +110,48 @@ impl Cmd { )); #[cfg(window_create)] { - let label = options.label.to_string(); - webview_manager - .create_webview(label.to_string(), options.url.clone(), |_| { - Ok(crate::app::webview::WindowConfig(options).into()) - }) - .await?; - webview_manager.emit_except( - label.to_string(), - "tauri://window-created", - Some(WindowCreatedEvent { label }), + let label: M::Label = options + .label + .parse() + .unwrap_or_else(|_| panic!("todo: label parsing")); + + let url = options.url.clone(); + let pending = PendingWindow::new(WindowConfig(options), label.clone(), url); + window.create_window(pending)?.emit_others_internal( + "tauri://window-created".to_string(), + Some(WindowCreatedEvent { + label: label.to_string(), + }), )?; } } - Self::SetResizable { resizable } => current_webview.set_resizable(resizable)?, - Self::SetTitle { title } => current_webview.set_title(&title)?, - Self::Maximize => current_webview.maximize()?, - Self::Unmaximize => current_webview.unmaximize()?, - Self::Minimize => current_webview.minimize()?, - Self::Unminimize => current_webview.unminimize()?, - Self::Show => current_webview.show()?, - Self::Hide => current_webview.hide()?, - Self::Close => current_webview.close()?, - Self::SetDecorations { decorations } => current_webview.set_decorations(decorations)?, - Self::SetAlwaysOnTop { always_on_top } => { - current_webview.set_always_on_top(always_on_top)? - } - Self::SetWidth { width } => current_webview.set_width(width)?, - Self::SetHeight { height } => current_webview.set_height(height)?, - Self::Resize { width, height } => current_webview.resize(width, height)?, + Self::SetResizable { resizable } => window.set_resizable(resizable)?, + Self::SetTitle { title } => window.set_title(&title)?, + Self::Maximize => window.maximize()?, + Self::Unmaximize => window.unmaximize()?, + Self::Minimize => window.minimize()?, + Self::Unminimize => window.unminimize()?, + Self::Show => window.show()?, + Self::Hide => window.hide()?, + Self::Close => window.close()?, + Self::SetDecorations { decorations } => window.set_decorations(decorations)?, + Self::SetAlwaysOnTop { always_on_top } => window.set_always_on_top(always_on_top)?, + Self::SetWidth { width } => window.set_width(width)?, + Self::SetHeight { height } => window.set_height(height)?, + Self::Resize { width, height } => window.resize(width, height)?, Self::SetMinSize { min_width, min_height, - } => current_webview.set_min_size(min_width, min_height)?, + } => window.set_min_size(min_width, min_height)?, Self::SetMaxSize { max_width, max_height, - } => current_webview.set_max_size(max_width, max_height)?, - Self::SetX { x } => current_webview.set_x(x)?, - Self::SetY { y } => current_webview.set_y(y)?, - Self::SetPosition { x, y } => current_webview.set_position(x, y)?, - Self::SetFullscreen { fullscreen } => current_webview.set_fullscreen(fullscreen)?, - Self::SetIcon { icon } => current_webview.set_icon(icon.into())?, + } => window.set_max_size(max_width, max_height)?, + Self::SetX { x } => window.set_x(x)?, + Self::SetY { y } => window.set_y(y)?, + Self::SetPosition { x, y } => window.set_position(x, y)?, + Self::SetFullscreen { fullscreen } => window.set_fullscreen(fullscreen)?, + Self::SetIcon { icon } => window.set_icon(icon.into())?, } Ok(().into()) } diff --git a/tauri/src/error.rs b/tauri/src/error.rs index 57921f236..8afc34dec 100644 --- a/tauri/src/error.rs +++ b/tauri/src/error.rs @@ -46,6 +46,9 @@ pub enum Error { /// Invalid args when running a command. #[error("invalid args for command `{0}`: {1}")] InvalidArgs(&'static str, serde_json::Error), + /// Encountered an error in the setup hook, + #[error("error encountered during setup hood: {0}")] + Setup(#[from] Box), } impl From for Error { diff --git a/tauri/src/event.rs b/tauri/src/event.rs new file mode 100644 index 000000000..325bd0a84 --- /dev/null +++ b/tauri/src/event.rs @@ -0,0 +1,219 @@ +use crate::runtime::tag::Tag; +use std::{ + boxed::Box, + collections::HashMap, + fmt, + hash::Hash, + sync::{Arc, Mutex}, +}; +use uuid::Uuid; + +/// Represents an event handler. +#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct EventHandler(Uuid); + +impl fmt::Display for EventHandler { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +/// An event that was triggered. +#[derive(Debug, Clone)] +pub struct Event { + id: EventHandler, + data: Option, +} + +impl Event { + /// The [`EventHandler`] that was triggered. + pub fn id(&self) -> EventHandler { + self.id + } + + /// The event payload. + pub fn payload(&self) -> Option<&str> { + self.data.as_deref() + } +} + +/// Stored in [`Listeners`] to be called upon when the event that stored it is triggered. +struct Handler { + window: Option, + callback: Box, +} + +/// A collection of handlers. Multiple handlers can represent the same event. +type Handlers = HashMap>>; + +#[derive(Clone)] +pub(crate) struct Listeners { + inner: Arc>>, + function_name: Uuid, + listeners_object_name: Uuid, + queue_object_name: Uuid, +} + +impl Default for Listeners { + fn default() -> Self { + Self { + inner: Arc::new(Mutex::default()), + function_name: Uuid::new_v4(), + listeners_object_name: Uuid::new_v4(), + queue_object_name: Uuid::new_v4(), + } + } +} + +impl Listeners { + /// Randomly generated function name to represent the JavaScript event function. + pub(crate) fn function_name(&self) -> String { + self.function_name.to_string() + } + + /// Randomly generated listener object name to represent the JavaScript event listener object. + pub(crate) fn listeners_object_name(&self) -> String { + self.function_name.to_string() + } + + /// Randomly generated queue object name to represent the JavaScript event queue object. + pub(crate) fn queue_object_name(&self) -> String { + self.queue_object_name.to_string() + } + + /// Adds an event listener for JS events. + pub(crate) fn listen( + &self, + event: E, + window: Option, + handler: F, + ) -> EventHandler { + let id = EventHandler(Uuid::new_v4()); + let handler = Handler { + window, + callback: Box::new(handler), + }; + self + .inner + .lock() + .expect("poisoned event mutex") + .entry(event) + .or_default() + .insert(id, handler); + id + } + + /// Listen to a JS event and immediately unlisten. + pub(crate) fn once( + &self, + event: E, + window: Option, + handler: F, + ) { + let self_ = self.clone(); + self.listen(event, window, move |e| { + self_.unlisten(e.id); + handler(e); + }); + } + + /// Removes an event listener. + pub(crate) fn unlisten(&self, handler_id: EventHandler) { + self + .inner + .lock() + .expect("poisoned event mutex") + .values_mut() + .for_each(|handler| { + handler.remove(&handler_id); + }) + } + + /// Triggers the given global event with its payload. + pub(crate) fn trigger(&self, event: E, window: Option, data: Option) { + if let Some(handlers) = self.inner.lock().expect("poisoned event mutex").get(&event) { + for (&id, handler) in handlers { + if window.is_none() || window == handler.window { + let data = data.clone(); + let payload = Event { id, data }; + (handler.callback)(payload) + } + } + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use proptest::prelude::*; + + // dummy event handler function + fn event_fn(s: Event) { + println!("{:?}", s); + } + + proptest! { + #![proptest_config(ProptestConfig::with_cases(10000))] + + // check to see if listen() is properly passing keys into the LISTENERS map + #[test] + fn listeners_check_key(e in "[a-z]+") { + let listeners: Listeners = Default::default(); + // clone e as the key + let key = e.clone(); + // pass e and an dummy func into listen + listeners.listen(e, None, event_fn); + + // lock mutex + let l = listeners.inner.lock().unwrap(); + + // check if the generated key is in the map + assert_eq!(l.contains_key(&key), true); + } + + // check to see if listen inputs a handler function properly into the LISTENERS map. + #[test] + fn listeners_check_fn(e in "[a-z]+") { + let listeners: Listeners = Default::default(); + // clone e as the key + let key = e.clone(); + // pass e and an dummy func into listen + listeners.listen(e, None, event_fn); + + // lock mutex + let mut l = listeners.inner.lock().unwrap(); + + // check if l contains key + if l.contains_key(&key) { + // grab key if it exists + let handler = l.get_mut(&key); + // check to see if we get back a handler or not + match handler { + // pass on Some(handler) + Some(_) => {}, + // Fail on None + None => panic!("handler is None") + } + } + } + + // check to see if on_event properly grabs the stored function from listen. + #[test] + fn check_on_event(e in "[a-z]+", d in "[a-z]+") { + let listeners: Listeners = Default::default(); + // clone e as the key + let key = e.clone(); + // call listen with e and the event_fn dummy func + listeners.listen(e.clone(), None, event_fn); + // call on event with e and d. + listeners.trigger(e, None, Some(d)); + + // lock the mutex + let l = listeners.inner.lock().unwrap(); + + // assert that the key is contained in the listeners map + assert!(l.contains_key(&key)); + } + } +} diff --git a/tauri/src/hooks.rs b/tauri/src/hooks.rs new file mode 100644 index 000000000..551e052ca --- /dev/null +++ b/tauri/src/hooks.rs @@ -0,0 +1,164 @@ +use crate::{ + api::rpc::{format_callback, format_callback_result}, + runtime::{app::App, window::Window, Params}, +}; +use serde::{Deserialize, Serialize}; +use std::future::Future; + +/// A closure that is run when the Tauri application is setting up. +pub type SetupHook = Box) -> Result<(), Box> + Send>; + +/// A closure that is run everytime Tauri receives a message it doesn't explicitly handle. +pub type InvokeHandler = dyn Fn(InvokeMessage) + Send + Sync + 'static; + +/// A closure that is run once every time a window is created and loaded. +pub type OnPageLoad = dyn Fn(Window, PageLoadPayload) + Send + Sync + 'static; + +/// The payload for the [`OnPageLoad`] hook. +#[derive(Debug, Clone, Deserialize)] +pub struct PageLoadPayload { + url: String, +} + +impl PageLoadPayload { + /// The page URL. + pub fn url(&self) -> &str { + &self.url + } +} + +/// Payload from an invoke call. +#[derive(Debug, Deserialize)] +pub(crate) struct InvokePayload { + #[serde(rename = "__tauriModule")] + pub(crate) tauri_module: Option, + pub(crate) callback: String, + pub(crate) error: String, + #[serde(rename = "mainThread", default)] + pub(crate) main_thread: bool, + #[serde(flatten)] + pub(crate) inner: serde_json::Value, +} + +/// An invoke message. +pub struct InvokeMessage { + window: Window, + command: String, + + /// Allow our crate to access the payload without cloning it. + pub(crate) payload: InvokePayload, +} + +impl InvokeMessage { + /// Create an new [`InvokeMessage`] from a payload send to a window. + pub(crate) fn new(window: Window, command: String, payload: InvokePayload) -> Self { + Self { + window, + command, + payload, + } + } + + /// The invoke command. + pub fn command(&self) -> &str { + &self.command + } + + /// The invoke payload. + pub fn payload(&self) -> serde_json::Value { + self.payload.inner.clone() + } + + /// The window that received the invoke. + pub fn window(&self) -> Window { + self.window.clone() + } + + /// Reply to the invoke promise with an async task. + pub fn respond_async< + T: Serialize, + Err: Serialize, + F: Future> + Send + 'static, + >( + self, + task: F, + ) { + if self.payload.main_thread { + crate::async_runtime::block_on(async move { + Self::return_task(self.window, task, self.payload.callback, self.payload.error).await; + }); + } else { + crate::async_runtime::spawn(async move { + Self::return_task(self.window, task, self.payload.callback, self.payload.error).await; + }); + } + } + + /// Reply to the invoke promise running the given closure. + pub fn respond_closure Result>(self, f: F) { + Self::return_closure(self.window, f, self.payload.callback, self.payload.error) + } + + /// Resolve the invoke promise with a value. + pub fn resolve(self, value: S) { + Self::return_result( + self.window, + Result::::Ok(value), + self.payload.callback, + self.payload.error, + ) + } + + /// Reject the invoke promise with a value. + pub fn reject(self, value: S) { + Self::return_result( + self.window, + Result::<(), S>::Err(value), + self.payload.callback, + self.payload.error, + ) + } + + /// Asynchronously executes the given task + /// and evaluates its Result to the JS promise described by the `success_callback` and `error_callback` function names. + /// + /// If the Result `is_ok()`, the callback will be the `success_callback` function name and the argument will be the Ok value. + /// If the Result `is_err()`, the callback will be the `error_callback` function name and the argument will be the Err value. + pub async fn return_task< + T: Serialize, + Err: Serialize, + F: std::future::Future> + Send + 'static, + >( + window: Window, + task: F, + success_callback: String, + error_callback: String, + ) { + let result = task.await; + Self::return_closure(window, || result, success_callback, error_callback) + } + + pub(crate) fn return_closure Result>( + window: Window, + f: F, + success_callback: String, + error_callback: String, + ) { + Self::return_result(window, f(), success_callback, error_callback) + } + + pub(crate) fn return_result( + window: Window, + result: Result, + success_callback: String, + error_callback: String, + ) { + let callback_string = + match format_callback_result(result, success_callback, error_callback.clone()) { + Ok(callback_string) => callback_string, + Err(e) => format_callback(error_callback, e.to_string()), + }; + + let _ = window.eval(&callback_string); + } +} diff --git a/tauri/src/lib.rs b/tauri/src/lib.rs index 3961dc66f..a536829a2 100644 --- a/tauri/src/lib.rs +++ b/tauri/src/lib.rs @@ -6,37 +6,41 @@ //! 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 Tauri-specific settings for your app e.g. notification permission status. +/// The Tauri error enum. +pub use error::Error; +pub use tauri_api as api; +pub(crate) use tauri_api::private::async_runtime; +pub use tauri_macros::*; + +/// The Tauri-specific settings for your runtime e.g. notification permission status. pub mod settings; -/// The webview application entry. -mod app; /// The Tauri API endpoints. mod endpoints; mod error; +mod event; +mod hooks; /// The plugin manager module contains helpers to manage runtime plugins. pub mod plugin; +/// The internal runtime between an [`App`] and the webview. +pub mod runtime; /// The salt helpers. mod salt; -/// The Tauri error enum. -pub use error::Error; - /// Tauri result type. pub type Result = std::result::Result; /// A task to run on the main thread. pub type SyncTask = Box; -pub use app::*; -pub use tauri_api as api; -pub(crate) use tauri_api::private::async_runtime; -pub use tauri_macros::*; - -/// The Tauri webview implementations. -pub mod flavors { - pub use super::app::WryApplication as Wry; -} +/// types likely to be used by applications +pub use { + api::config::WindowUrl, + hooks::InvokeMessage, + runtime::app::AppBuilder, + runtime::webview::Attributes, + runtime::{Context, Manager, Params}, +}; /// Easy helper function to use the Tauri Context you made during build time. #[macro_export] diff --git a/tauri/src/plugin.rs b/tauri/src/plugin.rs index 913302a4e..8f0c23211 100644 --- a/tauri/src/plugin.rs +++ b/tauri/src/plugin.rs @@ -1,17 +1,19 @@ use crate::{ - api::config::PluginConfig, ApplicationExt, InvokeMessage, PageLoadPayload, WebviewManager, + api::config::PluginConfig, + hooks::{InvokeMessage, PageLoadPayload}, + runtime::{window::Window, Params}, }; - -use std::sync::{Arc, Mutex}; +use serde_json::Value as JsonValue; +use std::collections::HashMap; /// The plugin interface. -pub trait Plugin: Send { +pub trait Plugin: Send { /// The plugin name. Used as key on the plugin config object. fn name(&self) -> &'static str; /// Initialize the plugin. #[allow(unused_variables)] - fn initialize(&mut self, config: String) -> crate::Result<()> { + fn initialize(&mut self, config: JsonValue) -> crate::Result<()> { Ok(()) } @@ -26,94 +28,82 @@ pub trait Plugin: Send { /// Callback invoked when the webview is created. #[allow(unused_variables)] - fn created(&mut self, webview_manager: WebviewManager) {} + fn created(&mut self, window: Window) {} /// Callback invoked when the webview performs a navigation. #[allow(unused_variables)] - fn on_page_load(&mut self, webview_manager: WebviewManager, payload: PageLoadPayload) {} + fn on_page_load(&mut self, window: Window, payload: PageLoadPayload) {} /// Add invoke_handler API extension commands. #[allow(unused_variables)] - fn extend_api(&mut self, webview_manager: WebviewManager, message: InvokeMessage) {} + fn extend_api(&mut self, message: InvokeMessage) {} } /// Plugin collection type. -pub type PluginStore = Arc + Send>>>>; - -/// Registers a plugin. -pub fn register( - store: &PluginStore, - plugin: impl Plugin + Send + 'static, -) { - let mut plugins = store.lock().unwrap(); - plugins.push(Box::new(plugin)); +pub struct PluginStore { + store: HashMap<&'static str, Box>>, } -pub(crate) fn initialize( - store: &PluginStore, - plugins_config: PluginConfig, -) -> crate::Result<()> { - let mut plugins = store.lock().unwrap(); - for plugin in plugins.iter_mut() { - let plugin_config = plugins_config.get(plugin.name()); - plugin.initialize(plugin_config)?; - } - - Ok(()) -} - -pub(crate) fn initialization_script(store: &PluginStore) -> String { - let mut plugins = store.lock().unwrap(); - let mut initialization_script = String::new(); - for plugin in plugins.iter_mut() { - if let Some(plugin_initialization_script) = plugin.initialization_script() { - initialization_script.push_str(&format!( - "(function () {{ {} }})();", - plugin_initialization_script - )); - } - } - initialization_script -} - -pub(crate) fn created( - store: &PluginStore, - webview_manager: &crate::WebviewManager, -) { - let mut plugins = store.lock().unwrap(); - for plugin in plugins.iter_mut() { - plugin.created(webview_manager.clone()); - } -} - -pub(crate) fn on_page_load( - store: &PluginStore, - webview_manager: &crate::WebviewManager, - payload: PageLoadPayload, -) { - let mut plugins = store.lock().unwrap(); - for plugin in plugins.iter_mut() { - plugin.on_page_load(webview_manager.clone(), payload.clone()); - } -} - -pub(crate) fn extend_api( - store: &PluginStore, - webview_manager: &crate::WebviewManager, - command: String, - message: InvokeMessage, -) { - let mut plugins = store.lock().unwrap(); - let target_plugin_name = command - .replace("plugin:", "") - .split('|') - .next() - .unwrap() - .to_string(); - for plugin in plugins.iter_mut() { - if plugin.name() == target_plugin_name { - plugin.extend_api(webview_manager.clone(), message); - break; +impl Default for PluginStore { + fn default() -> Self { + Self { + store: HashMap::new(), + } + } +} + +impl PluginStore { + /// Adds a plugin to the store. + /// + /// Returns `true` if a plugin with the same name is already in the store. + pub fn register + 'static>(&mut self, plugin: P) -> bool { + self.store.insert(plugin.name(), Box::new(plugin)).is_some() + } + + /// Initializes all plugins in the store. + pub(crate) fn initialize(&mut self, config: &PluginConfig) -> crate::Result<()> { + self.store.values_mut().try_for_each(|plugin| { + plugin.initialize(config.0.get(plugin.name()).cloned().unwrap_or_default()) + }) + } + + /// Generates an initialization script from all plugins in the store. + pub(crate) fn initialization_script(&self) -> String { + self + .store + .values() + .filter_map(|p| p.initialization_script()) + .fold(String::new(), |acc, script| { + format!("{}\n(function () {{ {} }})();", acc, script) + }) + } + + /// Runs the created hook for all plugins in the store. + pub(crate) fn created(&mut self, window: Window) { + self + .store + .values_mut() + .for_each(|plugin| plugin.created(window.clone())) + } + + /// Runs the on_page_load hook for all plugins in the store. + pub(crate) fn on_page_load(&mut self, window: Window, payload: PageLoadPayload) { + self + .store + .values_mut() + .for_each(|plugin| plugin.on_page_load(window.clone(), payload.clone())) + } + + pub(crate) fn extend_api(&mut self, command: String, message: InvokeMessage) { + let target = command + .replace("plugin:", "") + .split('|') + .next() + .expect("target plugin name empty") + .to_string(); + + if let Some(plugin) = self.store.get_mut(target.as_str()) { + plugin.extend_api(message); } } } diff --git a/tauri/src/runtime/app.rs b/tauri/src/runtime/app.rs new file mode 100644 index 000000000..17293f4c7 --- /dev/null +++ b/tauri/src/runtime/app.rs @@ -0,0 +1,185 @@ +use crate::{ + api::{assets::Assets, config::WindowUrl}, + hooks::{InvokeHandler, InvokeMessage, OnPageLoad, PageLoadPayload, SetupHook}, + plugin::{Plugin, PluginStore}, + runtime::{ + flavor::wry::Wry, + manager::WindowManager, + sealed::ManagerPrivate, + tag::Tag, + webview::{Attributes, WindowConfig}, + window::{PendingWindow, Window}, + Context, Dispatch, Manager, Params, Runtime, RuntimeOrDispatch, + }, +}; + +/// A handle to the currently running application. +pub struct App { + runtime: M::Runtime, + manager: M, +} + +impl Manager for App {} +impl ManagerPrivate for App { + fn manager(&self) -> &M { + &self.manager + } + + fn runtime(&mut self) -> RuntimeOrDispatch<'_, M> { + RuntimeOrDispatch::Runtime(&mut self.runtime) + } +} + +#[allow(missing_docs)] +pub struct Runner { + pending_windows: Vec>, + manager: M, + setup: SetupHook, +} + +impl Runner { + /// Consume and run the [`Application`] until it is finished. + pub fn run(mut self) -> crate::Result<()> { + // set up all the windows defined in the config + for config in self.manager.config().tauri.windows.clone() { + let url = config.url.clone(); + let label = config + .label + .parse() + .unwrap_or_else(|_| panic!("bad label: {}", config.label)); + + self + .pending_windows + .push(PendingWindow::new(WindowConfig(config), label, url)); + } + + self.manager.initialize_plugins()?; + let labels = self + .pending_windows + .iter() + .map(|p| p.label.clone()) + .collect::>(); + + let mut app = App { + runtime: M::Runtime::new()?, + manager: self.manager, + }; + + let pending_windows = self.pending_windows; + for pending in pending_windows { + let pending = app.manager.prepare_window(pending, &labels)?; + let detached = app.runtime.create_window(pending)?; + app.manager.attach_window(detached); + } + + (self.setup)(&mut app)?; + app.runtime.run(); + Ok(()) + } +} + +/// The App builder. +pub struct AppBuilder +where + E: Tag, + L: Tag, + A: Assets, + R: Runtime, +{ + /// The JS message handler. + invoke_handler: Box>>, + + /// The setup hook. + setup: SetupHook>, + + /// Page load hook. + on_page_load: Box>>, + + /// windows to create when starting up. + pending_windows: Vec>>, + + /// All passed plugins + plugins: PluginStore>, +} + +impl AppBuilder +where + E: Tag, + L: Tag, + A: Assets, + R: Runtime, +{ + /// Creates a new App builder. + pub fn new() -> Self { + Self { + setup: Box::new(|_| Ok(())), + invoke_handler: Box::new(|_| ()), + on_page_load: Box::new(|_, _| ()), + pending_windows: Default::default(), + plugins: PluginStore::default(), + } + } + + /// Defines the JS message handler callback. + pub fn invoke_handler(mut self, invoke_handler: F) -> Self + where + F: Fn(InvokeMessage>) + Send + Sync + 'static, + { + self.invoke_handler = Box::new(invoke_handler); + self + } + + /// Defines the setup hook. + pub fn setup(mut self, setup: F) -> Self + where + F: Fn(&mut App>) -> Result<(), Box> + + Send + + 'static, + { + self.setup = Box::new(setup); + self + } + + /// Defines the page load hook. + pub fn on_page_load(mut self, on_page_load: F) -> Self + where + F: Fn(Window>, PageLoadPayload) + Send + Sync + 'static, + { + self.on_page_load = Box::new(on_page_load); + self + } + + /// Adds a plugin to the runtime. + pub fn plugin> + 'static>(mut self, plugin: P) -> Self { + self.plugins.register(plugin); + self + } + + /// Creates a new webview. + pub fn create_window(mut self, label: L, url: WindowUrl, setup: F) -> Self + where + F: FnOnce(::Attributes) -> ::Attributes, + { + let attributes = setup(::Attributes::new()); + self + .pending_windows + .push(PendingWindow::new(attributes, label, url)); + self + } + + /// Builds the [`App`] and the underlying [`Runtime`]. + pub fn build(self, context: Context) -> Runner> { + Runner { + pending_windows: self.pending_windows, + setup: self.setup, + manager: WindowManager::with_handlers(context, self.invoke_handler, self.on_page_load), + } + } +} + +/// Make `Wry` the default `ApplicationExt` for `AppBuilder` +impl Default for AppBuilder { + fn default() -> Self { + Self::new() + } +} diff --git a/tauri/src/runtime/flavor/mod.rs b/tauri/src/runtime/flavor/mod.rs new file mode 100644 index 000000000..4ccc720f9 --- /dev/null +++ b/tauri/src/runtime/flavor/mod.rs @@ -0,0 +1,3 @@ +//! Officially supported webview runtimes. + +pub mod wry; diff --git a/tauri/src/app/webview/wry.rs b/tauri/src/runtime/flavor/wry.rs similarity index 65% rename from tauri/src/app/webview/wry.rs rename to tauri/src/runtime/flavor/wry.rs index 886d6fdd6..8b53fb942 100644 --- a/tauri/src/app/webview/wry.rs +++ b/tauri/src/runtime/flavor/wry.rs @@ -1,22 +1,24 @@ -use super::{ - ApplicationDispatcherExt, ApplicationExt, CustomProtocol, FileDropEvent, FileDropHandler, Icon, - RpcRequest, WebviewBuilderExt, WebviewBuilderExtPrivate, WebviewRpcHandler, WindowConfig, -}; +//! The [`wry`] webview runtime. -use crate::plugin::PluginStore; -use once_cell::sync::Lazy; +use crate::runtime::{ + webview::{ + Attributes, AttributesPrivate, CustomProtocol, FileDropEvent, FileDropHandler, Icon, + RpcRequest, WebviewRpcHandler, WindowConfig, + }, + window::{DetachedWindow, PendingWindow}, + Dispatch, Params, Runtime, +}; +use std::{convert::TryFrom, path::PathBuf}; #[cfg(target_os = "windows")] use std::fs::create_dir_all; #[cfg(target_os = "windows")] use tauri_api::path::{resolve_path, BaseDirectory}; -use std::{ - convert::{TryFrom, TryInto}, - path::PathBuf, -}; +/// Wraps a Tauri icon into a format [`wry`] expects the icon to be in. +pub struct WryIcon(wry::Icon); -impl TryFrom for wry::Icon { +impl TryFrom for WryIcon { type Error = crate::Error; fn try_from(icon: Icon) -> Result { let icon = match icon { @@ -27,11 +29,11 @@ impl TryFrom for wry::Icon { wry::Icon::from_bytes(raw).map_err(|e| crate::Error::InvalidIcon(e.to_string()))? } }; - Ok(icon) + Ok(Self(icon)) } } -impl WebviewBuilderExtPrivate for wry::Attributes { +impl AttributesPrivate for wry::Attributes { fn url(mut self, url: String) -> Self { self.url.replace(url); self @@ -98,9 +100,8 @@ impl From for wry::Attributes { } /// The webview builder. -impl WebviewBuilderExt for wry::Attributes { - /// The webview object that this builder creates. - type Webview = Self; +impl Attributes for wry::Attributes { + type Icon = WryIcon; fn new() -> Self { Default::default() @@ -191,9 +192,9 @@ impl WebviewBuilderExt for wry::Attributes { self } - fn icon(mut self, icon: Icon) -> crate::Result { - self.icon = Some(icon.try_into()?); - Ok(self) + fn icon(mut self, icon: Self::Icon) -> Self { + self.icon = Some(icon.0); + self } fn has_icon(&self) -> bool { @@ -205,8 +206,8 @@ impl WebviewBuilderExt for wry::Attributes { self } - fn finish(self) -> crate::Result { - Ok(self) + fn build(self) -> Self { + self } } @@ -229,270 +230,316 @@ impl From for FileDropEvent { } } +/// A dispatcher for a [`wry`] runtime. #[derive(Clone)] -pub struct WryDispatcher(wry::WindowProxy, wry::ApplicationProxy); +pub struct WryDispatcher { + window: wry::WindowProxy, + application: wry::ApplicationProxy, +} -impl ApplicationDispatcherExt for WryDispatcher { - type WebviewBuilder = wry::Attributes; +impl Dispatch for WryDispatcher { + type Runtime = Wry; + type Icon = WryIcon; + type Attributes = wry::Attributes; - fn create_webview( - &self, - attributes: Self::WebviewBuilder, - rpc_handler: Option>, - custom_protocol: Option, - file_drop_handler: Option, - ) -> crate::Result { - let app_dispatcher = self.1.clone(); + fn create_window>( + &mut self, + pending: PendingWindow, + ) -> crate::Result> { + let PendingWindow { + attributes, + rpc_handler, + custom_protocol, + file_drop_handler, + label, + .. + } = pending; - let wry_rpc_handler = Box::new( - move |dispatcher: wry::WindowProxy, request: wry::RpcRequest| { - if let Some(handler) = &rpc_handler { - handler( - WryDispatcher(dispatcher, app_dispatcher.clone()), - request.into(), - ); - } - None - }, - ); + let proxy = self.application.clone(); - let file_drop_handler = Box::new(move |event: wry::FileDropEvent| { - if let Some(handler) = &file_drop_handler { - handler(event.into()) - } else { - false - } - }); + let rpc_handler = + rpc_handler.map(|handler| create_rpc_handler(proxy.clone(), label.clone(), handler)); - let window_dispatcher = self - .1 + let file_drop_handler = file_drop_handler + .map(|handler| create_file_drop_handler(proxy.clone(), label.clone(), handler)); + + let window = self + .application .add_window_with_configs( attributes, - Some(wry_rpc_handler), - custom_protocol.map(|p| wry::CustomProtocol { - name: p.name.clone(), - handler: Box::new(move |a| (*p.handler)(a).map_err(|_| wry::Error::InitScriptError)), - }), - Some(file_drop_handler), + rpc_handler, + custom_protocol.map(create_custom_protocol), + file_drop_handler, ) - .map_err(|_| crate::Error::FailedToSendMessage)?; - Ok(Self(window_dispatcher, self.1.clone())) + .map_err(|_| crate::Error::CreateWebview)?; + + let dispatcher = WryDispatcher { + window, + application: proxy, + }; + + Ok(DetachedWindow { label, dispatcher }) } fn set_resizable(&self, resizable: bool) -> crate::Result<()> { self - .0 + .window .set_resizable(resizable) .map_err(|_| crate::Error::FailedToSendMessage) } fn set_title>(&self, title: S) -> crate::Result<()> { self - .0 + .window .set_title(title) .map_err(|_| crate::Error::FailedToSendMessage) } fn maximize(&self) -> crate::Result<()> { self - .0 + .window .maximize() .map_err(|_| crate::Error::FailedToSendMessage) } fn unmaximize(&self) -> crate::Result<()> { self - .0 + .window .unmaximize() .map_err(|_| crate::Error::FailedToSendMessage) } fn minimize(&self) -> crate::Result<()> { self - .0 + .window .minimize() .map_err(|_| crate::Error::FailedToSendMessage) } fn unminimize(&self) -> crate::Result<()> { self - .0 + .window .unminimize() .map_err(|_| crate::Error::FailedToSendMessage) } fn show(&self) -> crate::Result<()> { - self.0.show().map_err(|_| crate::Error::FailedToSendMessage) + self + .window + .show() + .map_err(|_| crate::Error::FailedToSendMessage) } fn hide(&self) -> crate::Result<()> { - self.0.hide().map_err(|_| crate::Error::FailedToSendMessage) + self + .window + .hide() + .map_err(|_| crate::Error::FailedToSendMessage) } fn close(&self) -> crate::Result<()> { self - .0 + .window .close() .map_err(|_| crate::Error::FailedToSendMessage) } fn set_decorations(&self, decorations: bool) -> crate::Result<()> { self - .0 + .window .set_decorations(decorations) .map_err(|_| crate::Error::FailedToSendMessage) } fn set_always_on_top(&self, always_on_top: bool) -> crate::Result<()> { self - .0 + .window .set_always_on_top(always_on_top) .map_err(|_| crate::Error::FailedToSendMessage) } fn set_width(&self, width: f64) -> crate::Result<()> { self - .0 + .window .set_width(width) .map_err(|_| crate::Error::FailedToSendMessage) } fn set_height(&self, height: f64) -> crate::Result<()> { self - .0 + .window .set_height(height) .map_err(|_| crate::Error::FailedToSendMessage) } fn resize(&self, width: f64, height: f64) -> crate::Result<()> { self - .0 + .window .resize(width, height) .map_err(|_| crate::Error::FailedToSendMessage) } fn set_min_size(&self, min_width: f64, min_height: f64) -> crate::Result<()> { self - .0 + .window .set_min_size(min_width, min_height) .map_err(|_| crate::Error::FailedToSendMessage) } fn set_max_size(&self, max_width: f64, max_height: f64) -> crate::Result<()> { self - .0 + .window .set_max_size(max_width, max_height) .map_err(|_| crate::Error::FailedToSendMessage) } fn set_x(&self, x: f64) -> crate::Result<()> { self - .0 + .window .set_x(x) .map_err(|_| crate::Error::FailedToSendMessage) } fn set_y(&self, y: f64) -> crate::Result<()> { self - .0 + .window .set_y(y) .map_err(|_| crate::Error::FailedToSendMessage) } fn set_position(&self, x: f64, y: f64) -> crate::Result<()> { self - .0 + .window .set_position(x, y) .map_err(|_| crate::Error::FailedToSendMessage) } fn set_fullscreen(&self, fullscreen: bool) -> crate::Result<()> { self - .0 + .window .set_fullscreen(fullscreen) .map_err(|_| crate::Error::FailedToSendMessage) } - fn set_icon(&self, icon: Icon) -> crate::Result<()> { + fn set_icon(&self, icon: Self::Icon) -> crate::Result<()> { self - .0 - .set_icon(icon.try_into()?) + .window + .set_icon(icon.0) .map_err(|_| crate::Error::FailedToSendMessage) } fn eval_script>(&self, script: S) -> crate::Result<()> { self - .0 + .window .evaluate_script(script) .map_err(|_| crate::Error::FailedToSendMessage) } } /// A wrapper around the wry Application interface. -pub struct WryApplication { +pub struct Wry { inner: wry::Application, } -impl ApplicationExt for WryApplication { - type WebviewBuilder = wry::Attributes; +impl Runtime for Wry { type Dispatcher = WryDispatcher; - fn plugin_store() -> &'static PluginStore { - static PLUGINS: Lazy> = Lazy::new(Default::default); - &PLUGINS - } - fn new() -> crate::Result { let app = wry::Application::new().map_err(|_| crate::Error::CreateWebview)?; Ok(Self { inner: app }) } - fn create_webview( + fn create_window>( &mut self, - webview_builder: Self::WebviewBuilder, - rpc_handler: Option>, - custom_protocol: Option, - file_drop_handler: Option, - ) -> crate::Result { - let app_dispatcher = self.inner.application_proxy(); + pending: PendingWindow, + ) -> crate::Result> { + let PendingWindow { + attributes, + rpc_handler, + custom_protocol, + file_drop_handler, + label, + .. + } = pending; - let app_dispatcher_ = app_dispatcher.clone(); - let wry_rpc_handler = Box::new( - move |dispatcher: wry::WindowProxy, request: wry::RpcRequest| { - if let Some(handler) = &rpc_handler { - handler( - WryDispatcher(dispatcher, app_dispatcher_.clone()), - request.into(), - ); - } - None - }, - ); + let proxy = self.inner.application_proxy(); - let file_drop_handler = Box::new(move |event: wry::FileDropEvent| { - if let Some(handler) = &file_drop_handler { - handler(event.into()) - } else { - false - } - }); + let rpc_handler = + rpc_handler.map(|handler| create_rpc_handler(proxy.clone(), label.clone(), handler)); - let dispatcher = self + let file_drop_handler = file_drop_handler + .map(|handler| create_file_drop_handler(proxy.clone(), label.clone(), handler)); + + let window = self .inner .add_window_with_configs( - webview_builder.finish()?, - Some(wry_rpc_handler), - custom_protocol.map(|p| wry::CustomProtocol { - name: p.name.clone(), - handler: Box::new(move |a| (*p.handler)(a).map_err(|_| wry::Error::InitScriptError)), - }), - Some(file_drop_handler), + attributes, + rpc_handler, + custom_protocol.map(create_custom_protocol), + file_drop_handler, ) .map_err(|_| crate::Error::CreateWebview)?; - Ok(WryDispatcher(dispatcher, app_dispatcher)) + + let dispatcher = WryDispatcher { + window, + application: proxy, + }; + + Ok(DetachedWindow { label, dispatcher }) } fn run(self) { wry::Application::run(self.inner) } } + +/// Create a wry rpc handler from a tauri rpc handler. +fn create_rpc_handler>( + app_proxy: wry::ApplicationProxy, + label: M::Label, + handler: WebviewRpcHandler, +) -> wry::WindowRpcHandler { + Box::new(move |window, request| { + handler( + DetachedWindow { + dispatcher: WryDispatcher { + window, + application: app_proxy.clone(), + }, + label: label.clone(), + }, + request.into(), + ); + None + }) +} + +/// Create a wry file drop handler from a tauri file drop handler. +fn create_file_drop_handler>( + app_proxy: wry::ApplicationProxy, + label: M::Label, + handler: FileDropHandler, +) -> wry::WindowFileDropHandler { + Box::new(move |window, event| { + handler( + event.into(), + DetachedWindow { + dispatcher: WryDispatcher { + window, + application: app_proxy.clone(), + }, + label: label.clone(), + }, + ) + }) +} + +/// Create a wry custom protocol from a tauri custom protocol. +fn create_custom_protocol(custom_protocol: CustomProtocol) -> wry::CustomProtocol { + wry::CustomProtocol { + name: custom_protocol.name.clone(), + handler: Box::new(move |data| { + (custom_protocol.handler)(data).map_err(|_| wry::Error::InitScriptError) + }), + } +} diff --git a/tauri/src/runtime/manager.rs b/tauri/src/runtime/manager.rs new file mode 100644 index 000000000..0dfbc1e41 --- /dev/null +++ b/tauri/src/runtime/manager.rs @@ -0,0 +1,527 @@ +use crate::{ + api::{ + assets::Assets, + config::{Config, WindowUrl}, + }, + event::{Event, EventHandler, Listeners}, + hooks::{InvokeHandler, InvokeMessage, InvokePayload, OnPageLoad, PageLoadPayload}, + plugin::PluginStore, + runtime::{ + sealed::ParamsPrivate, + tag::{tags_to_javascript_array, Tag, ToJavascript}, + webview::{ + Attributes, AttributesPrivate, CustomProtocol, FileDropEvent, FileDropHandler, + WebviewRpcHandler, + }, + window::{DetachedWindow, PendingWindow, Window}, + Context, Dispatch, Icon, Params, Runtime, + }, +}; +use serde::Serialize; +use serde_json::Value as JsonValue; +use std::{ + borrow::Cow, + collections::HashSet, + convert::TryInto, + sync::{Arc, Mutex}, +}; + +pub struct InnerWindowManager { + windows: Mutex>>, + plugins: Mutex>, + listeners: Listeners, + + /// The JS message handler. + invoke_handler: Box>, + + /// The page load hook, invoked when the webview performs a navigation. + on_page_load: Box>, + + config: Config, + assets: Arc, + default_window_icon: Option>, +} + +pub struct WindowManager +where + E: Tag, + L: Tag, + A: Assets + 'static, + R: Runtime, +{ + pub(crate) inner: Arc>, +} + +impl Clone for WindowManager +where + E: Tag, + L: Tag, + A: Assets + 'static, + R: Runtime, +{ + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } +} + +impl WindowManager +where + E: Tag, + L: Tag, + A: Assets, + R: Runtime, +{ + pub(crate) fn with_handlers( + context: Context, + invoke_handler: Box>, + on_page_load: Box>, + ) -> Self { + Self { + inner: Arc::new(InnerWindowManager { + windows: Mutex::default(), + plugins: Mutex::default(), + listeners: Listeners::default(), + invoke_handler, + on_page_load, + config: context.config, + assets: Arc::new(context.assets), + default_window_icon: context.default_window_icon, + }), + } + } + + // setup content for dev-server + #[cfg(dev)] + fn get_url(&self) -> String { + if self.inner.config.build.dev_path.starts_with("http") { + self.inner.config.build.dev_path.clone() + } else { + let path = "index.html"; + format!( + "data:text/html;base64,{}", + base64::encode( + self + .inner + .assets + .get(&path) + .ok_or_else(|| crate::Error::AssetNotFound(path.to_string())) + .map(Cow::into_owned) + .expect("Unable to find `index.html` under your devPath folder") + ) + ) + } + } + + #[cfg(custom_protocol)] + fn get_url(&self) -> String { + format!("tauri://{}", self.inner.config.tauri.bundle.identifier) + } + + fn prepare_attributes( + &self, + attrs: ::Attributes, + url: String, + label: L, + pending_labels: &[L], + ) -> crate::Result<::Attributes> { + let is_init_global = self.inner.config.build.with_global_tauri; + let plugin_init = self + .inner + .plugins + .lock() + .expect("poisoned plugin store") + .initialization_script(); + + let mut attributes = attrs + .url(url) + .initialization_script(&self.initialization_script(&plugin_init, is_init_global)) + .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 = tags_to_javascript_array(pending_labels)?, + current_window_label = label.to_javascript()?, + )); + + if !attributes.has_icon() { + if let Some(default_window_icon) = &self.inner.default_window_icon { + let icon = Icon::Raw(default_window_icon.clone()); + let icon = icon.try_into().expect("infallible icon convert failed"); + attributes = attributes.icon(icon); + } + } + + Ok(attributes) + } + + fn prepare_rpc_handler(&self) -> WebviewRpcHandler { + let manager = self.clone(); + Box::new(move |window, request| { + let window = manager.attach_window(window); + let command = request.command.clone(); + + let arg = request + .params + .unwrap() + .as_array_mut() + .unwrap() + .first_mut() + .unwrap_or(&mut JsonValue::Null) + .take(); + match serde_json::from_value::(arg) { + Ok(message) => { + let _ = window.on_message(command, message); + } + Err(e) => { + let error: crate::Error = e.into(); + let _ = window.eval(&format!( + r#"console.error({})"#, + JsonValue::String(error.to_string()) + )); + } + } + }) + } + + fn prepare_custom_protocol(&self) -> CustomProtocol { + let assets = self.inner.assets.clone(); + let bundle_identifier = self.inner.config.tauri.bundle.identifier.clone(); + CustomProtocol { + name: "tauri".into(), + handler: Box::new(move |path| { + let mut path = path + .split('?') + // ignore query string + .next() + .unwrap() + .to_string() + .replace(&format!("tauri://{}", bundle_identifier), ""); + if path.ends_with('/') { + path.pop(); + } + let path = if path.is_empty() { + // if the url is `tauri://${appId}`, we should load `index.html` + "index.html".to_string() + } else { + // skip leading `/` + path.chars().skip(1).collect::() + }; + + let asset_response = assets + .get(&path) + .ok_or(crate::Error::AssetNotFound(path)) + .map(Cow::into_owned); + match asset_response { + Ok(asset) => Ok(asset), + Err(e) => { + #[cfg(debug_assertions)] + eprintln!("{:?}", e); // TODO log::error! + Err(e) + } + } + }), + } + } + + fn prepare_file_drop(&self) -> FileDropHandler { + let manager = self.clone(); + Box::new(move |event, window| { + let manager = manager.clone(); + crate::async_runtime::block_on(async move { + let window = manager.attach_window(window); + let _ = match event { + FileDropEvent::Hovered(paths) => { + window.emit_internal("tauri://file-drop".to_string(), Some(paths)) + } + FileDropEvent::Dropped(paths) => { + window.emit_internal("tauri://file-drop-hover".to_string(), Some(paths)) + } + FileDropEvent::Cancelled => { + window.emit_internal("tauri://file-drop-cancelled".to_string(), Some(())) + } + }; + }); + true + }) + } + + fn initialization_script( + &self, + plugin_initialization_script: &str, + with_global_tauri: bool, + ) -> String { + format!( + r#" + {bundle_script} + {core_script} + {event_initialization_script} + if (window.rpc) {{ + window.__TAURI__.invoke("__initialized", {{ url: window.location.href }}) + }} else {{ + window.addEventListener('DOMContentLoaded', function () {{ + window.__TAURI__.invoke("__initialized", {{ url: window.location.href }}) + }}) + }} + {plugin_initialization_script} + "#, + core_script = include_str!("../../scripts/core.js"), + bundle_script = if with_global_tauri { + include_str!("../../scripts/bundle.js") + } else { + "" + }, + event_initialization_script = self.event_initialization_script(), + plugin_initialization_script = plugin_initialization_script + ) + } + + fn event_initialization_script(&self) -> String { + return format!( + " + window['{queue}'] = []; + window['{function}'] = function (eventData, salt, ignoreQueue) {{ + const listeners = (window['{listeners}'] && window['{listeners}'][eventData.event]) || [] + if (!ignoreQueue && listeners.length === 0) {{ + window['{queue}'].push({{ + eventData: eventData, + salt: salt + }}) + }} + + if (listeners.length > 0) {{ + window.__TAURI__.invoke('tauri', {{ + __tauriModule: 'Internal', + message: {{ + cmd: 'validateSalt', + salt: salt + }} + }}).then(function (flag) {{ + if (flag) {{ + for (let i = listeners.length - 1; i >= 0; i--) {{ + const listener = listeners[i] + eventData.id = listener.id + listener.handler(eventData) + }} + }} + }}) + }} + }} + ", + function = self.inner.listeners.function_name(), + queue = self.inner.listeners.queue_object_name(), + listeners = self.inner.listeners.listeners_object_name() + ); + } +} + +#[cfg(test)] +mod test { + use crate::{generate_context, runtime::flavor::wry::Wry}; + + use super::WindowManager; + + #[test] + fn check_get_url() { + let context = generate_context!("test/fixture/src-tauri/tauri.conf.json", crate::Context); + let manager: WindowManager = + WindowManager::with_handlers(context, Box::new(|_| ()), Box::new(|_, _| ())); + + #[cfg(custom_protocol)] + assert_eq!(manager.get_url(), "tauri://studio.tauri.example"); + + #[cfg(dev)] + { + use crate::runtime::sealed::ParamsPrivate; + assert_eq!(manager.get_url(), manager.config().build.dev_path); + } + } +} + +impl ParamsPrivate for WindowManager +where + E: Tag, + L: Tag, + A: Assets + 'static, + R: Runtime, +{ + fn run_invoke_handler(&self, message: InvokeMessage) { + (self.inner.invoke_handler)(message); + } + + fn run_on_page_load(&self, window: Window, payload: PageLoadPayload) { + (self.inner.on_page_load)(window.clone(), payload.clone()); + self + .inner + .plugins + .lock() + .expect("poisoned plugin store") + .on_page_load(window, payload); + } + + fn extend_api(&self, command: String, message: InvokeMessage) { + self + .inner + .plugins + .lock() + .expect("poisoned plugin store") + .extend_api(command, message); + } + + fn initialize_plugins(&self) -> crate::Result<()> { + self + .inner + .plugins + .lock() + .expect("poisoned plugin store") + .initialize(&self.inner.config.plugins) + } + + fn prepare_window( + &self, + mut pending: PendingWindow, + pending_labels: &[L], + ) -> crate::Result> { + let (is_local, url) = match &pending.url { + WindowUrl::App => (true, self.get_url()), + // todo: we should probably warn about how custom urls usually need to be valid urls + // e.g. cannot be relative without a base + WindowUrl::Custom(url) => (url.len() > 7 && &url[0..8] == "tauri://", url.clone()), + }; + + let attributes = pending.attributes.clone(); + if is_local { + let label = pending.label.clone(); + pending.attributes = self.prepare_attributes(attributes, url, label, pending_labels)?; + pending.rpc_handler = Some(self.prepare_rpc_handler()); + pending.custom_protocol = Some(self.prepare_custom_protocol()); + } else { + pending.attributes = attributes.url(url); + } + + pending.file_drop_handler = Some(self.prepare_file_drop()); + + Ok(pending) + } + + fn attach_window(&self, window: DetachedWindow) -> Window { + let window = Window::new(self.clone(), window); + + // insert the window into our manager + { + self + .inner + .windows + .lock() + .expect("poisoned window manager") + .insert(window.clone()); + } + + // let plugins know that a new window has been added to the manager + { + self + .inner + .plugins + .lock() + .expect("poisoned plugin store") + .created(window.clone()); + } + + window + } + + fn emit_filter_internal) -> bool>( + &self, + event: String, + payload: Option, + filter: F, + ) -> crate::Result<()> { + self + .inner + .windows + .lock() + .expect("poisoned window manager") + .iter() + .filter(|&w| filter(w)) + .try_for_each(|window| window.emit_internal(event.clone(), payload.clone())) + } + + fn emit_filter) -> bool>( + &self, + event: E, + payload: Option, + filter: F, + ) -> crate::Result<()> { + self + .inner + .windows + .lock() + .expect("poisoned window manager") + .iter() + .filter(|&w| filter(w)) + .try_for_each(|window| window.emit(&event, payload.clone())) + } + + fn labels(&self) -> HashSet { + self + .inner + .windows + .lock() + .expect("poisoned window manager") + .iter() + .map(|w| w.label().clone()) + .collect() + } + + fn config(&self) -> &Config { + &self.inner.config + } + + fn unlisten(&self, handler_id: EventHandler) { + self.inner.listeners.unlisten(handler_id) + } + + fn trigger(&self, event: E, window: Option, data: Option) { + self.inner.listeners.trigger(event, window, data) + } + + fn listen( + &self, + event: E, + window: Option, + handler: F, + ) -> EventHandler { + self.inner.listeners.listen(event, window, handler) + } + + fn once(&self, event: E, window: Option, handler: F) { + self.inner.listeners.once(event, window, handler) + } + + fn event_listeners_object_name(&self) -> String { + self.inner.listeners.listeners_object_name() + } + + fn event_queue_object_name(&self) -> String { + self.inner.listeners.queue_object_name() + } + + fn event_emit_function_name(&self) -> String { + self.inner.listeners.function_name() + } +} + +impl Params for WindowManager +where + E: Tag, + L: Tag, + A: Assets, + R: Runtime, +{ + type Event = E; + type Label = L; + type Assets = A; + type Runtime = R; +} diff --git a/tauri/src/runtime/mod.rs b/tauri/src/runtime/mod.rs new file mode 100644 index 000000000..03db5a04f --- /dev/null +++ b/tauri/src/runtime/mod.rs @@ -0,0 +1,328 @@ +use crate::{ + api::{assets::Assets, config::Config}, + event::{Event, EventHandler}, + runtime::{ + tag::Tag, + webview::{Attributes, AttributesPrivate, Icon, WindowConfig}, + window::{DetachedWindow, PendingWindow, Window}, + }, +}; +use serde::Serialize; +use std::convert::TryFrom; + +pub(crate) mod app; +pub mod flavor; +pub(crate) mod manager; +pub(crate) mod tag; +pub(crate) mod webview; +pub(crate) mod window; + +/// Important configurable items required by Tauri. +pub struct Context { + /// The config the application was prepared with. + pub config: Config, + + /// The assets to be served directly by Tauri. + pub assets: A, + + /// The default window icon Tauri should use when creating windows. + pub default_window_icon: Option>, +} + +/// The webview runtime interface. +pub trait Runtime: Sized + 'static { + /// The message dispatcher. + type Dispatcher: Dispatch; + + /// Creates a new webview runtime. + fn new() -> crate::Result; + + /// Creates a new webview window. + fn create_window>( + &mut self, + pending: PendingWindow, + ) -> crate::Result>; + + /// Run the webview runtime. + fn run(self); +} + +/// Webview dispatcher. A thread-safe handle to the webview API. +pub trait Dispatch: Clone + Send + Sized + 'static { + /// The runtime this [`Dispatch`] runs under. + type Runtime: Runtime; + + /// Representation of a window icon. + type Icon: TryFrom; + + /// The webview builder type. + type Attributes: Attributes + + AttributesPrivate + + From + + Clone + + Send; + + /// Creates a new webview window. + fn create_window>( + &mut self, + pending: PendingWindow, + ) -> crate::Result>; + + /// Updates the window resizable flag. + fn set_resizable(&self, resizable: bool) -> crate::Result<()>; + + /// Updates the window title. + fn set_title>(&self, title: S) -> crate::Result<()>; + + /// Maximizes the window. + fn maximize(&self) -> crate::Result<()>; + + /// Unmaximizes the window. + fn unmaximize(&self) -> crate::Result<()>; + + /// Minimizes the window. + fn minimize(&self) -> crate::Result<()>; + + /// Unminimizes the window. + fn unminimize(&self) -> crate::Result<()>; + + /// Shows the window. + fn show(&self) -> crate::Result<()>; + + /// Hides the window. + fn hide(&self) -> crate::Result<()>; + + /// Closes the window. + fn close(&self) -> crate::Result<()>; + + /// Updates the hasDecorations flag. + fn set_decorations(&self, decorations: bool) -> crate::Result<()>; + + /// Updates the window alwaysOnTop flag. + fn set_always_on_top(&self, always_on_top: bool) -> crate::Result<()>; + + /// Updates the window width. + fn set_width(&self, width: f64) -> crate::Result<()>; + + /// Updates the window height. + fn set_height(&self, height: f64) -> crate::Result<()>; + + /// Resizes the window. + fn resize(&self, width: f64, height: f64) -> crate::Result<()>; + + /// Updates the window min size. + fn set_min_size(&self, min_width: f64, min_height: f64) -> crate::Result<()>; + + /// Updates the window max size. + fn set_max_size(&self, max_width: f64, max_height: f64) -> crate::Result<()>; + + /// Updates the X position. + fn set_x(&self, x: f64) -> crate::Result<()>; + + /// Updates the Y position. + fn set_y(&self, y: f64) -> crate::Result<()>; + + /// Updates the window position. + fn set_position(&self, x: f64, y: f64) -> crate::Result<()>; + + /// Updates the window fullscreen state. + fn set_fullscreen(&self, fullscreen: bool) -> crate::Result<()>; + + /// Updates the window icon. + fn set_icon(&self, icon: Self::Icon) -> crate::Result<()>; + + /// Executes javascript on the window this [`Dispatch`] represents. + fn eval_script>(&self, script: S) -> crate::Result<()>; +} + +/// Prevent implementation details from leaking out of the [`Manager`] and [`Managed`] traits. +pub(crate) mod sealed { + use super::Params; + use crate::{ + api::config::Config, + event::{Event, EventHandler}, + hooks::{InvokeMessage, PageLoadPayload}, + runtime::{ + window::{DetachedWindow, PendingWindow, Window}, + RuntimeOrDispatch, + }, + }; + use serde::Serialize; + use std::collections::HashSet; + + /// private manager api + pub trait ParamsPrivate: Clone + Send + Sized + 'static { + /// Pass messages not handled by modules or plugins to the running application + fn run_invoke_handler(&self, message: InvokeMessage); + + /// Ran once for every window when the page is loaded. + fn run_on_page_load(&self, window: Window, payload: PageLoadPayload); + + /// Pass a message to be handled by a plugin that expects the command. + fn extend_api(&self, command: String, message: InvokeMessage); + + /// Initialize all the plugins attached to the [`Manager`]. + fn initialize_plugins(&self) -> crate::Result<()>; + + /// Prepare a [`PendingWindow`] to be created by the [`Runtime`]. + /// + /// The passed labels should represent either all the windows in the manager. If the application + /// has not yet been started, the passed labels should represent all windows that will be + /// created before starting. + fn prepare_window( + &self, + pending: PendingWindow, + labels: &[M::Label], + ) -> crate::Result>; + + /// Attach a detached window to the manager. + fn attach_window(&self, window: DetachedWindow) -> Window; + + /// Emit an event to javascript windows that pass the predicate. + fn emit_filter_internal) -> bool>( + &self, + event: String, + payload: Option, + filter: F, + ) -> crate::Result<()>; + + /// Emit an event to javascript windows that pass the predicate. + fn emit_filter) -> bool>( + &self, + event: M::Event, + payload: Option, + predicate: F, + ) -> crate::Result<()>; + + /// All current window labels existing. + fn labels(&self) -> HashSet; + + /// The configuration the [`Manager`] was built with. + fn config(&self) -> &Config; + + /// Remove the specified event handler. + fn unlisten(&self, handler_id: EventHandler); + + /// Trigger an event. + fn trigger(&self, event: M::Event, window: Option, data: Option); + + /// Set up a listener to an event. + fn listen( + &self, + event: M::Event, + window: Option, + handler: F, + ) -> EventHandler; + + /// Set up a listener to and event that is automatically removed after called once. + fn once( + &self, + event: M::Event, + window: Option, + handler: F, + ); + + fn event_listeners_object_name(&self) -> String; + fn event_queue_object_name(&self) -> String; + fn event_emit_function_name(&self) -> String; + } + + /// Represents a managed handle to the application runner. + pub trait ManagerPrivate { + /// The manager behind the [`Managed`] item. + fn manager(&self) -> &M; + + /// The runtime or runtime dispatcher of the [`Managed`] item. + fn runtime(&mut self) -> RuntimeOrDispatch<'_, M>; + } +} + +/// Represents either a [`Runtime`] or its dispatcher. +pub enum RuntimeOrDispatch<'m, M: Params> { + /// Mutable reference to the [`Runtime`]. + Runtime(&'m mut M::Runtime), + + /// Copy of the [`Runtime`]'s dispatcher. + Dispatch(::Dispatcher), +} + +/// Represents a managed handle to the application runner +pub trait Manager: sealed::ManagerPrivate { + /// The [`Config`] the manager was created with. + fn config(&self) -> &Config { + self.manager().config() + } + + /// Emits a event to all windows. + fn emit_all( + &self, + event: M::Event, + payload: Option, + ) -> crate::Result<()> { + self.manager().emit_filter(event, payload, |_| true) + } + + /// Emits an event to a window with the specified label. + fn emit_to( + &self, + label: &M::Label, + event: M::Event, + payload: Option, + ) -> crate::Result<()> { + self + .manager() + .emit_filter(event, payload, |w| w.label() == label) + } + + /// Creates a new [`Window`] on the [`Runtime`] and attaches it to the [`Manager`]. + fn create_window(&mut self, pending: PendingWindow) -> crate::Result> { + let labels = self.manager().labels().into_iter().collect::>(); + let pending = self.manager().prepare_window(pending, &labels)?; + match self.runtime() { + RuntimeOrDispatch::Runtime(runtime) => runtime.create_window(pending), + RuntimeOrDispatch::Dispatch(mut dispatcher) => dispatcher.create_window(pending), + } + .map(|window| self.manager().attach_window(window)) + } + + /// Listen to a global event. + fn listen_global(&self, event: M::Event, handler: F) -> EventHandler + where + F: Fn(Event) + Send + 'static, + { + self.manager().listen(event, None, handler) + } + + /// Listen to a global event only once. + fn once_global(&self, event: M::Event, handler: F) + where + F: Fn(Event) + Send + 'static, + { + self.manager().once(event, None, handler) + } + + /// Trigger a global event. + fn trigger_global(&self, event: M::Event, data: Option) { + self.manager().trigger(event, None, data) + } + + /// Remove an event listener. + fn unlisten(&self, handler_id: EventHandler) { + self.manager().unlisten(handler_id) + } +} + +/// Types that the manager needs to have passed in by the application. +pub trait Params: sealed::ParamsPrivate { + /// The event type used to create and listen to events. + type Event: Tag; + + /// The type used to determine the name of windows. + type Label: Tag; + + /// Assets that Tauri should serve from itself. + type Assets: Assets; + + /// The underlying webview runtime used by the Tauri application. + type Runtime: Runtime; +} diff --git a/tauri/src/runtime/tag.rs b/tauri/src/runtime/tag.rs new file mode 100644 index 000000000..5791233fd --- /dev/null +++ b/tauri/src/runtime/tag.rs @@ -0,0 +1,48 @@ +//! Working with "string-able" types. + +use std::{ + fmt::{Debug, Display}, + hash::Hash, + str::FromStr, +}; + +/// Represents a "string-able" type. +/// +/// The type is required to be able to be represented as a string [`Display`], along with knowing +/// how to be parsed from the string representation [`FromStr`]. +/// +/// [`Clone`], [`Hash`], and [`Eq`] are needed so that it can represent un-hashable types. +/// +/// [`Send`] and [`Sync`] and `'static` are current requirements due to how it is sometimes sent +/// across thread boundaries, although some of those constraints may relax in the future. +/// +/// The simplest type that fits all these requirements is a [`String`](std::string::String). +pub trait Tag: Hash + Eq + FromStr + Display + Debug + Clone + Send + Sync + 'static {} + +/// Automatically implement [`Tag`] for all types that fit the requirements. +impl Tag for T where T: Hash + Eq + FromStr + Display + Debug + Clone + Send + Sync + 'static {} + +/// Private helper to turn [`Tag`] related things into JavaScript, safely. +/// +/// The main concern is string escaping, so we rely on [`serde_json`] to handle all serialization +/// of the [`Tag`] as a string. We do this instead of requiring [`serde::Serialize`] on [`Tag`] +/// because it really represents a string, not any serializable data structure. +/// +/// We don't want downstream users to implement this trait so that [`Tag`]s cannot be turned into +/// invalid JavaScript - regardless of their content. +pub(crate) trait ToJavascript { + fn to_javascript(&self) -> crate::Result; +} + +impl ToJavascript for T { + /// Turn any [`Tag`] into the JavaScript representation of a string. + fn to_javascript(&self) -> crate::Result { + Ok(serde_json::to_string(&self.to_string())?) + } +} + +/// Turn any collection of [`Tag`]s into a JavaScript array of strings. +pub(crate) fn tags_to_javascript_array(tags: &[impl Tag]) -> crate::Result { + let tags: Vec = tags.iter().map(ToString::to_string).collect(); + Ok(serde_json::to_string(&tags)?) +} diff --git a/tauri/src/runtime/webview.rs b/tauri/src/runtime/webview.rs new file mode 100644 index 000000000..6c5abba89 --- /dev/null +++ b/tauri/src/runtime/webview.rs @@ -0,0 +1,127 @@ +use crate::runtime::window::DetachedWindow; +use serde_json::Value as JsonValue; +use std::{convert::TryFrom, path::PathBuf}; + +/// A icon definition. +pub enum Icon { + /// Icon from file path. + File(PathBuf), + /// Icon from raw bytes. + Raw(Vec), +} + +pub struct WindowConfig(pub crate::api::config::WindowConfig); + +pub trait AttributesPrivate: Sized { + /// Sets the webview url. + fn url(self, url: String) -> Self; +} + +/// The webview builder. +pub trait Attributes: Sized { + /// Expected icon format. + type Icon: TryFrom; + + /// Initializes a new webview builder. + fn new() -> Self; + + /// Sets the init script. + fn initialization_script(self, init: &str) -> 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: S) -> 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; + + /// Whether the window should be immediately visible upon creation. + fn visible(self, visible: bool) -> Self; + + /// Whether the the window should be transparent. If this is true, writing colors + /// with alpha values different than `1.0` will produce a transparent window. + fn transparent(self, transparent: bool) -> Self; + + /// Whether the window should have borders and bars. + fn decorations(self, decorations: bool) -> Self; + + /// Whether the window should always be on top of other windows. + fn always_on_top(self, always_on_top: bool) -> Self; + + /// Sets the window icon. + fn icon(self, icon: Self::Icon) -> Self; + + /// Whether the icon was set or not. + fn has_icon(&self) -> bool; + + /// User data path for the webview. Actually only supported on Windows. + fn user_data_path(self, user_data_path: Option) -> Self; + + /// The full attributes. + fn build(self) -> Self; +} + +// TODO: should probably expand the following documentation + +/// Rpc request. +pub struct RpcRequest { + /// RPC command. + pub command: String, + /// Params. + pub params: Option, +} + +/// Rpc handler. +pub type WebviewRpcHandler = Box, RpcRequest) + Send>; + +/// Uses a custom handler to resolve file requests +pub struct CustomProtocol { + /// Name of the protocol + pub name: String, + /// Handler for protocol + pub handler: Box crate::Result> + Send>, +} + +/// The file drop event payload. +#[derive(Debug, Clone)] +pub enum FileDropEvent { + /// The file(s) have been dragged onto the window, but have not been dropped yet. + Hovered(Vec), + /// The file(s) have been dropped onto the window. + Dropped(Vec), + /// The file drop was aborted. + Cancelled, +} + +/// File drop handler callback +/// Return `true` in the callback to block the OS' default behavior of handling a file drop. +pub type FileDropHandler = Box) -> bool + Send>; diff --git a/tauri/src/runtime/window.rs b/tauri/src/runtime/window.rs new file mode 100644 index 000000000..591247477 --- /dev/null +++ b/tauri/src/runtime/window.rs @@ -0,0 +1,364 @@ +use crate::{ + api::config::WindowUrl, + event::{Event, EventHandler}, + hooks::{InvokeMessage, InvokePayload, PageLoadPayload}, + runtime::{ + sealed::ManagerPrivate, + tag::ToJavascript, + webview::{CustomProtocol, FileDropHandler, Icon, WebviewRpcHandler}, + Dispatch, Manager, Params, Runtime, RuntimeOrDispatch, + }, +}; +use serde::Serialize; +use serde_json::Value as JsonValue; +use std::{ + convert::TryInto, + hash::{Hash, Hasher}, +}; + +/// A webview window that has yet to be built. +pub struct PendingWindow { + /// The label that the window will be named. + pub label: M::Label, + + /// The url the window will open with. + pub url: WindowUrl, + + /// The [`Attributes`] that the webview window be created with. + pub attributes: <::Dispatcher as Dispatch>::Attributes, + + /// How to handle RPC calls on the webview window. + pub rpc_handler: Option>, + + /// How to handle custom protocols for the webview window. + pub custom_protocol: Option, + + /// How to handle a file dropping onto the webview window. + pub file_drop_handler: Option>, +} + +impl PendingWindow { + pub fn new( + attributes: impl Into<<::Dispatcher as Dispatch>::Attributes>, + label: M::Label, + url: WindowUrl, + ) -> Self { + Self { + attributes: attributes.into(), + label, + url, + rpc_handler: None, + custom_protocol: None, + file_drop_handler: None, + } + } +} + +/// A webview window that is not yet managed by Tauri. +pub struct DetachedWindow { + pub label: M::Label, + pub dispatcher: ::Dispatcher, +} + +impl Clone for DetachedWindow { + fn clone(&self) -> Self { + Self { + label: self.label.clone(), + dispatcher: self.dispatcher.clone(), + } + } +} + +impl Hash for DetachedWindow { + /// Only use the [`DetachedWindow`]'s label to represent its hash. + fn hash(&self, state: &mut H) { + self.label.hash(state) + } +} + +impl Eq for DetachedWindow {} +impl PartialEq for DetachedWindow { + /// Only use the [`DetachedWindow`]'s label to compare equality. + fn eq(&self, other: &Self) -> bool { + self.label.eq(&other.label) + } +} + +/// A webview window managed by Tarui. +/// +/// TODO: expand these docs since this is a pretty important type +pub struct Window { + /// The webview window created by the runtime. + window: DetachedWindow, + + /// The manager to associate this webview window with. + manager: M, +} + +impl Clone for Window { + fn clone(&self) -> Self { + Self { + window: self.window.clone(), + manager: self.manager.clone(), + } + } +} + +impl Hash for Window { + /// Only use the [`Window`]'s label to represent its hash. + fn hash(&self, state: &mut H) { + self.window.label.hash(state) + } +} + +impl Eq for Window {} +impl PartialEq for Window { + /// Only use the [`Window`]'s label to compare equality. + fn eq(&self, other: &Self) -> bool { + self.window.label.eq(&other.window.label) + } +} + +impl Manager for Window {} +impl ManagerPrivate for Window { + fn manager(&self) -> &M { + &self.manager + } + + fn runtime(&mut self) -> RuntimeOrDispatch<'_, M> { + RuntimeOrDispatch::Dispatch(self.dispatcher()) + } +} + +impl Window { + /// Create a new window that is attached to the manager. + pub(crate) fn new(manager: M, window: DetachedWindow) -> Self { + Self { manager, window } + } + + /// The current window's dispatcher. + pub(crate) fn dispatcher(&self) -> ::Dispatcher { + self.window.dispatcher.clone() + } + + /// How to handle this window receiving an [`InvokeMessage`]. + pub(crate) fn on_message(self, command: String, payload: InvokePayload) -> crate::Result<()> { + let manager = self.manager.clone(); + if &command == "__initialized" { + let payload: PageLoadPayload = serde_json::from_value(payload.inner)?; + manager.run_on_page_load(self, payload); + } else { + let message = InvokeMessage::new(self, command.to_string(), payload); + if let Some(module) = &message.payload.tauri_module { + let module = module.to_string(); + crate::endpoints::handle(module, message, manager.config()); + } else if command.starts_with("plugin:") { + manager.extend_api(command, message); + } else { + manager.run_invoke_handler(message); + } + } + + Ok(()) + } + + /// The label of this window. + pub fn label(&self) -> &M::Label { + &self.window.label + } + + pub(crate) fn emit_internal( + &self, + event: E, + payload: Option, + ) -> crate::Result<()> { + let js_payload = match payload { + Some(payload_value) => serde_json::to_value(payload_value)?, + None => JsonValue::Null, + }; + + self.eval(&format!( + "window['{}']({{event: {}, payload: {}}}, '{}')", + self.manager.event_emit_function_name(), + event.to_javascript()?, + js_payload, + crate::salt::generate() + ))?; + + Ok(()) + } + + /// Emits an event to the current window. + pub fn emit(&self, event: &M::Event, payload: Option) -> crate::Result<()> { + self.emit_internal(event.clone(), payload) + } + + pub(crate) fn emit_others_internal( + &self, + event: String, + payload: Option, + ) -> crate::Result<()> { + self + .manager + .emit_filter_internal(event, payload, |w| w != self) + } + + /// Emits an event on all windows except this one. + pub fn emit_others( + &self, + event: M::Event, + payload: Option, + ) -> crate::Result<()> { + self.manager.emit_filter(event, payload, |w| w != self) + } + + /// Listen to an event on this window. + pub fn listen(&self, event: M::Event, handler: F) -> EventHandler + where + F: Fn(Event) + Send + 'static, + { + let label = self.window.label.clone(); + self.manager.listen(event, Some(label), handler) + } + + /// Listen to a an event on this window a single time. + pub fn once(&self, event: M::Event, handler: F) + where + F: Fn(Event) + Send + 'static, + { + let label = self.window.label.clone(); + self.manager.once(event, Some(label), handler) + } + + /// Triggers an event on this window. + pub(crate) fn trigger(&self, event: M::Event, data: Option) { + let label = self.window.label.clone(); + self.manager.trigger(event, Some(label), data) + } + + /// Evaluates JavaScript on this window. + pub fn eval(&self, js: &str) -> crate::Result<()> { + self.window.dispatcher.eval_script(js) + } + + /// Determines if this window should be resizable. + pub fn set_resizable(&self, resizable: bool) -> crate::Result<()> { + self.window.dispatcher.set_resizable(resizable) + } + + /// Set this window's title. + pub fn set_title(&self, title: &str) -> crate::Result<()> { + self.window.dispatcher.set_title(title.to_string()) + } + + /// Maximizes this window. + pub fn maximize(&self) -> crate::Result<()> { + self.window.dispatcher.maximize() + } + + /// Un-maximizes this window. + pub fn unmaximize(&self) -> crate::Result<()> { + self.window.dispatcher.unmaximize() + } + + /// Minimizes this window. + pub fn minimize(&self) -> crate::Result<()> { + self.window.dispatcher.minimize() + } + + /// Un-minimizes this window. + pub fn unminimize(&self) -> crate::Result<()> { + self.window.dispatcher.unminimize() + } + + /// Show this window. + pub fn show(&self) -> crate::Result<()> { + self.window.dispatcher.show() + } + + /// Hide this window. + pub fn hide(&self) -> crate::Result<()> { + self.window.dispatcher.hide() + } + + /// Closes this window. + pub fn close(&self) -> crate::Result<()> { + self.window.dispatcher.close() + } + + /// Determines if this window should be [decorated]. + /// + /// [decorated]: https://en.wikipedia.org/wiki/Window_(computing)#Window_decoration + pub fn set_decorations(&self, decorations: bool) -> crate::Result<()> { + self.window.dispatcher.set_decorations(decorations) + } + + /// Determines if this window should always be on top of other windows. + pub fn set_always_on_top(&self, always_on_top: bool) -> crate::Result<()> { + self.window.dispatcher.set_always_on_top(always_on_top) + } + + /// Sets this window's width. + pub fn set_width(&self, width: impl Into) -> crate::Result<()> { + self.window.dispatcher.set_width(width.into()) + } + + /// Sets this window's height. + pub fn set_height(&self, height: impl Into) -> crate::Result<()> { + self.window.dispatcher.set_height(height.into()) + } + + /// Resizes this window. + pub fn resize(&self, width: impl Into, height: impl Into) -> crate::Result<()> { + self.window.dispatcher.resize(width.into(), height.into()) + } + + /// Sets this window's minimum size. + pub fn set_min_size( + &self, + min_width: impl Into, + min_height: impl Into, + ) -> crate::Result<()> { + self + .window + .dispatcher + .set_min_size(min_width.into(), min_height.into()) + } + + /// Sets this window's maximum size. + pub fn set_max_size( + &self, + max_width: impl Into, + max_height: impl Into, + ) -> crate::Result<()> { + self + .window + .dispatcher + .set_max_size(max_width.into(), max_height.into()) + } + + /// Sets this window's x position. + pub fn set_x(&self, x: impl Into) -> crate::Result<()> { + self.window.dispatcher.set_x(x.into()) + } + + /// Sets this window's y position. + pub fn set_y(&self, y: impl Into) -> crate::Result<()> { + self.window.dispatcher.set_y(y.into()) + } + + /// Sets this window's position. + pub fn set_position(&self, x: impl Into, y: impl Into) -> crate::Result<()> { + self.window.dispatcher.set_position(x.into(), y.into()) + } + + /// Determines if this window should be fullscreen. + pub fn set_fullscreen(&self, fullscreen: bool) -> crate::Result<()> { + self.window.dispatcher.set_fullscreen(fullscreen) + } + + /// Sets this window' icon. + pub fn set_icon(&self, icon: Icon) -> crate::Result<()> { + self.window.dispatcher.set_icon(icon.try_into()?) + } +}