snapshot of https://github.com/tauri-apps/tauri.git (dev, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Audit / audit-js (push) Failing after 0s
bench / bench (map[os:ubuntu-latest runner:xvfb-run --auto-servernum target:x86_64-unknown-linux-gnu], nightly) (push) Failing after 0s
covector version or publish / run-integration-tests (ubuntu-latest) (push) Failing after 0s
Audit / audit-rust (push) Failing after 0s
test `@tauri-apps/cli` / test (ubuntu-latest) (push) Failing after 0s
test `tauri-cli` / test (map[os:ubuntu-latest target:x86_64-unknown-linux-gnu]) (push) Failing after 0s
test core / test (map[args:--all-features key:all], map[cargo:cargo command:test os:ubuntu-latest target:x86_64-unknown-linux-gnu toolchain:1.90]) (push) Failing after 0s
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:test os:ubuntu-latest target:x86_64-unknown-linux-gnu toolchain:1.90]) (push) Failing after 0s
test core / test (map[args:--no-default-features key:no-default], map[cargo:cross command:build os:ubuntu-latest target:aarch64-linux-android toolchain:1.90]) (push) Failing after 0s
deploy schema worker / deploy (push) Failing after 0s
lint rust / clippy (map[cargo:cargo include: os:ubuntu-latest target:x86_64-unknown-linux-gnu]) (push) Failing after 0s
lint rust / clippy (map[cargo:cross include:--package api os:ubuntu-latest target:aarch64-linux-android]) (push) Failing after 0s
supply chain health status / check rust dependencies with cargo vet (push) Failing after 0s
test `@tauri-apps/api` e2e (mobile) / android (push) Failing after 0s
test `@tauri-apps/api` e2e / test (map[name:Linux os:ubuntu-latest]) (push) Failing after 0s
test core / test (map[args:--all-features key:all], map[cargo:cross command:build os:ubuntu-latest target:aarch64-linux-android toolchain:1.90]) (push) Failing after 0s
Udeps / changes (push) Failing after 0s
Udeps / setup (push) Has been skipped
Udeps / udeps (push) Has been skipped
bench / bench (map[os:macos-latest runner: target:aarch64-apple-darwin], nightly) (push) Has been cancelled
bench / bench (map[os:windows-latest runner: target:x86_64-pc-windows-msvc], nightly) (push) Has been cancelled
covector version or publish / run-integration-tests (macos-latest) (push) Has been cancelled
covector version or publish / run-integration-tests (windows-latest) (push) Has been cancelled
covector version or publish / version-or-publish (push) Has been cancelled
test `@tauri-apps/cli` / test (macos-latest) (push) Has been cancelled
test `tauri-cli` / test (map[args:--no-default-features --features native-tls-vendored os:windows-latest restore-only:true target:aarch64-pc-windows-msvc]) (push) Has been cancelled
test `tauri-cli` / test (map[os:macos-latest target:x86_64-apple-darwin]) (push) Has been cancelled
test `tauri-cli` / test (map[os:windows-latest target:x86_64-pc-windows-msvc]) (push) Has been cancelled
test core / test (map[args:--all-features key:all], map[cargo:cargo command:build os:macos-latest target:aarch64-apple-ios toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--all-features key:all], map[cargo:cargo command:test os:macos-14 target:aarch64-apple-darwin toolchain:1.90]) (push) Has been cancelled
lint rust / clippy (map[cargo:cargo include: os:macos-latest target:aarch64-apple-darwin]) (push) Has been cancelled
lint rust / clippy (map[cargo:cargo include: os:windows-latest target:x86_64-pc-windows-msvc]) (push) Has been cancelled
lint rust / clippy (map[cargo:cargo include:--package api os:macos-latest target:aarch64-apple-ios]) (push) Has been cancelled
test `@tauri-apps/api` e2e (mobile) / ios (push) Has been cancelled
test `@tauri-apps/api` e2e / test (map[name:Windows os:windows-latest]) (push) Has been cancelled
test `@tauri-apps/api` e2e / test (map[name:macOS os:macos-latest]) (push) Has been cancelled
test `@tauri-apps/cli` / test (windows-latest) (push) Has been cancelled
test core / test (map[args:--all-features key:all], map[cargo:cargo command:test os:windows-latest target:x86_64-pc-windows-msvc toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:build os:macos-latest target:aarch64-apple-ios toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:test os:macos-14 target:aarch64-apple-darwin toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:test os:windows-latest target:x86_64-pc-windows-msvc toolchain:1.90]) (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-23 17:00:42 +05:00
commit feda81bf2f
1178 changed files with 156404 additions and 0 deletions

2842
crates/tauri/src/app.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,158 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use tauri_utils::{Theme, config::BundleType};
use crate::{
AppHandle, Manager, ResourceId, Runtime, Webview, command,
plugin::{Builder, TauriPlugin},
};
#[command(root = "crate")]
pub fn version<R: Runtime>(app: AppHandle<R>) -> String {
app.package_info().version.to_string()
}
#[command(root = "crate")]
pub fn name<R: Runtime>(app: AppHandle<R>) -> String {
app.package_info().name.clone()
}
#[command(root = "crate")]
pub fn tauri_version() -> &'static str {
crate::VERSION
}
#[command(root = "crate")]
pub fn identifier<R: Runtime>(app: AppHandle<R>) -> String {
app.config().identifier.clone()
}
#[command(root = "crate")]
#[allow(unused_variables)]
pub fn app_show<R: Runtime>(app: AppHandle<R>) -> crate::Result<()> {
#[cfg(target_os = "macos")]
app.show()?;
Ok(())
}
#[command(root = "crate")]
#[allow(unused_variables)]
pub fn app_hide<R: Runtime>(app: AppHandle<R>) -> crate::Result<()> {
#[cfg(target_os = "macos")]
app.hide()?;
Ok(())
}
#[command(root = "crate")]
pub fn exit<R: Runtime>(app: AppHandle<R>, code: i32) {
app.exit(code)
}
#[command(root = "crate")]
#[allow(unused_variables)]
pub async fn fetch_data_store_identifiers<R: Runtime>(
app: AppHandle<R>,
) -> crate::Result<Vec<[u8; 16]>> {
#[cfg(target_vendor = "apple")]
return app.fetch_data_store_identifiers().await;
#[cfg(not(target_vendor = "apple"))]
return Ok(Vec::new());
}
#[command(root = "crate")]
#[allow(unused_variables)]
pub async fn remove_data_store<R: Runtime>(app: AppHandle<R>, uuid: [u8; 16]) -> crate::Result<()> {
#[cfg(target_vendor = "apple")]
app.remove_data_store(uuid).await?;
#[cfg(not(target_vendor = "apple"))]
let _ = uuid;
Ok(())
}
#[command(root = "crate")]
pub fn default_window_icon<R: Runtime>(
webview: Webview<R>,
app: AppHandle<R>,
) -> Option<ResourceId> {
app.default_window_icon().cloned().map(|icon| {
let mut resources_table = webview.resources_table();
resources_table.add(icon.to_owned())
})
}
#[command(root = "crate")]
pub async fn set_app_theme<R: Runtime>(app: AppHandle<R>, theme: Option<Theme>) {
app.set_theme(theme);
}
#[command(root = "crate")]
pub async fn set_dock_visibility<R: Runtime>(
app: AppHandle<R>,
visible: bool,
) -> crate::Result<()> {
#[cfg(target_os = "macos")]
{
let mut focused_window = None;
for window in app.manager.windows().into_values() {
if window.is_focused().unwrap_or_default() {
focused_window.replace(window);
break;
}
}
app.set_dock_visibility(visible)?;
// retain focus
if let Some(focused_window) = focused_window {
let _ = focused_window.set_focus();
}
}
#[cfg(not(target_os = "macos"))]
let (_app, _visible) = (app, visible);
Ok(())
}
#[command(root = "crate")]
pub fn bundle_type() -> Option<BundleType> {
tauri_utils::platform::bundle_type()
}
#[command(root = "crate")]
pub fn supports_multiple_windows<R: Runtime>(app: AppHandle<R>) -> bool {
app.supports_multiple_windows()
}
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("app")
.invoke_handler(crate::generate_handler![
#![plugin(app)]
version,
name,
tauri_version,
identifier,
app_show,
app_hide,
exit,
fetch_data_store_identifiers,
remove_data_store,
default_window_icon,
set_app_theme,
set_dock_visibility,
bundle_type,
supports_multiple_windows,
])
.setup(|_app, _api| {
#[cfg(target_os = "android")]
{
let handle = _api.register_android_plugin("app.tauri", "AppPlugin")?;
_app.manage(AppPlugin(handle));
}
Ok(())
})
.build()
}
#[cfg(target_os = "android")]
pub(crate) struct AppPlugin<R: Runtime>(pub crate::plugin::PluginHandle<R>);

View File

@@ -0,0 +1,363 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
//! The singleton async runtime used by Tauri and exposed to users.
//!
//! Tauri uses [`tokio`] Runtime to initialize code, such as
//! [`Plugin::initialize`](../plugin/trait.Plugin.html#method.initialize) and [`crate::Builder::setup`] hooks.
//! This module also re-export some common items most developers need from [`tokio`]. If there's
//! one you need isn't here, you could use types in [`tokio`] directly.
//! For custom command handlers, it's recommended to use a plain `async fn` command.
pub use tokio::{
runtime::{Handle as TokioHandle, Runtime as TokioRuntime},
sync::{
Mutex, RwLock,
mpsc::{Receiver, Sender, channel},
},
task::JoinHandle as TokioJoinHandle,
};
use std::{
future::Future,
pin::Pin,
sync::OnceLock,
task::{Context, Poll},
};
static RUNTIME: OnceLock<GlobalRuntime> = OnceLock::new();
struct GlobalRuntime {
runtime: Option<Runtime>,
handle: RuntimeHandle,
}
impl GlobalRuntime {
fn handle(&self) -> RuntimeHandle {
if let Some(r) = &self.runtime {
r.handle()
} else {
self.handle.clone()
}
}
#[track_caller]
fn spawn<F>(&self, task: F) -> JoinHandle<F::Output>
where
F: Future + Send + 'static,
F::Output: Send + 'static,
{
if let Some(r) = &self.runtime {
r.spawn(task)
} else {
self.handle.spawn(task)
}
}
#[track_caller]
/// Runs the provided function on an executor dedicated to blocking operations.
pub fn spawn_blocking<F, R>(&self, func: F) -> JoinHandle<R>
where
F: FnOnce() -> R + Send + 'static,
R: Send + 'static,
{
if let Some(r) = &self.runtime {
r.spawn_blocking(func)
} else {
self.handle.spawn_blocking(func)
}
}
#[track_caller]
fn block_on<F: Future>(&self, task: F) -> F::Output {
if let Some(r) = &self.runtime {
r.block_on(task)
} else {
self.handle.block_on(task)
}
}
}
/// A runtime used to execute asynchronous tasks.
pub enum Runtime {
/// The tokio runtime.
Tokio(TokioRuntime),
}
impl Runtime {
/// Gets a reference to the [`TokioRuntime`].
pub fn inner(&self) -> &TokioRuntime {
let Self::Tokio(r) = self;
r
}
/// Returns a handle of the async runtime.
pub fn handle(&self) -> RuntimeHandle {
match self {
Self::Tokio(r) => RuntimeHandle::Tokio(r.handle().clone()),
}
}
#[track_caller]
/// Spawns a future onto the runtime.
pub fn spawn<F>(&self, task: F) -> JoinHandle<F::Output>
where
F: Future + Send + 'static,
F::Output: Send + 'static,
{
match self {
Self::Tokio(r) => {
let _guard = r.enter();
JoinHandle::Tokio(tokio::spawn(task))
}
}
}
#[track_caller]
/// Runs the provided function on an executor dedicated to blocking operations.
pub fn spawn_blocking<F, R>(&self, func: F) -> JoinHandle<R>
where
F: FnOnce() -> R + Send + 'static,
R: Send + 'static,
{
match self {
Self::Tokio(r) => JoinHandle::Tokio(r.spawn_blocking(func)),
}
}
#[track_caller]
/// Runs a future to completion on runtime.
pub fn block_on<F: Future>(&self, task: F) -> F::Output {
match self {
Self::Tokio(r) => r.block_on(task),
}
}
}
/// An owned permission to join on a task (await its termination).
#[derive(Debug)]
pub enum JoinHandle<T> {
/// The tokio JoinHandle.
Tokio(TokioJoinHandle<T>),
}
impl<T> JoinHandle<T> {
/// Gets a reference to the [`TokioJoinHandle`].
pub fn inner(&self) -> &TokioJoinHandle<T> {
let Self::Tokio(t) = self;
t
}
/// Abort the task associated with the handle.
///
/// Awaiting a cancelled task might complete as usual if the task was
/// already completed at the time it was cancelled, but most likely it
/// will fail with a cancelled `JoinError`.
pub fn abort(&self) {
match self {
Self::Tokio(t) => t.abort(),
}
}
}
impl<T> Future for JoinHandle<T> {
type Output = crate::Result<T>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
match self.get_mut() {
Self::Tokio(t) => Pin::new(t).poll(cx).map_err(Into::into),
}
}
}
/// A handle to the async runtime
#[derive(Clone)]
pub enum RuntimeHandle {
/// The tokio handle.
Tokio(TokioHandle),
}
impl RuntimeHandle {
/// Gets a reference to the [`TokioHandle`].
pub fn inner(&self) -> &TokioHandle {
let Self::Tokio(h) = self;
h
}
#[track_caller]
/// Runs the provided function on an executor dedicated to blocking operations.
///
/// Use this instead of [`Self::spawn`] for code that blocks the thread it runs on
/// (synchronous file or network I/O, heavy computation, FFI calls) so the async executor
/// is not blocked. The returned [`JoinHandle`] resolves to the closure's return value.
///
/// # Examples
///
/// ```rust
/// use tauri::Manager;
///
/// tauri::Builder::default()
/// .setup(|app| {
/// let handle = app.handle().clone();
/// tauri::async_runtime::handle().spawn_blocking(move || {
/// let contents = std::fs::read_to_string("file.txt").unwrap_or_default();
/// handle.package_info().name.len() + contents.len()
/// });
/// Ok(())
/// });
/// ```
pub fn spawn_blocking<F, R>(&self, func: F) -> JoinHandle<R>
where
F: FnOnce() -> R + Send + 'static,
R: Send + 'static,
{
match self {
Self::Tokio(h) => JoinHandle::Tokio(h.spawn_blocking(func)),
}
}
#[track_caller]
/// Spawns a future onto the runtime.
pub fn spawn<F>(&self, task: F) -> JoinHandle<F::Output>
where
F: Future + Send + 'static,
F::Output: Send + 'static,
{
match self {
Self::Tokio(h) => {
let _guard = h.enter();
JoinHandle::Tokio(tokio::spawn(task))
}
}
}
#[track_caller]
/// Runs a future to completion on runtime.
pub fn block_on<F: Future>(&self, task: F) -> F::Output {
match self {
Self::Tokio(h) => h.block_on(task),
}
}
}
fn default_runtime() -> GlobalRuntime {
let runtime = Runtime::Tokio(TokioRuntime::new().unwrap());
let handle = runtime.handle();
GlobalRuntime {
runtime: Some(runtime),
handle,
}
}
/// Sets the runtime to use to execute asynchronous tasks.
/// For convenience, this method takes a [`TokioHandle`].
/// Note that you cannot drop the underlying [`TokioRuntime`].
///
/// # Examples
///
/// ```rust
/// #[tokio::main]
/// async fn main() {
/// // perform some async task before initializing the app
/// do_something().await;
/// // share the current runtime with Tauri
/// tauri::async_runtime::set(tokio::runtime::Handle::current());
///
/// // bootstrap the tauri app...
/// // tauri::Builder::default().run().unwrap();
/// }
///
/// async fn do_something() {}
/// ```
///
/// # Panics
///
/// Panics if the runtime is already set.
pub fn set(handle: TokioHandle) {
RUNTIME
.set(GlobalRuntime {
runtime: None,
handle: RuntimeHandle::Tokio(handle),
})
.unwrap_or_else(|_| panic!("runtime already initialized"))
}
/// Returns a handle of the async runtime.
pub fn handle() -> RuntimeHandle {
let runtime = RUNTIME.get_or_init(default_runtime);
runtime.handle()
}
#[track_caller]
/// Runs a future to completion on runtime.
pub fn block_on<F: Future>(task: F) -> F::Output {
let runtime = RUNTIME.get_or_init(default_runtime);
runtime.block_on(task)
}
#[track_caller]
/// Spawns a future onto the runtime.
pub fn spawn<F>(task: F) -> JoinHandle<F::Output>
where
F: Future + Send + 'static,
F::Output: Send + 'static,
{
let runtime = RUNTIME.get_or_init(default_runtime);
runtime.spawn(task)
}
#[track_caller]
/// Runs the provided function on an executor dedicated to blocking operations.
pub fn spawn_blocking<F, R>(func: F) -> JoinHandle<R>
where
F: FnOnce() -> R + Send + 'static,
R: Send + 'static,
{
let runtime = RUNTIME.get_or_init(default_runtime);
runtime.spawn_blocking(func)
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn runtime_spawn() {
let join = spawn(async { 5 });
assert_eq!(join.await.unwrap(), 5);
}
#[test]
fn runtime_block_on() {
assert_eq!(block_on(async { 0 }), 0);
}
#[tokio::test]
async fn handle_spawn() {
let handle = handle();
let join = handle.spawn(async { 5 });
assert_eq!(join.await.unwrap(), 5);
}
#[test]
fn handle_block_on() {
let handle = handle();
assert_eq!(handle.block_on(async { 0 }), 0);
}
#[tokio::test]
async fn handle_abort() {
let handle = handle();
let join = handle.spawn(async {
// Here we sleep 1 second to ensure this task to be uncompleted when abort() invoked.
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
5
});
join.abort();
if let crate::Error::JoinError(raw_error) = join.await.unwrap_err() {
assert!(raw_error.is_cancelled());
} else {
panic!("Abort did not result in the expected `JoinError`");
}
}
}

198
crates/tauri/src/error.rs Normal file
View File

@@ -0,0 +1,198 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::fmt;
/// A generic boxed error.
#[derive(Debug)]
pub struct SetupError(Box<dyn std::error::Error>);
impl From<Box<dyn std::error::Error>> for SetupError {
fn from(error: Box<dyn std::error::Error>) -> Self {
Self(error)
}
}
// safety: the setup error is only used on the main thread
// and we exit the process immediately.
unsafe impl Send for SetupError {}
unsafe impl Sync for SetupError {}
impl fmt::Display for SetupError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl std::error::Error for SetupError {}
/// Runtime errors that can happen inside a Tauri application.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
/// Runtime error.
#[error("runtime error: {0}")]
Runtime(#[from] tauri_runtime::Error),
/// Window label must be unique.
#[error("a window with label `{0}` already exists")]
WindowLabelAlreadyExists(String),
/// Webview label must be unique.
#[error("a webview with label `{0}` already exists")]
WebviewLabelAlreadyExists(String),
/// Cannot use the webview reparent function on webview windows.
#[error("cannot reparent when using a WebviewWindow")]
CannotReparentWebviewWindow,
/// Embedded asset not found.
#[error("asset not found: {0}")]
AssetNotFound(String),
/// Failed to serialize/deserialize.
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
/// IO error.
#[error("{0}")]
Io(#[from] std::io::Error),
/// Failed to load window icon.
#[error("invalid icon: {0}")]
InvalidIcon(std::io::Error),
/// Invalid args when running a command.
#[error("invalid args `{1}` for command `{0}`: {2}")]
InvalidArgs(&'static str, &'static str, serde_json::Error),
/// Encountered an error in the setup hook,
#[error("error encountered during setup hook: {0}")]
Setup(SetupError),
/// Error initializing plugin.
#[error("failed to initialize plugin `{0}`: {1}")]
PluginInitialization(String, String),
/// A part of the URL is malformed or invalid. This may occur when parsing and combining
/// user-provided URLs and paths.
#[error("invalid url: {0}")]
InvalidUrl(url::ParseError),
/// Task join error.
#[error(transparent)]
JoinError(#[from] tokio::task::JoinError),
/// An error happened inside the isolation pattern.
#[cfg(feature = "isolation")]
#[error("isolation pattern error: {0}")]
IsolationPattern(#[from] tauri_utils::pattern::isolation::Error),
/// An invalid window URL was provided. Includes details about the error.
#[error("invalid window url: {0}")]
InvalidWebviewUrl(&'static str),
/// Invalid glob pattern.
#[error("invalid glob pattern: {0}")]
GlobPattern(#[from] glob::PatternError),
/// Image error.
#[cfg(any(feature = "image-png", feature = "image-ico"))]
#[error("failed to process image: {0}")]
Image(#[from] image::error::ImageError),
/// The Window's raw handle is invalid for the platform.
#[error("Unexpected `raw_window_handle` for the current platform")]
InvalidWindowHandle,
/// JNI error.
#[cfg(target_os = "android")]
#[error("jni error: {0}")]
Jni(#[from] jni::errors::Error),
/// Failed to receive message .
#[error("failed to receive message")]
FailedToReceiveMessage,
/// Menu error.
#[error("menu error: {0}")]
#[cfg(desktop)]
Menu(#[from] muda::Error),
/// Bad menu icon error.
#[error(transparent)]
#[cfg(desktop)]
BadMenuIcon(#[from] muda::BadIcon),
/// Tray icon error.
#[error("tray icon error: {0}")]
#[cfg(all(desktop, feature = "tray-icon"))]
#[cfg_attr(docsrs, doc(cfg(all(desktop, feature = "tray-icon"))))]
Tray(#[from] tray_icon::Error),
/// Bad tray icon error.
#[error(transparent)]
#[cfg(all(desktop, feature = "tray-icon"))]
#[cfg_attr(docsrs, doc(cfg(all(desktop, feature = "tray-icon"))))]
BadTrayIcon(#[from] tray_icon::BadIcon),
/// Path does not have a parent.
#[error("path does not have a parent")]
NoParent,
/// Path does not have an extension.
#[error("path does not have an extension")]
NoExtension,
/// Path does not have a basename.
#[error("path does not have a basename")]
NoBasename,
/// Path is not valid UTF-8 and cannot be represented in a URL.
#[error("path is not valid UTF-8: {}", .0.display())]
NonUtf8Path(std::path::PathBuf),
/// Cannot resolve current directory.
#[error("failed to read current dir: {0}")]
CurrentDir(std::io::Error),
/// Unknown path.
#[cfg(not(target_os = "android"))]
#[error("unknown path")]
UnknownPath,
/// Failed to invoke mobile plugin.
#[cfg(target_os = "android")]
#[error(transparent)]
PluginInvoke(#[from] crate::plugin::mobile::PluginInvokeError),
/// window not found.
#[error("window not found")]
WindowNotFound,
/// The resource id is invalid.
#[error("The resource id {0} is invalid.")]
BadResourceId(crate::resources::ResourceId),
/// The anyhow crate error.
#[error(transparent)]
Anyhow(#[from] anyhow::Error),
/// webview not found.
#[error("webview not found")]
WebviewNotFound,
/// API requires the unstable feature flag.
#[error("this feature requires the `unstable` flag on Cargo.toml")]
UnstableFeatureNotSupported,
/// Failed to deserialize scope object.
#[error("error deserializing scope: {0}")]
CannotDeserializeScope(Box<dyn std::error::Error + Send + Sync>),
/// Failed to get a raw handle.
#[error(transparent)]
RawHandleError(#[from] raw_window_handle::HandleError),
/// Something went wrong with the CSPRNG.
#[error("unable to generate random bytes from the operating system: {0}")]
Csprng(getrandom::Error),
/// Bad `__TAURI_INVOKE_KEY__` value received in ipc message.
#[error("bad __TAURI_INVOKE_KEY__ value received in ipc message")]
InvokeKey,
/// Illegal event name.
#[error("only alphanumeric, '-', '/', ':', '_' permitted for event names: {0:?}")]
IllegalEventName(String),
/// tokio oneshot channel failed to receive message
#[error(transparent)]
TokioOneshotRecv(#[from] tokio::sync::oneshot::error::RecvError),
/// Unexpected menu kind passed to menu/tray plugin command
#[error("Unexpected menu kind")]
UnexpectedMenuKind,
/// [`crate::image::Image::from_icon_resource`] failed
#[cfg(windows)]
#[cfg_attr(docsrs, doc(cfg(windows)))]
#[error("Can not load Image from icon resources: {0}")]
ImageFromResource(windows::core::Error),
}
impl From<getrandom::Error> for Error {
fn from(value: getrandom::Error) -> Self {
Self::Csprng(value)
}
}
/// `Result<T, ::tauri::Error>`
pub type Result<T> = std::result::Result<T, Error>;
#[cfg(test)]
mod tests {
#[test]
fn error_is_send_sync() {
crate::test_utils::assert_send::<super::Error>();
crate::test_utils::assert_sync::<super::Error>();
}
}

View File

@@ -0,0 +1,34 @@
# Tauri Event System Specification
## Emitters
Emitters can emit to any and all listeners.
- `App` and `AppHandle`
- `Window`
- `Webview`
- `WebviewWindow`
- Any type that implements `Manager` trait.
## Emit functions
- `emit`: emits an event to all listeners.
- `emit_to`: emits an event to a specified target.
- `emit_filter`: emits an event to targets based on a filtering callback.
## Listeners
Emitters can emit to any and all listeners.
- `App` and `AppHandle`
- `Window`
- `Webview`
- `WebviewWindow`
- Any type that implements `Manager` trait but is limited to only using `listen_any/once_any`.
## Listen functions
- `listen`: Listens to all events targeting this listener type only.
- `once`: Listens to a single event targeting this listener type only.
- `listen_any` (available only through `Manager` trait): Listens to all events to any target (aka event sniffer).
- `once_any` (available only through `Manager` trait): Listens to a single event to any target (aka event sniffer).

View File

@@ -0,0 +1,70 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use serde::{Deserialize, Deserializer};
/// Checks if an event name is valid.
fn is_event_name_valid(event: &str) -> bool {
event
.chars()
.all(|c| c.is_alphanumeric() || c == '-' || c == '/' || c == ':' || c == '_')
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub(crate) struct EventName<S = String>(S);
impl Copy for EventName<&str> {}
impl<S: AsRef<str>> EventName<S> {
pub(crate) fn new(s: S) -> crate::Result<EventName<S>> {
if !is_event_name_valid(s.as_ref()) {
return Err(crate::Error::IllegalEventName(s.as_ref().to_string()));
}
Ok(EventName(s))
}
pub(crate) fn as_str_event(&self) -> EventName<&str> {
EventName(self.0.as_ref())
}
pub(crate) fn as_str(&self) -> &str {
self.0.as_ref()
}
}
impl<S: std::fmt::Display> std::fmt::Display for EventName<S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl EventName<&'static str> {
// this convenience method is for using in const contexts to discharge the preconditions
// &'static prevents using this function accidentally with dynamically built string slices
pub(crate) const fn from_str(s: &'static str) -> EventName<&'static str> {
EventName(s)
}
}
impl EventName<&str> {
pub fn into_owned(self) -> EventName {
EventName(self.0.to_string())
}
}
impl<'de> Deserialize<'de> for EventName {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let event_id = String::deserialize(deserializer)?;
if is_event_name_valid(&event_id) {
Ok(EventName(event_id))
} else {
Err(serde::de::Error::custom(
"Event name must include only alphanumeric characters, `-`, `/`, `:` and `_`.",
))
}
}
}

View File

@@ -0,0 +1,10 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
// eslint-disable-next-line
Object.defineProperty(window, '__TAURI_EVENT_PLUGIN_INTERNALS__', {
value: {
unregisterListener: __RAW_unregister_listener_function__
}
})

View File

@@ -0,0 +1,488 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use crate::{Runtime, Webview};
use super::{EmitArgs, Event, EventId, EventTarget};
use std::{
boxed::Box,
cell::Cell,
collections::{HashMap, HashSet},
sync::{
Arc, Mutex,
atomic::{AtomicU32, Ordering},
},
};
/// What to do with the pending handler when resolving it?
enum Pending {
Unlisten(EventId),
Listen {
id: EventId,
event: crate::EventName,
handler: Handler,
},
Emit(EmitArgs),
RemoveForTarget(EventTarget),
}
/// Stored in [`Listeners`] to be called upon, when the event that stored it, is triggered.
struct Handler {
target: EventTarget,
callback: Box<dyn Fn(Event) + Send>,
}
impl Handler {
fn new<F: Fn(Event) + Send + 'static>(target: EventTarget, callback: F) -> Self {
Self {
target,
callback: Box::new(callback),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct JsHandler {
target: EventTarget,
id: EventId,
}
impl JsHandler {
fn new(target: EventTarget, id: EventId) -> Self {
Self { target, id }
}
}
type WebviewLabel = String;
/// Holds event handlers and pending event handlers, along with the salts associating them.
struct InnerListeners {
pending: Mutex<Vec<Pending>>,
handlers: Mutex<HashMap<crate::EventName, HashMap<EventId, Handler>>>,
js_event_listeners: Mutex<HashMap<WebviewLabel, HashMap<crate::EventName, HashSet<JsHandler>>>>,
function_name: &'static str,
listeners_object_name: &'static str,
next_event_id: Arc<AtomicU32>,
}
/// A self-contained event manager.
#[derive(Clone)]
pub struct Listeners {
inner: Arc<InnerListeners>,
}
impl Default for Listeners {
fn default() -> Self {
Self {
inner: Arc::new(InnerListeners {
pending: Mutex::default(),
handlers: Mutex::default(),
js_event_listeners: Mutex::default(),
function_name: "__internal_unstable_listeners_function_id__",
listeners_object_name: "__internal_unstable_listeners_object_id__",
next_event_id: Default::default(),
}),
}
}
}
impl Listeners {
pub(crate) fn next_event_id(&self) -> EventId {
self.inner.next_event_id.fetch_add(1, Ordering::Relaxed)
}
/// Function name to represent the JavaScript event function.
pub(crate) fn function_name(&self) -> &str {
self.inner.function_name
}
/// Listener object name to represent the JavaScript event listener object.
pub(crate) fn listeners_object_name(&self) -> &str {
self.inner.listeners_object_name
}
/// Insert a pending event action to the queue.
fn insert_pending(&self, action: Pending) {
self
.inner
.pending
.lock()
.expect("poisoned pending event queue")
.push(action)
}
/// Finish all pending event actions.
fn flush_pending(&self) -> crate::Result<()> {
let pending = {
let mut lock = self
.inner
.pending
.lock()
.expect("poisoned pending event queue");
std::mem::take(&mut *lock)
};
for action in pending {
match action {
Pending::Unlisten(id) => self.unlisten(id),
Pending::Listen { id, event, handler } => self.listen_with_id(id, event, handler),
Pending::Emit(args) => self.emit(args)?,
Pending::RemoveForTarget(target) => self.remove_listeners_for_target(target),
}
}
Ok(())
}
fn listen_with_id(&self, id: EventId, event: crate::EventName, handler: Handler) {
match self.inner.handlers.try_lock() {
Err(_) => self.insert_pending(Pending::Listen { id, event, handler }),
Ok(mut lock) => {
lock.entry(event).or_default().insert(id, handler);
}
}
}
/// Adds an event listener.
pub(crate) fn listen<F: Fn(Event) + Send + 'static>(
&self,
event: crate::EventName,
target: EventTarget,
handler: F,
) -> EventId {
let id = self.next_event_id();
let handler = Handler::new(target, handler);
self.listen_with_id(id, event, handler);
id
}
/// Listen to an event and immediately unlisten.
pub(crate) fn once<F: FnOnce(Event) + Send + 'static>(
&self,
event: crate::EventName,
target: EventTarget,
handler: F,
) -> EventId {
let self_ = self.clone();
let handler = Cell::new(Some(handler));
self.listen(event, target, move |event| {
let id = event.id;
self_.unlisten(id);
let handler = handler
.take()
.expect("attempted to call handler more than once");
handler(event);
})
}
/// Removes an event listener.
pub(crate) fn unlisten(&self, id: EventId) {
match self.inner.handlers.try_lock() {
Err(_) => self.insert_pending(Pending::Unlisten(id)),
Ok(mut lock) => lock.values_mut().for_each(|handler| {
handler.remove(&id);
}),
}
}
/// Removes every Rust-side event listener registered for the exact given target.
///
/// Called when a window or webview is destroyed so its listeners don't leak.
/// Global targets such as [`EventTarget::App`], [`EventTarget::Any`] and [`EventTarget::AnyLabel`] are never matched here.
fn remove_listeners_for_target(&self, target: EventTarget) {
match self.inner.handlers.try_lock() {
Err(_) => self.insert_pending(Pending::RemoveForTarget(target)),
Ok(mut lock) => {
for handlers in lock.values_mut() {
handlers.retain(|_, handler| handler.target != target);
}
lock.retain(|_, handlers| !handlers.is_empty());
}
}
}
/// Emits the given event with its payload based on a filter.
pub(crate) fn emit_filter<F>(&self, emit_args: EmitArgs, filter: Option<F>) -> crate::Result<()>
where
F: Fn(&EventTarget) -> bool,
{
let mut maybe_pending = false;
match self.inner.handlers.try_lock() {
Err(_) => self.insert_pending(Pending::Emit(emit_args)),
Ok(lock) => {
if let Some(handlers) = lock.get(&emit_args.event) {
let handlers = handlers.iter();
let handlers = handlers.filter(|(_, h)| match_any_or_filter(&h.target, &filter));
for (&id, Handler { callback, .. }) in handlers {
maybe_pending = true;
(callback)(Event::new(id, emit_args.payload.clone()))
}
}
}
}
if maybe_pending {
self.flush_pending()?;
}
Ok(())
}
/// Emits the given event with its payload.
pub(crate) fn emit(&self, emit_args: EmitArgs) -> crate::Result<()> {
self.emit_filter(emit_args, None::<&dyn Fn(&EventTarget) -> bool>)
}
pub(crate) fn listen_js(
&self,
event: crate::EventName<&str>,
source_webview_label: &str,
target: EventTarget,
id: EventId,
) {
let event = event.into_owned();
let mut listeners = self.inner.js_event_listeners.lock().unwrap();
listeners
.entry(source_webview_label.to_string())
.or_default()
.entry(event)
.or_default()
.insert(JsHandler::new(target, id));
}
pub(crate) fn unlisten_js(&self, event: crate::EventName<&str>, id: EventId) {
let event = event.into_owned();
let mut js_listeners = self.inner.js_event_listeners.lock().unwrap();
let js_listeners = js_listeners.values_mut();
for js_listeners in js_listeners {
if let Some(handlers) = js_listeners.get_mut(&event) {
handlers.retain(|h| h.id != id);
if handlers.is_empty() {
js_listeners.remove(&event);
}
}
}
}
/// Removes all JS event listeners registered from the given webview.
fn remove_webview_js_listeners(&self, webview_label: &str) {
self
.inner
.js_event_listeners
.lock()
.unwrap()
.remove(webview_label);
}
/// Removes all event listeners associated with a destroyed webview.
pub(crate) fn remove_webview_listeners(&self, label: &str) {
self.remove_webview_js_listeners(label);
self.remove_listeners_for_target(EventTarget::webview(label));
}
/// Removes all event listeners associated with a destroyed window.
pub(crate) fn remove_window_listeners(&self, label: &str) {
self.remove_listeners_for_target(EventTarget::window(label));
self.remove_listeners_for_target(EventTarget::webview_window(label));
}
pub(crate) fn has_js_listener<F: Fn(&EventTarget) -> bool>(
&self,
event: crate::EventName<&str>,
filter: F,
) -> bool {
let event = event.into_owned();
let js_listeners = self.inner.js_event_listeners.lock().unwrap();
js_listeners.values().any(|events| {
events
.get(&event)
.map(|handlers| handlers.iter().any(|handler| filter(&handler.target)))
.unwrap_or(false)
})
}
pub(crate) fn emit_js_filter<'a, R, I, F>(
&self,
mut webviews: I,
emit_args: &EmitArgs,
filter: Option<F>,
) -> crate::Result<()>
where
R: Runtime,
I: Iterator<Item = &'a Webview<R>>,
F: Fn(&EventTarget) -> bool,
{
let event = &emit_args.event;
let js_listeners = self.inner.js_event_listeners.lock().unwrap();
webviews.try_for_each(|webview| {
if let Some(handlers) = js_listeners.get(webview.label()).and_then(|s| s.get(event)) {
let ids = handlers
.iter()
.filter(|handler| match_any_or_filter(&handler.target, &filter))
.map(|handler| handler.id)
.collect::<Vec<_>>();
webview.emit_js(emit_args, &ids)?;
}
Ok(())
})
}
pub(crate) fn emit_js<'a, R, I>(&self, webviews: I, emit_args: &EmitArgs) -> crate::Result<()>
where
R: Runtime,
I: Iterator<Item = &'a Webview<R>>,
{
self.emit_js_filter(webviews, emit_args, None::<&dyn Fn(&EventTarget) -> bool>)
}
}
#[inline(always)]
fn match_any_or_filter<F: Fn(&EventTarget) -> bool>(
target: &EventTarget,
filter: &Option<F>,
) -> bool {
*target == EventTarget::Any || filter.as_ref().map(|f| f(target)).unwrap_or(true)
}
#[cfg(test)]
mod test {
use super::*;
use crate::event::EventTarget;
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 = crate::EventName::new(e).unwrap();
// pass e and an dummy func into listen
listeners.listen(key.clone(), EventTarget::Any, event_fn);
// lock mutex
let l = listeners.inner.handlers.lock().unwrap();
// check if the generated key is in the map
assert!(l.contains_key(&key));
}
// 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();
let key = crate::EventName::new(e).unwrap();
// pass e and an dummy func into listen
listeners.listen(key.clone(), EventTarget::Any, event_fn);
// lock mutex
let mut l = listeners.inner.handlers.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();
let key = crate::EventName::new(e).unwrap();
// call listen with key and the event_fn dummy func
listeners.listen(key.clone(), EventTarget::Any, event_fn);
// call on event with key and d.
listeners.emit(EmitArgs::new(key.as_str_event(), &d)?)?;
// lock the mutex
let l = listeners.inner.handlers.lock().unwrap();
// assert that the key is contained in the listeners map
assert!(l.contains_key(&key));
}
}
#[test]
fn event_no_deadlocks() {
let listeners = Listeners::default();
let listeners_clone = listeners.clone();
let event = crate::EventName::new("test-event".to_owned()).unwrap();
let event_clone = event.clone();
listeners.once(event.clone(), EventTarget::Any, move |_event| {
listeners_clone
.emit(EmitArgs::new(event_clone.as_str_event(), &()).unwrap())
.unwrap();
});
listeners
.emit(EmitArgs::new(event.as_str_event(), &()).unwrap())
.unwrap();
}
#[test]
fn listeners_removed_on_webview_close() {
let listeners = Listeners::default();
let event = crate::EventName::new("test-event".to_owned()).unwrap();
let webview_label = "main";
let window = listeners.listen(event.clone(), EventTarget::window(webview_label), event_fn);
// Same label as the window, but a different target kind (multi-webview mode).
let webview = listeners.listen(event.clone(), EventTarget::webview(webview_label), event_fn);
let webview_window = listeners.listen(
event.clone(),
EventTarget::webview_window(webview_label),
event_fn,
);
let other = listeners.listen(event.clone(), EventTarget::webview("other"), event_fn);
let app = listeners.listen(event.clone(), EventTarget::App, event_fn);
let any = listeners.listen(event.clone(), EventTarget::Any, event_fn);
let labeled = listeners.listen(event.clone(), EventTarget::labeled(webview_label), event_fn);
let id = listeners.next_event_id();
listeners.listen_js(
event.as_str_event(),
webview_label,
EventTarget::webview(webview_label),
id,
);
assert!(listeners.has_js_listener(event.as_str_event(), |_| true));
listeners.remove_webview_listeners(webview_label);
assert!(!listeners.has_js_listener(event.as_str_event(), |_| true));
let lock = listeners.inner.handlers.lock().unwrap();
let remaining: HashSet<EventId> = lock.get(&event).unwrap().keys().copied().collect();
// only the exact `Webview { label: "main" }` listener is gone
assert!(!remaining.contains(&webview));
// same label but different kind is kept
assert!(remaining.contains(&window));
assert!(remaining.contains(&webview_window));
// unrelated and global listeners are kept
assert!(remaining.contains(&other));
assert!(remaining.contains(&app));
assert!(remaining.contains(&any));
assert!(remaining.contains(&labeled));
}
}

View File

@@ -0,0 +1,258 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
mod listener;
pub(crate) mod plugin;
use std::{convert::Infallible, str::FromStr};
pub(crate) use listener::Listeners;
use serde::{Deserialize, Serialize};
mod event_name;
pub(crate) use event_name::EventName;
use crate::ipc::CallbackFn;
/// Unique id of an event.
pub type EventId = u32;
/// Event Target
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
#[serde(tag = "kind")]
#[non_exhaustive]
pub enum EventTarget {
/// Any and all event targets.
Any,
/// Any [`Window`](crate::Window), [`Webview`](crate::Webview) or [`WebviewWindow`](crate::WebviewWindow) that have this label.
AnyLabel {
/// Target label.
label: String,
},
/// [`App`](crate::App) and [`AppHandle`](crate::AppHandle) targets.
App,
/// [`Window`](crate::Window) target.
Window {
/// window label.
label: String,
},
/// [`Webview`](crate::Webview) target.
Webview {
/// webview label.
label: String,
},
/// [`WebviewWindow`](crate::WebviewWindow) target.
WebviewWindow {
/// webview window label.
label: String,
},
}
impl EventTarget {
/// [`Self::Any`] target.
pub fn any() -> Self {
Self::Any
}
/// [`Self::App`] target.
pub fn app() -> Self {
Self::App
}
/// [`Self::AnyLabel`] target.
pub fn labeled(label: impl Into<String>) -> Self {
Self::AnyLabel {
label: label.into(),
}
}
/// [`Self::Window`] target.
pub fn window(label: impl Into<String>) -> Self {
Self::Window {
label: label.into(),
}
}
/// [`Self::Webview`] target.
pub fn webview(label: impl Into<String>) -> Self {
Self::Webview {
label: label.into(),
}
}
/// [`Self::WebviewWindow`] target.
pub fn webview_window(label: impl Into<String>) -> Self {
Self::WebviewWindow {
label: label.into(),
}
}
}
impl<T: AsRef<str>> From<T> for EventTarget {
fn from(value: T) -> Self {
Self::AnyLabel {
label: value.as_ref().to_string(),
}
}
}
impl FromStr for EventTarget {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self::AnyLabel {
label: s.to_string(),
})
}
}
/// Serialized emit arguments.
#[derive(Clone)]
pub struct EmitArgs {
/// event name.
event: EventName,
/// Serialized payload.
payload: String,
}
impl EmitArgs {
pub fn new<S: Serialize>(event: EventName<&str>, payload: &S) -> crate::Result<Self> {
#[cfg(feature = "tracing")]
let _span = tracing::debug_span!("window::emit::serialize").entered();
Ok(EmitArgs {
event: event.into_owned(),
payload: serde_json::to_string(payload)?,
})
}
pub fn new_str(event: EventName<&str>, payload: String) -> crate::Result<Self> {
#[cfg(feature = "tracing")]
let _span = tracing::debug_span!("window::emit::json").entered();
Ok(EmitArgs {
event: event.into_owned(),
payload,
})
}
}
/// An event that was emitted.
#[derive(Debug, Clone)]
pub struct Event {
id: EventId,
data: String,
}
impl Event {
fn new(id: EventId, data: String) -> Self {
Self { id, data }
}
/// The [`EventId`] of the handler that was triggered.
pub fn id(&self) -> EventId {
self.id
}
/// The event payload.
pub fn payload(&self) -> &str {
&self.data
}
}
pub(crate) fn listen_js_script(
listeners_object_name: &str,
serialized_target: &str,
event: EventName<&str>,
event_id: EventId,
handler: CallbackFn,
) -> String {
let handler_id = handler.0;
format!(
"(function () {{
if (window['{listeners_object_name}'] === void 0) {{
Object.defineProperty(window, '{listeners_object_name}', {{ value: Object.create(null) }});
}}
if (window['{listeners_object_name}']['{event}'] === void 0) {{
Object.defineProperty(window['{listeners_object_name}'], '{event}', {{ value: Object.create(null) }});
}}
const eventListeners = window['{listeners_object_name}']['{event}']
const listener = {{
target: {serialized_target},
handlerId: {handler_id}
}};
Object.defineProperty(eventListeners, '{event_id}', {{ value: listener, configurable: true }});
}})()
",
)
}
pub(crate) fn emit_js_script(
event_emit_function_name: &str,
emit_args: &EmitArgs,
serialized_ids: &str,
) -> crate::Result<String> {
Ok(format!(
"(function () {{ const fn = window['{}']; fn && fn({{event: '{}', payload: {}}}, {ids}) }})()",
event_emit_function_name,
emit_args.event,
emit_args.payload,
ids = serialized_ids,
))
}
pub(crate) fn unlisten_js_script(
listeners_object_name: &str,
event_arg: &str,
event_id_arg: &str,
) -> String {
format!(
"(function () {{
const listeners = (window['{listeners_object_name}'] || {{}})[{event_arg}]
if (listeners) {{
const listener = listeners[{event_id_arg}]
if (listener) {{
window.__TAURI_INTERNALS__.unregisterCallback(listener.handlerId)
}}
}}
}})()
",
)
}
pub(crate) fn event_initialization_script(function_name: &str, listeners: &str) -> String {
format!(
"Object.defineProperty(window, '{function_name}', {{
value: function (eventData, ids) {{
const listeners = (window['{listeners}'] && window['{listeners}'][eventData.event]) || []
for (const id of ids) {{
const listener = listeners[id]
if (listener) {{
eventData.id = id
window.__TAURI_INTERNALS__.runCallback(listener.handlerId, eventData)
}}
}}
}}
}});
"
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_illegal_event_name() {
let s = EventName::new("some\r illegal event name")
.unwrap_err()
.to_string();
assert_eq!(
"only alphanumeric, '-', '/', ':', '_' permitted for event names: \"some\\r illegal event name\"",
s
);
}
}

View File

@@ -0,0 +1,82 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use serde_json::Value as JsonValue;
use serialize_to_javascript::{DefaultTemplate, Template, default_template};
use crate::plugin::{Builder, TauriPlugin};
use crate::{AppHandle, Emitter, Manager, Webview};
use crate::{EventId, Result, Runtime, command, ipc::CallbackFn};
use super::EventName;
use super::EventTarget;
#[command(root = "crate")]
async fn listen<R: Runtime>(
webview: Webview<R>,
event: EventName,
target: EventTarget,
handler: CallbackFn,
) -> Result<EventId> {
webview.listen_js(event.as_str_event(), target, handler)
}
#[command(root = "crate")]
async fn unlisten<R: Runtime>(
webview: Webview<R>,
event: EventName,
event_id: EventId,
) -> Result<()> {
webview.unlisten_js(event.as_str_event(), event_id)
}
#[command(root = "crate")]
async fn emit<R: Runtime>(
app: AppHandle<R>,
event: EventName,
payload: Option<JsonValue>,
) -> Result<()> {
app.emit(event.as_str(), payload)
}
#[command(root = "crate")]
async fn emit_to<R: Runtime>(
app: AppHandle<R>,
target: EventTarget,
event: EventName,
payload: Option<JsonValue>,
) -> Result<()> {
app.emit_to(target, event.as_str(), payload)
}
/// Initializes the event plugin.
pub(crate) fn init<R: Runtime, M: Manager<R>>(manager: &M) -> TauriPlugin<R> {
let listeners = manager.manager().listeners();
#[derive(Template)]
#[default_template("./init.js")]
struct InitJavascript {
#[raw]
unregister_listener_function: String,
}
let init_script = InitJavascript {
unregister_listener_function: format!(
"(event, eventId) => {}",
crate::event::unlisten_js_script(listeners.listeners_object_name(), "event", "eventId")
),
};
Builder::new("event")
.invoke_handler(crate::generate_handler![
#![plugin(event)]
listen, unlisten, emit, emit_to
])
.js_init_script(
init_script
.render_default(&Default::default())
.unwrap()
.to_string(),
)
.build()
}

View File

@@ -0,0 +1,527 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
//! Image types used by this crate and also referenced by the JavaScript API layer.
pub(crate) mod plugin;
use std::borrow::Cow;
use std::sync::Arc;
#[cfg(windows)]
use windows::{
Win32::{
Foundation::{E_FAIL, ERROR_INVALID_PARAMETER, ERROR_NOT_SUPPORTED, WIN32_ERROR},
Graphics::Gdi::{
BI_RGB, BITMAPINFO, BITMAPINFOHEADER, CreateCompatibleDC, DIB_RGB_COLORS, DeleteDC,
GetDIBits, HBITMAP,
},
System::LibraryLoader::GetModuleHandleW,
UI::WindowsAndMessaging::{
GetIconInfo, GetSystemMetrics, HICON, ICONINFO, IMAGE_ICON, LR_DEFAULTCOLOR, LoadImageW,
SM_CXICON, SM_CYICON,
},
},
core::{Owned, PCWSTR},
};
use crate::{Resource, ResourceId, ResourceTable};
/// Identifies an icon resource embedded in the executable.
#[cfg(windows)]
#[cfg_attr(docsrs, doc(cfg(windows)))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum IconResource<'a> {
/// An integer resource identifier (`MAKEINTRESOURCE`).
Id(u16),
/// A string resource name.
Name(&'a str),
}
#[cfg(windows)]
impl From<u16> for IconResource<'_> {
fn from(id: u16) -> Self {
Self::Id(id)
}
}
#[cfg(windows)]
impl<'a> From<&'a str> for IconResource<'a> {
fn from(name: &'a str) -> Self {
Self::Name(name)
}
}
/// Loads the default window icon from the application icon resource, reporting failures.
///
/// Used by [`crate::generate_context!`]; not public API.
#[cfg(windows)]
#[doc(hidden)]
pub fn default_window_icon_from_app_icon_resource() -> Option<Image<'static>> {
// the window icon is drawn in the title bar and, as a fallback for the taskbar icon,
// at the system's large icon size (32x32 at 96 DPI, scaled with the system DPI),
// so pick the entry Windows would use for the taskbar instead of a larger one it has to shrink
// `GetSystemMetrics` returns 0 on failure
let metric = |index| match unsafe { GetSystemMetrics(index) } {
n if n > 0 => n as u32,
_ => 32,
};
let (width, height) = (metric(SM_CXICON), metric(SM_CYICON));
match Image::from_icon_resource(
crate::utils::platform::WINDOWS_APP_ICON_RESOURCE_ID,
width,
height,
) {
Ok(icon) => Some(icon),
Err(e) => {
// a logger is usually not installed yet when `generate_context!` runs
#[cfg(debug_assertions)]
eprintln!("failed to load the default window icon from the application icon resource: {e}");
log::warn!("failed to load the default window icon from the application icon resource: {e}");
None
}
}
}
#[cfg(windows)]
const BYTES_PER_PIXEL: usize = 4;
/// Reads `hbm` as a top-down 32bpp BGRA bitmap of the given dimensions.
///
/// # Safety
///
/// `hbm` must be a valid bitmap handle and `width` and `height` must be positive.
#[cfg(windows)]
unsafe fn read_bgra(hbm: HBITMAP, width: i32, height: i32) -> crate::Result<Vec<u8>> {
let image_bytes = (width as usize)
.checked_mul(height as usize)
.and_then(|n| n.checked_mul(BYTES_PER_PIXEL))
.ok_or_else(|| resource_error(ERROR_INVALID_PARAMETER, "image size overflows usize"))?;
let mut bgra = vec![0u8; image_bytes];
let mut bitmap_info = BITMAPINFO::default();
bitmap_info.bmiHeader.biSize = std::mem::size_of::<BITMAPINFOHEADER>() as _;
bitmap_info.bmiHeader.biWidth = width;
// negative value for top-down
bitmap_info.bmiHeader.biHeight = -height;
bitmap_info.bmiHeader.biBitCount = (BYTES_PER_PIXEL * 8) as u16;
bitmap_info.bmiHeader.biPlanes = 1;
bitmap_info.bmiHeader.biCompression = BI_RGB.0;
unsafe {
let hdc = CreateCompatibleDC(None);
let scan_lines = GetDIBits(
hdc,
hbm,
0,
height as u32,
Some(bgra.as_mut_ptr() as _),
&mut bitmap_info,
DIB_RGB_COLORS,
);
// capture the error before `DeleteDC` can overwrite it
let error = (scan_lines != height).then(|| {
last_error_or(&format!(
"GetDIBits copied {scan_lines} of {height} scan lines"
))
});
let _ = DeleteDC(hdc);
if let Some(error) = error {
return Err(crate::Error::ImageFromResource(error));
}
}
Ok(bgra)
}
#[cfg(windows)]
fn resource_error(code: WIN32_ERROR, message: &str) -> crate::Error {
crate::Error::ImageFromResource(windows::core::Error::new(code.to_hresult(), message))
}
/// Returns the calling thread's last error, or a generic `E_FAIL` with `message`
/// when no error code was set (GDI functions do not always set one).
#[cfg(windows)]
fn last_error_or(message: &str) -> windows::core::Error {
let error = windows::core::Error::from_thread();
if error.code().is_ok() {
windows::core::Error::new(E_FAIL, message)
} else {
error
}
}
/// An RGBA Image in row-major order from top to bottom.
#[derive(Clone)]
pub struct Image<'a> {
rgba: Cow<'a, [u8]>,
width: u32,
height: u32,
}
impl std::fmt::Debug for Image<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Image")
.field(
"rgba",
// Reduces the debug size compared to the derived default, as the default
// would format the raw bytes as numbers `[0, 0, 0, 0]` for 1 pixel.
// The custom format doesn't grow as much with larger images:
// `Image { rgba: Cow::Borrowed([u8; 4096]), width: 32, height: 32 }`
&format_args!(
"Cow::{}([u8; {}])",
match &self.rgba {
Cow::Borrowed(_) => "Borrowed",
Cow::Owned(_) => "Owned",
},
self.rgba.len()
),
)
.field("width", &self.width)
.field("height", &self.height)
.finish()
}
}
impl Resource for Image<'static> {}
impl Image<'static> {
/// Creates a new Image using RGBA data, in row-major order from top to bottom, and with specified width and height.
///
/// Similar to [`Self::new`] but avoids cloning the rgba data to get an owned Image.
pub const fn new_owned(rgba: Vec<u8>, width: u32, height: u32) -> Self {
Self {
rgba: Cow::Owned(rgba),
width,
height,
}
}
}
impl<'a> Image<'a> {
/// Creates a new Image using RGBA data, in row-major order from top to bottom, and with specified width and height.
pub const fn new(rgba: &'a [u8], width: u32, height: u32) -> Self {
Self {
rgba: Cow::Borrowed(rgba),
width,
height,
}
}
/// Creates a new image using the provided bytes.
///
/// Only `ico` and `png` are supported (based on activated feature flag).
#[cfg(any(feature = "image-ico", feature = "image-png"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "image-ico", feature = "image-png"))))]
pub fn from_bytes(bytes: &[u8]) -> crate::Result<Self> {
let img = image::load_from_memory(bytes)?;
let (width, height) = (img.width(), img.height());
Ok(Self {
rgba: Cow::Owned(img.into_rgba8().into_raw()),
width,
height,
})
}
/// Creates a new image using the provided path.
///
/// Only `ico` and `png` are supported (based on activated feature flag).
#[cfg(any(feature = "image-ico", feature = "image-png"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "image-ico", feature = "image-png"))))]
pub fn from_path<P: AsRef<std::path::Path>>(path: P) -> crate::Result<Self> {
let bytes = std::fs::read(path)?;
Self::from_bytes(&bytes)
}
/// Creates a new image from the application icon embedded in the executable of the current process.
///
/// The application icon is the one `tauri-build` embeds with the
/// [`WINDOWS_APP_ICON_RESOURCE_ID`](crate::utils::platform::WINDOWS_APP_ICON_RESOURCE_ID) id,
/// this could change in the future.
#[cfg(windows)]
#[cfg_attr(docsrs, doc(cfg(windows)))]
pub fn from_app_icon_resource(size: u32) -> crate::Result<Self> {
Image::from_icon_resource(
crate::utils::platform::WINDOWS_APP_ICON_RESOURCE_ID,
size,
size,
)
}
/// Create a new image from an icon resource embedded in the executable of the current process.
///
/// Resources are looked up in the process executable (`GetModuleHandleW(NULL)`),
/// not in the DLL containing this code when tauri is built as a library.
///
/// **Note**: This might take ~2ms for [`LoadImageW`] to load the image for the first time.
///
/// ## Examples
///
/// The resource can be identified by its integer id or by its name, see [`IconResource`].
///
/// ```no_run
/// # use tauri::image::Image;
/// # fn main() -> tauri::Result<()> {
/// let icon = Image::from_icon_resource(1, 32, 32)?;
/// let icon = Image::from_icon_resource("icon", 32, 32)?;
/// # Ok(())
/// # }
/// ```
#[cfg(windows)]
#[cfg_attr(docsrs, doc(cfg(windows)))]
pub fn from_icon_resource<'r>(
resource: impl Into<IconResource<'r>>,
width: u32,
height: u32,
) -> crate::Result<Self> {
let (width_i32, height_i32) = match (i32::try_from(width), i32::try_from(height)) {
(Ok(w), Ok(h)) if w > 0 && h > 0 => (w, h),
_ => {
return Err(resource_error(
ERROR_INVALID_PARAMETER,
"width and height must be between 1 and i32::MAX",
));
}
};
// keeps the wide string alive for the `LoadImageW` call
let name: Vec<u16>;
let resource_id = match resource.into() {
// MAKEINTRESOURCE
IconResource::Id(id) => PCWSTR(id as usize as *const u16),
IconResource::Name(n) => {
name = n.encode_utf16().chain(std::iter::once(0)).collect();
PCWSTR(name.as_ptr())
}
};
let hicon = unsafe {
Owned::new(HICON(
LoadImageW(
Some(
GetModuleHandleW(PCWSTR::null())
.map_err(crate::Error::ImageFromResource)?
.into(),
),
resource_id,
IMAGE_ICON,
width_i32,
height_i32,
LR_DEFAULTCOLOR,
)
.map_err(crate::Error::ImageFromResource)?
.0,
))
};
let mut icon_info = ICONINFO::default();
unsafe { GetIconInfo(*hicon, &mut icon_info).map_err(crate::Error::ImageFromResource)? };
let hbm_mask = unsafe { Owned::new(icon_info.hbmMask) };
let hbm_color = unsafe { Owned::new(icon_info.hbmColor) };
// monochrome icons only have a mask bitmap (AND mask stacked on top of the XOR mask)
if hbm_color.is_invalid() {
return Err(resource_error(
ERROR_NOT_SUPPORTED,
"monochrome icons are not supported",
));
}
let mut bgra = unsafe { read_bgra(*hbm_color, width_i32, height_i32)? };
// Color bitmaps without an alpha channel (e.g. 24bpp icons) read back with alpha = 0 on every pixel,
// so recover the alpha channel from the AND mask: a set bit means the pixel is transparent.
if bgra
.as_chunks::<BYTES_PER_PIXEL>()
.0
.iter()
.all(|px| px[3] == 0)
{
let mask = unsafe { read_bgra(*hbm_mask, width_i32, height_i32)? };
for (px, mask) in bgra
.as_chunks_mut::<BYTES_PER_PIXEL>()
.0
.iter_mut()
.zip(mask.as_chunks::<BYTES_PER_PIXEL>().0)
{
// the 1bpp mask expands to black (clear bit) or white (set bit)
px[3] = if mask[0] == 0 { 0xFF } else { 0 };
}
}
let rgba = {
for px in bgra.as_chunks_mut::<BYTES_PER_PIXEL>().0 {
// Swap Blue and Red channels
px.swap(0, 2);
}
bgra
};
Ok(Image::new_owned(rgba, width, height))
}
/// Returns the RGBA data for this image, in row-major order from top to bottom.
pub fn rgba(&'a self) -> &'a [u8] {
&self.rgba
}
/// Returns the width of this image.
pub fn width(&self) -> u32 {
self.width
}
/// Returns the height of this image.
pub fn height(&self) -> u32 {
self.height
}
/// Convert into a 'static owned [`Image`].
/// This will allocate.
pub fn to_owned(self) -> Image<'static> {
Image {
rgba: match self.rgba {
Cow::Owned(v) => Cow::Owned(v),
Cow::Borrowed(v) => Cow::Owned(v.to_vec()),
},
height: self.height,
width: self.width,
}
}
}
impl<'a> From<Image<'a>> for crate::runtime::Icon<'a> {
fn from(img: Image<'a>) -> Self {
Self {
rgba: img.rgba,
width: img.width,
height: img.height,
}
}
}
#[cfg(desktop)]
impl TryFrom<Image<'_>> for muda::Icon {
type Error = crate::Error;
fn try_from(img: Image<'_>) -> Result<Self, Self::Error> {
muda::Icon::from_rgba(img.rgba.into_owned(), img.width, img.height).map_err(Into::into)
}
}
#[cfg(all(desktop, feature = "tray-icon"))]
impl TryFrom<Image<'_>> for tray_icon::Icon {
type Error = crate::Error;
fn try_from(img: Image<'_>) -> Result<Self, Self::Error> {
tray_icon::Icon::from_rgba(img.rgba.into_owned(), img.width, img.height).map_err(Into::into)
}
}
/// An image type that accepts file paths, raw bytes, previously loaded images and image objects.
///
/// This type is meant to be used along the [transformImage](https://v2.tauri.app/reference/javascript/api/namespaceimage/#transformimage) API.
///
/// # Stability
///
/// The stability of the variants are not guaranteed, and matching against them is not recommended.
/// Use [`JsImage::into_img`] instead.
#[derive(serde::Deserialize)]
#[serde(untagged)]
#[non_exhaustive]
pub enum JsImage {
/// A reference to an image in the filesystem. This requires `image-ico` or `image-png` cargo features.
#[non_exhaustive]
Path(std::path::PathBuf),
/// ICO or PNG image in raw bytes. This requires `image-ico` or `image-png` cargo features.
#[non_exhaustive]
Bytes(Vec<u8>),
/// An image that was previously loaded with the API and is stored in the resource table.
#[non_exhaustive]
Resource(ResourceId),
/// Raw RGBA definition of an image.
#[non_exhaustive]
Rgba {
/// Image bytes.
rgba: Vec<u8>,
/// Image width.
width: u32,
/// Image height.
height: u32,
},
}
impl JsImage {
/// Converts this intermediate image format into an actual [`Image`].
///
/// This will retrieve the image from the passed [`ResourceTable`] if it is [`JsImage::Resource`]
/// and will return an error if it doesn't exist in the passed [`ResourceTable`] so make sure
/// the passed [`ResourceTable`] is the same one used to store the image, usually this should be
/// the webview resources table.
pub fn into_img(self, resources_table: &ResourceTable) -> crate::Result<Arc<Image<'_>>> {
match self {
Self::Resource(rid) => resources_table.get::<Image<'static>>(rid),
#[cfg(any(feature = "image-ico", feature = "image-png"))]
Self::Path(path) => Image::from_path(path).map(Arc::new),
#[cfg(any(feature = "image-ico", feature = "image-png"))]
Self::Bytes(bytes) => Image::from_bytes(&bytes).map(Arc::new),
Self::Rgba {
rgba,
width,
height,
} => Ok(Arc::new(Image::new_owned(rgba, width, height))),
#[cfg(not(any(feature = "image-ico", feature = "image-png")))]
_ => Err(
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"expected RGBA image data, found {}",
match self {
JsImage::Path(_) => "a file path",
JsImage::Bytes(_) => "raw bytes",
_ => unreachable!(),
}
),
)
.into(),
),
}
}
}
#[cfg(all(test, windows))]
mod tests {
use super::{IconResource, Image, default_window_icon_from_app_icon_resource};
/// The test executable has no icon resources, so every lookup must fail with an error
/// (instead of panicking or returning a stretched placeholder).
#[test]
fn from_icon_resource_missing_resource_is_an_error() {
for resource in [
IconResource::Id(u16::MAX),
IconResource::Name("tauri-image-test-missing-icon"),
] {
let error = Image::from_icon_resource(resource, 32, 32).unwrap_err();
assert!(
matches!(error, crate::Error::ImageFromResource(_)),
"{resource:?}: {error:?}"
);
}
assert!(default_window_icon_from_app_icon_resource().is_none());
}
#[test]
fn from_icon_resource_rejects_invalid_sizes() {
for (width, height) in [(0, 32), (32, 0), (u32::MAX, 32), (32, i32::MAX as u32 + 1)] {
let error = Image::from_icon_resource(1, width, height).unwrap_err();
assert!(
matches!(error, crate::Error::ImageFromResource(_)),
"{width}x{height}: {error:?}"
);
}
}
}

View File

@@ -0,0 +1,88 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use serde::Serialize;
use crate::Manager;
use crate::plugin::{Builder, TauriPlugin};
use crate::{ResourceId, Runtime, Webview, command, image::Image};
#[command(root = "crate")]
fn new<R: Runtime>(
webview: Webview<R>,
rgba: Vec<u8>,
width: u32,
height: u32,
) -> crate::Result<ResourceId> {
let image = Image::new_owned(rgba, width, height);
let mut resources_table = webview.resources_table();
let rid = resources_table.add(image);
Ok(rid)
}
#[cfg(any(feature = "image-ico", feature = "image-png"))]
#[command(root = "crate")]
fn from_bytes<R: Runtime>(webview: Webview<R>, bytes: Vec<u8>) -> crate::Result<ResourceId> {
let image = Image::from_bytes(&bytes)?.to_owned();
let mut resources_table = webview.resources_table();
let rid = resources_table.add(image);
Ok(rid)
}
#[cfg(not(any(feature = "image-ico", feature = "image-png")))]
#[command(root = "crate")]
fn from_bytes() -> std::result::Result<(), &'static str> {
Err("from_bytes is only supported if the `image-ico` or `image-png` Cargo features are enabled")
}
#[cfg(any(feature = "image-ico", feature = "image-png"))]
#[command(root = "crate")]
fn from_path<R: Runtime>(
webview: Webview<R>,
path: std::path::PathBuf,
) -> crate::Result<ResourceId> {
let image = Image::from_path(path)?.to_owned();
let mut resources_table = webview.resources_table();
let rid = resources_table.add(image);
Ok(rid)
}
#[cfg(not(any(feature = "image-ico", feature = "image-png")))]
#[command(root = "crate")]
fn from_path() -> std::result::Result<(), &'static str> {
Err("from_path is only supported if the `image-ico` or `image-png` Cargo features are enabled")
}
#[command(root = "crate")]
fn rgba<R: Runtime>(webview: Webview<R>, rid: ResourceId) -> crate::Result<crate::ipc::Response> {
let resources_table = webview.resources_table();
let image = resources_table.get::<Image<'_>>(rid)?;
Ok(crate::ipc::Response::new(image.rgba().to_owned()))
}
#[derive(Serialize)]
struct Size {
width: u32,
height: u32,
}
#[command(root = "crate")]
fn size<R: Runtime>(webview: Webview<R>, rid: ResourceId) -> crate::Result<Size> {
let resources_table = webview.resources_table();
let image = resources_table.get::<Image<'_>>(rid)?;
Ok(Size {
width: image.width(),
height: image.height(),
})
}
/// Initializes the plugin.
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("image")
.invoke_handler(crate::generate_handler![
#![plugin(image)]
new, from_bytes, from_path, rgba, size
])
.build()
}

49
crates/tauri/src/ios.rs Normal file
View File

@@ -0,0 +1,49 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use swift_rs::{SRString, SwiftArg, swift};
use std::{
ffi::c_void,
os::raw::{c_char, c_int, c_ulonglong},
};
type PluginMessageCallbackFn = unsafe extern "C" fn(c_int, c_int, *const c_char);
pub struct PluginMessageCallback(pub PluginMessageCallbackFn);
impl<'a> SwiftArg<'a> for PluginMessageCallback {
type ArgType = PluginMessageCallbackFn;
unsafe fn as_arg(&'a self) -> Self::ArgType {
self.0
}
}
type ChannelSendDataCallbackFn = unsafe extern "C" fn(c_ulonglong, *const c_char);
pub struct ChannelSendDataCallback(pub ChannelSendDataCallbackFn);
impl<'a> SwiftArg<'a> for ChannelSendDataCallback {
type ArgType = ChannelSendDataCallbackFn;
unsafe fn as_arg(&'a self) -> Self::ArgType {
self.0
}
}
swift!(pub fn run_plugin_command(
id: i32,
name: &SRString,
method: &SRString,
data: &SRString,
callback: PluginMessageCallback,
send_channel_data_callback: ChannelSendDataCallback
));
swift!(pub fn register_plugin(
name: &SRString,
plugin: *const c_void,
config: &SRString,
webview: *const c_void
));
swift!(pub fn on_webview_created(webview: *const c_void, controller: *const c_void));
swift!(pub fn log_stdout());

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,170 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use serde::Serialize;
use tauri_utils::{
acl::{
Scopes,
capability::{Capability, CapabilityFile, PermissionEntry},
},
platform::Target,
};
/// A capability that can be added at runtime.
pub trait RuntimeCapability {
/// Creates the capability file.
fn build(self) -> CapabilityFile;
}
impl<T: AsRef<str>> RuntimeCapability for T {
fn build(self) -> CapabilityFile {
self.as_ref().parse().expect("invalid capability")
}
}
/// A builder for a [`Capability`].
pub struct CapabilityBuilder(Capability);
impl CapabilityBuilder {
/// Creates a new capability builder with a unique identifier.
pub fn new(identifier: impl Into<String>) -> Self {
Self(Capability {
identifier: identifier.into(),
description: "".into(),
remote: None,
local: true,
windows: Vec::new(),
webviews: Vec::new(),
permissions: Vec::new(),
platforms: None,
})
}
/// Allows this capability to be used by a remote URL.
pub fn remote(mut self, url: String) -> Self {
self
.0
.remote
.get_or_insert_with(Default::default)
.urls
.push(url);
self
}
/// Whether this capability is applied on local app URLs or not. Defaults to `true`.
pub fn local(mut self, local: bool) -> Self {
self.0.local = local;
self
}
/// Link this capability to the given window label.
pub fn window(mut self, window: impl Into<String>) -> Self {
self.0.windows.push(window.into());
self
}
/// Link this capability to the a list of window labels.
pub fn windows(mut self, windows: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.0.windows.extend(windows.into_iter().map(|w| w.into()));
self
}
/// Link this capability to the given webview label.
pub fn webview(mut self, webview: impl Into<String>) -> Self {
self.0.webviews.push(webview.into());
self
}
/// Link this capability to the a list of window labels.
pub fn webviews(mut self, webviews: impl IntoIterator<Item = impl Into<String>>) -> Self {
self
.0
.webviews
.extend(webviews.into_iter().map(|w| w.into()));
self
}
/// Add a new permission to this capability.
pub fn permission(mut self, permission: impl Into<String>) -> Self {
let permission = permission.into();
self.0.permissions.push(PermissionEntry::PermissionRef(
permission
.clone()
.try_into()
.unwrap_or_else(|_| panic!("invalid permission identifier '{permission}'")),
));
self
}
/// Add a new scoped permission to this capability.
pub fn permission_scoped<T: Serialize>(
mut self,
permission: impl Into<String>,
allowed: Vec<T>,
denied: Vec<T>,
) -> Self {
let permission = permission.into();
let identifier = permission
.clone()
.try_into()
.unwrap_or_else(|_| panic!("invalid permission identifier '{permission}'"));
let allowed_scope = allowed
.into_iter()
.map(|a| {
serde_json::to_value(a)
.expect("failed to serialize scope")
.into()
})
.collect();
let denied_scope = denied
.into_iter()
.map(|a| {
serde_json::to_value(a)
.expect("failed to serialize scope")
.into()
})
.collect();
let scope = Scopes {
allow: Some(allowed_scope),
deny: Some(denied_scope),
};
self
.0
.permissions
.push(PermissionEntry::ExtendedPermission { identifier, scope });
self
}
/// Adds a target platform for this capability.
///
/// By default all platforms are applied.
pub fn platform(mut self, platform: Target) -> Self {
self
.0
.platforms
.get_or_insert_with(Default::default)
.push(platform);
self
}
/// Adds target platforms for this capability.
///
/// By default all platforms are applied.
pub fn platforms(mut self, platforms: impl IntoIterator<Item = Target>) -> Self {
self
.0
.platforms
.get_or_insert_with(Default::default)
.extend(platforms);
self
}
}
impl RuntimeCapability for CapabilityBuilder {
fn build(self) -> CapabilityFile {
CapabilityFile::Capability(self.0)
}
}

View File

@@ -0,0 +1,530 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::{
collections::HashMap,
str::FromStr,
sync::{
Arc, Mutex,
atomic::{AtomicU32, AtomicUsize, Ordering},
},
};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::{
Manager, Runtime, State, Webview, command,
ipc::{CommandArg, CommandItem},
plugin::{Builder as PluginBuilder, TauriPlugin},
};
use super::{
CallbackFn, InvokeError, InvokeResponseBody, IpcResponse, Request, Response,
format_callback::format_raw_js,
};
pub const IPC_PAYLOAD_PREFIX: &str = "__CHANNEL__:";
// TODO: Change this to `channel` in v3
pub const CHANNEL_PLUGIN_NAME: &str = "__TAURI_CHANNEL__";
// TODO: Change this to `plugin:channel|fetch` in v3
pub const FETCH_CHANNEL_DATA_COMMAND: &str = "plugin:__TAURI_CHANNEL__|fetch";
const CHANNEL_ID_HEADER_NAME: &str = "Tauri-Channel-Id";
/// Maximum size a JSON we should send directly without going through the fetch process
// 8192 byte JSON payload runs roughly 2x faster through eval than through fetch on WebView2 v135
const MAX_JSON_DIRECT_EXECUTE_THRESHOLD: usize = 8192;
// 1024 byte payload runs roughly 30% faster through eval than through fetch on macOS
const MAX_RAW_DIRECT_EXECUTE_THRESHOLD: usize = 1024;
static CHANNEL_COUNTER: AtomicU32 = AtomicU32::new(0);
/// Maps channel ids to pending data that must be sent to the JavaScript side via the IPC.
///
/// Scoped per webview: each webview has its own id sequence and lookups only
/// ever touch the calling webview's entries.
#[derive(Default, Clone)]
pub struct ChannelDataIpcQueue(Arc<Mutex<HashMap<String, WebviewChannelDataQueue>>>);
/// Pending channel data of one webview.
#[derive(Default)]
struct WebviewChannelDataQueue {
next_id: u32,
entries: HashMap<u32, InvokeResponseBody>,
}
impl ChannelDataIpcQueue {
/// Stores the body for the given webview and returns its id.
///
/// Ids are sequential per webview and only address that webview's entries.
fn insert(&self, webview_label: &str, body: InvokeResponseBody) -> u32 {
let mut cache = self.0.lock().unwrap();
let queue = cache.entry(webview_label.to_string()).or_default();
let data_id = loop {
let candidate = queue.next_id;
queue.next_id = queue.next_id.wrapping_add(1);
if !queue.entries.contains_key(&candidate) {
break candidate;
}
};
queue.entries.insert(data_id, body);
data_id
}
/// Removes and returns the entry with the given id from the given webview's queue.
fn remove(&self, webview_label: &str, data_id: u32) -> Option<InvokeResponseBody> {
self
.0
.lock()
.unwrap()
.get_mut(webview_label)?
.entries
.remove(&data_id)
}
/// Drops all entries of the given webview.
pub(crate) fn remove_webview_entries(&self, webview_label: &str) {
self.0.lock().unwrap().remove(webview_label);
}
}
/// An IPC channel, used to stream values from Rust to the frontend.
///
/// A command can only resolve once, so a channel is how you push an arbitrary number of messages
/// to the JavaScript side after the command returned: download progress, log lines, streamed
/// responses and so on. Each message is a `TSend` value, which must implement [`IpcResponse`]
/// (automatically implemented for every [`serde::Serialize`] type) and is delivered to the
/// `onmessage` handler of the matching `Channel` on the JavaScript side.
///
/// The usual flow is to create the channel on the frontend and pass it as a command argument,
/// since [`Channel`] implements [`CommandArg`]:
///
/// ```javascript
/// import { Channel, invoke } from '@tauri-apps/api/core'
///
/// const onProgress = new Channel()
/// onProgress.onmessage = (message) => console.log(message)
/// await invoke('download', { url, onProgress })
/// ```
///
/// A channel can also be created on the Rust side with [`Channel::new`], or resolved from an id
/// the frontend sent inside a bigger payload with [`JavaScriptChannelId::channel_on`].
///
/// Channels are cheap to clone (every clone refers to the same JavaScript callback) and can be
/// stored in the app state or moved to another thread to send messages later. When the last clone
/// of a channel created from the frontend is dropped, the JavaScript side is notified that no more
/// messages will arrive and the callback is unregistered.
///
/// # Ordering
///
/// The channel automatically orders the messages: every message carries the index it was sent with,
/// and the JavaScript side buffers out-of-order messages until the missing ones arrive, so the
/// `onmessage` handler always observes messages in the same order [`Channel::send`] was called.
/// See [`Builder::channel_interceptor`](crate::Builder::channel_interceptor) if you need to
/// intercept or replace this delivery mechanism.
///
/// # Raw payloads
///
/// Any [`serde::Serialize`] value is sent as JSON. To stream binary data without the JSON overhead,
/// send an [`InvokeResponseBody::Raw`] through a `Channel<InvokeResponseBody>` (the default type
/// parameter) or a [`Response`]: the payload is then received in JavaScript as an `ArrayBuffer`.
/// Note that a `Channel<Vec<u8>>` does *not* do this - `Vec<u8>` is `Serialize`,
/// so it is sent as a JSON array of numbers.
///
/// # Examples
///
/// Streaming progress to the frontend from a command:
///
/// ```rust
/// use tauri::ipc::Channel;
///
/// #[derive(Clone, serde::Serialize)]
/// #[serde(rename_all = "camelCase")]
/// struct DownloadProgress {
/// downloaded: usize,
/// content_length: usize,
/// }
///
/// #[tauri::command]
/// fn download(url: String, on_progress: Channel<DownloadProgress>) -> tauri::Result<()> {
/// let content_length = 1000;
/// for downloaded in (0..=content_length).step_by(100) {
/// on_progress.send(DownloadProgress { downloaded, content_length })?;
/// }
/// Ok(())
/// }
/// ```
///
/// Streaming binary chunks, received as `ArrayBuffer`s in JavaScript:
///
/// ```rust
/// use tauri::ipc::{Channel, InvokeResponseBody};
///
/// #[tauri::command]
/// fn read_file(on_chunk: Channel<InvokeResponseBody>) -> tauri::Result<()> {
/// for chunk in [vec![0u8; 16], vec![1u8; 16]] {
/// on_chunk.send(InvokeResponseBody::Raw(chunk))?;
/// }
/// Ok(())
/// }
/// ```
pub struct Channel<TSend = InvokeResponseBody> {
inner: Arc<ChannelInner>,
phantom: std::marker::PhantomData<TSend>,
}
#[cfg(feature = "specta")]
const _: () = {
#[derive(specta::Type)]
#[specta(remote = super::Channel)]
#[allow(dead_code, non_camel_case_types)]
struct TAURI_CHANNEL<TSend>(std::marker::PhantomData<TSend>);
};
impl<TSend> Clone for Channel<TSend> {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
phantom: self.phantom,
}
}
}
type OnDropFn = Option<Box<dyn Fn() + Send + Sync + 'static>>;
type OnMessageFn = Box<dyn Fn(InvokeResponseBody) -> crate::Result<()> + Send + Sync>;
struct ChannelInner {
id: u32,
on_message: OnMessageFn,
on_drop: OnDropFn,
}
impl Drop for ChannelInner {
fn drop(&mut self) {
if let Some(on_drop) = &self.on_drop {
on_drop();
}
}
}
impl<TSend> Serialize for Channel<TSend> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&format!("{IPC_PAYLOAD_PREFIX}{}", self.inner.id))
}
}
/// The ID of a channel that was defined on the JavaScript layer.
///
/// Useful when expecting [`Channel`] as part of a JSON object instead of a top-level command argument.
///
/// # Examples
///
/// ```rust
/// use tauri::{ipc::JavaScriptChannelId, Runtime, Webview};
///
/// #[derive(serde::Deserialize)]
/// #[serde(rename_all = "camelCase")]
/// struct Button {
/// label: String,
/// on_click: JavaScriptChannelId,
/// }
///
/// #[tauri::command]
/// fn add_button<R: Runtime>(webview: Webview<R>, button: Button) {
/// let channel = button.on_click.channel_on(webview);
/// channel.send("clicked").unwrap();
/// }
/// ```
pub struct JavaScriptChannelId(CallbackFn);
impl FromStr for JavaScriptChannelId {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
s.strip_prefix(IPC_PAYLOAD_PREFIX)
.ok_or("invalid channel string")
.and_then(|id| id.parse().map_err(|_| "invalid channel ID"))
.map(|id| Self(CallbackFn(id)))
}
}
impl JavaScriptChannelId {
/// Gets a [`Channel`] for this channel ID on the given [`Webview`].
pub fn channel_on<R: Runtime, TSend>(&self, webview: Webview<R>) -> Channel<TSend> {
let callback_fn = self.0;
let callback_id = callback_fn.0;
let counter = Arc::new(AtomicUsize::new(0));
let counter_clone = counter.clone();
let webview_clone = webview.clone();
Channel::new_with_id(
callback_id,
Box::new(move |body| {
let current_index = counter.fetch_add(1, Ordering::Relaxed);
if let Some(interceptor) = &webview.manager.channel_interceptor {
if interceptor(&webview, callback_fn, current_index, &body) {
return Ok(());
}
}
match body {
// Don't go through the fetch process if the payload is small
InvokeResponseBody::Json(json_string)
if json_string.len() < MAX_JSON_DIRECT_EXECUTE_THRESHOLD =>
{
webview.eval(format_raw_js(
callback_id,
format!("{{ message: {json_string}, index: {current_index} }}"),
))?;
}
InvokeResponseBody::Raw(bytes) if bytes.len() < MAX_RAW_DIRECT_EXECUTE_THRESHOLD => {
let bytes_as_json_array = serde_json::to_string(&bytes)?;
webview.eval(format_raw_js(callback_id, format!("{{ message: new Uint8Array({bytes_as_json_array}).buffer, index: {current_index} }}")))?;
}
// use the fetch API to speed up larger response payloads
_ => {
let data_id = webview
.state::<ChannelDataIpcQueue>()
.insert(webview.label(), body);
webview.eval(format!(
"window.__TAURI_INTERNALS__.invoke('{FETCH_CHANNEL_DATA_COMMAND}', null, {{ headers: {{ '{CHANNEL_ID_HEADER_NAME}': '{data_id}' }} }}).then((response) => window.__TAURI_INTERNALS__.runCallback({callback_id}, {{ message: response, index: {current_index} }})).catch(console.error)",
))?;
}
}
Ok(())
}),
Some(Box::new(move || {
let current_index = counter_clone.load(Ordering::Relaxed);
let _ = webview_clone.eval(format_raw_js(
callback_id,
format!("{{ end: true, index: {current_index} }}"),
));
})),
)
}
}
impl<'de> Deserialize<'de> for JavaScriptChannelId {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value: String = Deserialize::deserialize(deserializer)?;
Self::from_str(&value).map_err(|_| {
serde::de::Error::custom(format!(
"invalid channel value `{value}`, expected a string in the `{IPC_PAYLOAD_PREFIX}ID` format"
))
})
}
}
impl<TSend> Channel<TSend> {
/// Creates a new channel with the given message handler.
///
/// This does not involve the frontend: the closure is called with the body of every message sent
/// through [`Channel::send`], and it is up to you to forward it. Use it to create a channel that
/// a plugin or a mobile command expects, or to consume channel messages in Rust.
///
/// To push messages to a channel that was created by the frontend, receive the [`Channel`] as a
/// command argument (see [`CommandArg`]) or deserialize a [`JavaScriptChannelId`] and call
/// [`JavaScriptChannelId::channel_on`] with the target [`Webview`], which wires the messages
/// through the IPC for you.
///
/// # Examples
///
/// ```rust
/// use tauri::ipc::{Channel, InvokeResponseBody};
///
/// let channel = Channel::new(|message: InvokeResponseBody| {
/// match message {
/// InvokeResponseBody::Json(json) => println!("channel message: {json}"),
/// InvokeResponseBody::Raw(bytes) => println!("channel message: {} bytes", bytes.len()),
/// }
/// Ok(())
/// });
///
/// channel.send("hello").unwrap();
/// ```
pub fn new<F: Fn(InvokeResponseBody) -> crate::Result<()> + Send + Sync + 'static>(
on_message: F,
) -> Self {
Self::new_with_id(
CHANNEL_COUNTER.fetch_add(1, Ordering::Relaxed),
Box::new(on_message),
None,
)
}
fn new_with_id(id: u32, on_message: OnMessageFn, on_drop: OnDropFn) -> Self {
#[allow(clippy::let_and_return)]
let channel = Self {
inner: Arc::new(ChannelInner {
id,
on_message,
on_drop,
}),
phantom: Default::default(),
};
#[cfg(mobile)]
crate::plugin::mobile::register_channel(Channel {
inner: channel.inner.clone(),
phantom: Default::default(),
});
channel
}
// This is used from the IPC handler
pub(crate) fn from_callback_fn<R: Runtime>(webview: Webview<R>, callback: CallbackFn) -> Self {
let callback_id = callback.0;
Channel::new_with_id(
callback_id,
Box::new(move |body| {
match body {
// Don't go through the fetch process if the payload is small
InvokeResponseBody::Json(json_string)
if json_string.len() < MAX_JSON_DIRECT_EXECUTE_THRESHOLD =>
{
webview.eval(format_raw_js(callback_id, json_string))?;
}
InvokeResponseBody::Raw(bytes) if bytes.len() < MAX_RAW_DIRECT_EXECUTE_THRESHOLD => {
let bytes_as_json_array = serde_json::to_string(&bytes)?;
webview.eval(format_raw_js(
callback_id,
format!("new Uint8Array({bytes_as_json_array}).buffer"),
))?;
}
// use the fetch API to speed up larger response payloads
_ => {
let data_id = webview
.state::<ChannelDataIpcQueue>()
.insert(webview.label(), body);
webview.eval(format!(
"window.__TAURI_INTERNALS__.invoke('{FETCH_CHANNEL_DATA_COMMAND}', null, {{ headers: {{ '{CHANNEL_ID_HEADER_NAME}': '{data_id}' }} }}).then((response) => window.__TAURI_INTERNALS__.runCallback({callback_id}, response)).catch(console.error)",
))?;
}
}
Ok(())
}),
None,
)
}
/// The channel identifier.
pub fn id(&self) -> u32 {
self.inner.id
}
/// Sends the given data through the channel.
pub fn send(&self, data: TSend) -> crate::Result<()>
where
TSend: IpcResponse,
{
(self.inner.on_message)(data.body()?)
}
}
impl<'de, R: Runtime, TSend> CommandArg<'de, R> for Channel<TSend> {
/// Grabs the [`Webview`] from the [`CommandItem`] and returns the associated [`Channel`].
fn from_command(command: CommandItem<'de, R>) -> Result<Self, InvokeError> {
let name = command.name;
let arg = command.key;
let webview = command.message.webview();
let value: String =
Deserialize::deserialize(command).map_err(|e| crate::Error::InvalidArgs(name, arg, e))?;
JavaScriptChannelId::from_str(&value)
.map(|id| id.channel_on(webview))
.map_err(|_| {
InvokeError::from(format!(
"invalid channel value `{value}`, expected a string in the `{IPC_PAYLOAD_PREFIX}ID` format"
))
})
}
}
#[command(root = "crate")]
fn fetch<R: Runtime>(
webview: Webview<R>,
request: Request<'_>,
cache: State<'_, ChannelDataIpcQueue>,
) -> Result<Response, &'static str> {
if let Some(id) = request
.headers()
.get(CHANNEL_ID_HEADER_NAME)
.and_then(|v| v.to_str().ok())
.and_then(|id| id.parse().ok())
{
if let Some(data) = cache.remove(webview.label(), id) {
Ok(Response::new(data))
} else {
Err("data not found")
}
} else {
Err("missing channel id header")
}
}
pub fn plugin<R: Runtime>() -> TauriPlugin<R> {
PluginBuilder::new(CHANNEL_PLUGIN_NAME)
.invoke_handler(crate::generate_handler![
#![plugin(__TAURI_CHANNEL__)]
fetch
])
.build()
}
#[cfg(test)]
mod tests {
use super::*;
fn json_body(s: &str) -> InvokeResponseBody {
InvokeResponseBody::Json(s.to_string())
}
#[test]
fn queue_entries_are_scoped_to_the_owning_webview() {
let queue = ChannelDataIpcQueue::default();
let id = queue.insert("main", json_body("{}"));
// foreign webviews cannot fetch the entry
assert!(queue.remove("settings", id).is_none());
// the owner can, once
assert!(queue.remove("main", id).is_some());
assert!(queue.remove("main", id).is_none());
}
#[test]
fn ids_are_scoped_per_webview() {
let queue = ChannelDataIpcQueue::default();
let a = queue.insert("a", json_body("1"));
let b = queue.insert("b", json_body("2"));
// each webview has its own id sequence, so the same id exists twice
// without the entries being visible across webviews
assert_eq!(a, b);
assert!(queue.remove("b", a).is_some());
assert!(queue.remove("a", a).is_some());
}
#[test]
fn purge_removes_only_the_closed_webview_entries() {
let queue = ChannelDataIpcQueue::default();
let a = queue.insert("a", json_body("1"));
let b = queue.insert("b", json_body("2"));
queue.remove_webview_entries("a");
assert!(queue.remove("a", a).is_none());
assert!(queue.remove("b", b).is_some());
}
}

View File

@@ -0,0 +1,332 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
//! The Tauri custom commands types and traits.
//!
//! You usually don't need to create these items yourself. These are created from [command](../attr.command.html)
//! attribute macro along the way and used by [`crate::generate_handler`] macro.
use crate::{
Runtime,
ipc::{InvokeBody, InvokeError, InvokeMessage},
};
use serde::{
Deserialize, Deserializer,
de::{Error, Visitor},
};
use tauri_utils::acl::resolved::ResolvedCommand;
/// Represents a custom command.
pub struct CommandItem<'a, R: Runtime> {
/// Name of the plugin if this command targets one.
pub plugin: Option<&'static str>,
/// The name of the command, e.g. `handler` on `#[command] fn handler(value: u64)`
pub name: &'static str,
/// The key of the command item, e.g. `value` on `#[command] fn handler(value: u64)`
pub key: &'static str,
/// The [`InvokeMessage`] that was passed to this command.
pub message: &'a InvokeMessage<R>,
/// The resolved ACL for this command.
pub acl: &'a Option<Vec<ResolvedCommand>>,
}
/// Trait implemented by command arguments to derive a value from a [`CommandItem`].
///
/// # Command Arguments
///
/// A command argument is any type that represents an item parsable from a [`CommandItem`]. Most
/// implementations will use the data stored in [`InvokeMessage`] since [`CommandItem`] is mostly a
/// wrapper around it.
///
/// # Provided Implementations
///
/// Tauri implements [`CommandArg`] automatically for a number of types.
/// * [`crate::AppHandle`]
/// * [`crate::Window`]
/// * [`crate::Webview`]
/// * [`crate::WebviewWindow`]
/// * [`crate::State`]
/// * [`crate::ipc::Channel`]
/// * [`crate::ipc::Request`]
/// * Gives access to the raw request body and headers instead of a deserialized argument.
/// * [`crate::ipc::CommandScope`] and [`crate::ipc::GlobalScope`]
/// * The scope defined by the capabilities that allowed the command, see the
/// [scope documentation](https://v2.tauri.app/security/scope/).
/// * `T where T: serde::Deserialize`
/// * Any type that implements `Deserialize` can automatically be used as a [`CommandArg`].
///
/// Note that the types above are resolved from the command message and not from the command
/// arguments sent by the frontend, so they do not need to be provided on the JavaScript side.
pub trait CommandArg<'de, R: Runtime>: Sized {
/// Derives an instance of `Self` from the [`CommandItem`].
///
/// If the derivation fails, the corresponding message will be rejected using [`InvokeMessage#reject`].
fn from_command(command: CommandItem<'de, R>) -> Result<Self, InvokeError>;
}
/// Automatically implement [`CommandArg`] for any type that can be deserialized.
impl<'de, D: Deserialize<'de>, R: Runtime> CommandArg<'de, R> for D {
fn from_command(command: CommandItem<'de, R>) -> Result<D, InvokeError> {
let name = command.name;
let arg = command.key;
#[cfg(feature = "tracing")]
let _span = tracing::trace_span!("ipc::request::deserialize_arg", arg = arg).entered();
Self::deserialize(command).map_err(|e| crate::Error::InvalidArgs(name, arg, e).into())
}
}
/// Pass the result of [`serde_json::Value::get`] into [`serde_json::Value`]'s deserializer.
///
/// Returns an error if the [`CommandItem`]'s key does not exist in the value.
macro_rules! pass {
($fn:ident, $($arg:ident: $argt:ty),+) => {
fn $fn<V: Visitor<'de>>(self, $($arg: $argt),*) -> Result<V::Value, Self::Error> {
self.deserialize_json()?.$fn($($arg),*)
}
}
}
impl<'a, R: Runtime> CommandItem<'a, R> {
fn deserialize_json(self) -> serde_json::Result<&'a serde_json::Value> {
if self.key.is_empty() {
return Err(serde_json::Error::custom(format!(
"command {} has an argument with no name with a non-optional value",
self.name
)));
}
match &self.message.payload {
InvokeBody::Raw(_body) => Err(serde_json::Error::custom(format!(
"command {} expected a value for key {} but the IPC call used a bytes payload",
self.name, self.key
))),
InvokeBody::Json(v) => match v.get(self.key) {
Some(value) => Ok(value),
None => Err(serde_json::Error::custom(format!(
"command {} missing required key {}",
self.name, self.key
))),
},
}
}
}
/// A [`Deserializer`] wrapper around [`CommandItem`].
///
/// If the key doesn't exist, an error will be returned if the deserialized type is not expecting
/// an optional item. If the key does exist, the value will be called with
/// [`Value`](serde_json::Value)'s [`Deserializer`] implementation.
impl<'de, R: Runtime> Deserializer<'de> for CommandItem<'de, R> {
type Error = serde_json::Error;
pass!(deserialize_any, visitor: V);
pass!(deserialize_bool, visitor: V);
pass!(deserialize_i8, visitor: V);
pass!(deserialize_i16, visitor: V);
pass!(deserialize_i32, visitor: V);
pass!(deserialize_i64, visitor: V);
pass!(deserialize_u8, visitor: V);
pass!(deserialize_u16, visitor: V);
pass!(deserialize_u32, visitor: V);
pass!(deserialize_u64, visitor: V);
pass!(deserialize_f32, visitor: V);
pass!(deserialize_f64, visitor: V);
pass!(deserialize_char, visitor: V);
pass!(deserialize_str, visitor: V);
pass!(deserialize_string, visitor: V);
pass!(deserialize_bytes, visitor: V);
pass!(deserialize_byte_buf, visitor: V);
fn deserialize_option<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> {
match &self.message.payload {
InvokeBody::Raw(_body) => Err(serde_json::Error::custom(format!(
"command {} expected a value for key {} but the IPC call used a bytes payload",
self.name, self.key
))),
InvokeBody::Json(v) => match v.get(self.key) {
Some(value) => value.deserialize_option(visitor),
None => visitor.visit_none(),
},
}
}
pass!(deserialize_unit, visitor: V);
pass!(deserialize_unit_struct, name: &'static str, visitor: V);
pass!(deserialize_newtype_struct, name: &'static str, visitor: V);
pass!(deserialize_seq, visitor: V);
pass!(deserialize_tuple, len: usize, visitor: V);
pass!(
deserialize_tuple_struct,
name: &'static str,
len: usize,
visitor: V
);
pass!(deserialize_map, visitor: V);
pass!(
deserialize_struct,
name: &'static str,
fields: &'static [&'static str],
visitor: V
);
pass!(
deserialize_enum,
name: &'static str,
fields: &'static [&'static str],
visitor: V
);
pass!(deserialize_identifier, visitor: V);
pass!(deserialize_ignored_any, visitor: V);
}
/// [Autoref-based stable specialization](https://github.com/dtolnay/case-studies/blob/master/autoref-specialization/README.md)
///
/// Nothing in this module is considered stable.
#[doc(hidden)]
pub mod private {
use crate::{
Runtime,
ipc::{InvokeError, InvokeResolver, InvokeResponseBody, IpcResponse},
};
use std::future::Future;
#[cfg(feature = "tracing")]
pub use tracing;
// ===== impl IpcResponse =====
pub struct ResponseTag;
pub trait ResponseKind {
#[inline(always)]
fn blocking_kind(&self) -> ResponseTag {
ResponseTag
}
#[inline(always)]
fn async_kind(&self) -> ResponseTag {
ResponseTag
}
}
impl<T: IpcResponse> ResponseKind for &T {}
impl ResponseTag {
#[inline(always)]
pub fn block<R, T>(self, value: T, resolver: InvokeResolver<R>)
where
R: Runtime,
T: IpcResponse,
{
resolver.respond(Ok(value))
}
#[inline(always)]
pub async fn future<T>(self, value: T) -> Result<InvokeResponseBody, InvokeError>
where
T: IpcResponse,
{
Ok(value.body()?)
}
}
// ===== Result<impl Serialize, impl Into<InvokeError>> =====
pub struct ResultTag;
pub trait ResultKind {
#[inline(always)]
fn blocking_kind(&self) -> ResultTag {
ResultTag
}
#[inline(always)]
fn async_kind(&self) -> ResultTag {
ResultTag
}
}
impl<T: IpcResponse, E: Into<InvokeError>> ResultKind for Result<T, E> {}
impl ResultTag {
#[inline(always)]
pub fn block<R, T, E>(self, value: Result<T, E>, resolver: InvokeResolver<R>)
where
R: Runtime,
T: IpcResponse,
E: Into<InvokeError>,
{
resolver.respond(value.map_err(Into::into))
}
#[inline(always)]
pub async fn future<T, E>(self, value: Result<T, E>) -> Result<InvokeResponseBody, InvokeError>
where
T: IpcResponse,
E: Into<InvokeError>,
{
Ok(value.map_err(Into::into)?.body()?)
}
}
// ===== Future<Output = impl IpcResponse> =====
pub struct FutureTag;
pub trait FutureKind {
#[inline(always)]
fn async_kind(&self) -> FutureTag {
FutureTag
}
}
impl<T: IpcResponse, F: Future<Output = T>> FutureKind for &F {}
impl FutureTag {
#[inline(always)]
pub async fn future<T, F>(self, value: F) -> Result<InvokeResponseBody, InvokeError>
where
T: IpcResponse,
F: Future<Output = T> + Send + 'static,
{
Ok(value.await.body()?)
}
}
// ===== Future<Output = Result<impl Serialize, impl Into<InvokeError>>> =====
pub struct ResultFutureTag;
pub trait ResultFutureKind {
#[inline(always)]
fn async_kind(&self) -> ResultFutureTag {
ResultFutureTag
}
}
impl<T: IpcResponse, E: Into<InvokeError>, F: Future<Output = Result<T, E>>> ResultFutureKind
for F
{
}
impl ResultFutureTag {
#[inline(always)]
pub async fn future<T, E, F>(self, value: F) -> Result<InvokeResponseBody, InvokeError>
where
T: IpcResponse,
E: Into<InvokeError>,
F: Future<Output = Result<T, E>> + Send,
{
let response = value.await.map_err(Into::into)?;
Ok(response.body()?)
}
}
}

View File

@@ -0,0 +1,344 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use serde::Serialize;
use serde_json::value::RawValue;
use serialize_to_javascript::Serialized;
use super::CallbackFn;
/// The information about this is quite limited. On Chrome/Edge and Firefox, [the maximum string size is approximately 1 GB](https://stackoverflow.com/a/34958490).
///
/// [From MDN:](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/length#description)
///
/// ECMAScript 2016 (ed. 7) established a maximum length of 2^53 - 1 elements. Previously, no maximum length was specified.
///
/// In Firefox, strings have a maximum length of 2\*\*30 - 2 (~1GB). In versions prior to Firefox 65, the maximum length was 2\*\*28 - 1 (~256MB).
const MAX_JSON_STR_LEN: usize = usize::pow(2, 30) - 2;
/// Minimum size JSON needs to be in order to convert it to JSON.parse with [`format_json`].
// TODO: this number should be benchmarked and checked for optimal range, I set 10 KiB arbitrarily
// we don't want to lose the gained object parsing time to extra allocations preparing it
const MIN_JSON_PARSE_LEN: usize = 10_240;
/// Transforms & escapes a JSON value.
///
/// If it's an object or array, JSON.parse('{json}') is used, with the '{json}' string properly escaped.
/// The return value of this function can be safely used on [`eval`](crate::Window#method.eval) calls.
///
/// Single quotes chosen because double quotes are already used in JSON. With single quotes, we only
/// need to escape strings that include backslashes or single quotes. If we used double quotes, then
/// there would be no cases that a string doesn't need escaping.
///
/// The function takes a closure to handle the escaped string in order to avoid unnecessary allocations.
///
/// # Safety
///
/// The ability to safely escape JSON into a JSON.parse('{json}') relies entirely on 2 things.
///
/// 1. `serde_json`'s ability to correctly escape and format json into a string.
/// 2. JavaScript engines not accepting anything except another unescaped, literal single quote
/// character to end a string that was opened with it.
fn serialize_js_with<F: FnOnce(&str) -> String>(
json_string: String,
options: serialize_to_javascript::Options,
cb: F,
) -> crate::Result<String> {
// get a raw &str representation of a serialized json value.
let raw = RawValue::from_string(json_string)?;
// from here we know json.len() > 1 because an empty string is not a valid json value.
let json = raw.get();
let first = json.as_bytes()[0];
#[cfg(debug_assertions)]
if first == b'"' {
assert!(
json.len() < MAX_JSON_STR_LEN,
"passing a string larger than the max JavaScript literal string size"
)
}
let return_val = if json.len() > MIN_JSON_PARSE_LEN && (first == b'{' || first == b'[') {
let serialized = Serialized::new(&raw, &options).into_string();
// only use JSON.parse('{arg}') for arrays and objects less than the limit
// smaller literals do not benefit from being parsed from json
if serialized.len() < MAX_JSON_STR_LEN {
cb(&serialized)
} else {
cb(json)
}
} else {
cb(json)
};
Ok(return_val)
}
/// Formats a function name and a serializable argument to be evaluated as callback.
///
/// See [`format_raw`] for more information.
pub fn format<T: Serialize>(function_name: CallbackFn, arg: &T) -> crate::Result<String> {
format_raw(function_name, serde_json::to_string(arg)?)
}
/// Formats a function name and a raw JSON string argument to be evaluated as callback.
///
/// This will serialize primitive JSON types (e.g. booleans, strings, numbers, etc.) as JavaScript literals,
/// but will serialize arrays and objects whose serialized JSON string is smaller than 1 GB and larger
/// than 10 KiB with `JSON.parse('...')`.
/// See [json-parse-benchmark](https://github.com/GoogleChromeLabs/json-parse-benchmark).
pub fn format_raw(function_name: CallbackFn, json_string: String) -> crate::Result<String> {
let callback_id = function_name.0;
serialize_js_with(json_string, Default::default(), |arg| {
format_raw_js(callback_id, arg)
})
}
/// Formats a function name and a JavaScript string argument to be evaluated as callback.
pub fn format_raw_js(callback_id: u32, js: impl AsRef<str>) -> String {
fn format_inner(callback_id: u32, js: &str) -> String {
format!("window.__TAURI_INTERNALS__.runCallback({callback_id}, {js})")
}
format_inner(callback_id, js.as_ref())
}
/// Formats a serializable Result type to its Promise response.
///
/// See [`format_result_raw`] for more information.
pub fn format_result<T: Serialize, E: Serialize>(
result: Result<T, E>,
success_callback: CallbackFn,
error_callback: CallbackFn,
) -> crate::Result<String> {
match result {
Ok(res) => format(success_callback, &res),
Err(err) => format(error_callback, &err),
}
}
/// Formats a Result type of raw JSON strings to its Promise response.
/// Useful for Promises handling.
/// 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.
///
/// * `result` the Result to check
/// * `success_callback` the function name of the Ok callback. Usually the `resolve` of the JS Promise.
/// * `error_callback` the function name of the Err callback. Usually the `reject` of the JS Promise.
///
/// Note that the callback strings are automatically generated by the `invoke` helper.
pub fn format_result_raw(
raw_result: Result<String, String>,
success_callback: CallbackFn,
error_callback: CallbackFn,
) -> crate::Result<String> {
match raw_result {
Ok(res) => format_raw(success_callback, res),
Err(err) => format_raw(error_callback, err),
}
}
#[cfg(test)]
mod test {
use super::*;
use quickcheck::{Arbitrary, Gen};
use quickcheck_macros::quickcheck;
impl Arbitrary for CallbackFn {
fn arbitrary(g: &mut Gen) -> CallbackFn {
CallbackFn(u32::arbitrary(g))
}
}
#[derive(Debug, Clone)]
struct JsonStr(String);
impl Arbitrary for JsonStr {
fn arbitrary(g: &mut Gen) -> Self {
if bool::arbitrary(g) {
Self(format!(
"{{ {}: {} }}",
serde_json::to_string(&String::arbitrary(g)).unwrap(),
serde_json::to_string(&String::arbitrary(g)).unwrap()
))
} else {
Self(serde_json::to_string(&String::arbitrary(g)).unwrap())
}
}
}
fn serialize_js<T: Serialize>(value: &T) -> crate::Result<String> {
serialize_js_with(serde_json::to_string(value)?, Default::default(), |v| {
v.into()
})
}
fn serialize_js_raw(value: impl Into<String>) -> crate::Result<String> {
serialize_js_with(value.into(), Default::default(), |v| v.into())
}
#[test]
fn test_serialize_js() {
assert_eq!(serialize_js(&()).unwrap(), "null");
assert_eq!(serialize_js(&5i32).unwrap(), "5");
#[derive(serde::Serialize)]
struct JsonObj {
value: String,
}
let raw_str = "T".repeat(MIN_JSON_PARSE_LEN);
assert_eq!(serialize_js(&raw_str).unwrap(), format!("\"{raw_str}\""));
assert_eq!(
serialize_js(&JsonObj {
value: raw_str.clone()
})
.unwrap(),
format!("JSON.parse('{{\"value\":\"{raw_str}\"}}')")
);
assert_eq!(
serialize_js(&JsonObj {
value: format!("\"{raw_str}\"")
})
.unwrap(),
format!("JSON.parse('{{\"value\":\"\\\\\"{raw_str}\\\\\"\"}}')")
);
let dangerous_json = RawValue::from_string(
r#"{"test":"don\\🚀🐱‍👤\\'t forget to escape me!🚀🐱‍👤","te🚀🐱‍👤st2":"don't forget to escape me!","test3":"\\🚀🐱‍👤\\\\'''\\\\🚀🐱‍👤\\\\🚀🐱‍👤\\'''''"}"#.into()
).unwrap();
let definitely_escaped_dangerous_json = format!(
"JSON.parse('{}')",
dangerous_json
.get()
.replace('\\', "\\\\")
.replace('\'', "\\'")
);
let escape_single_quoted_json_test =
serialize_to_javascript::Serialized::new(&dangerous_json, &Default::default()).into_string();
let result = r#"JSON.parse('{"test":"don\\\\🚀🐱‍👤\\\\\'t forget to escape me!🚀🐱‍👤","te🚀🐱‍👤st2":"don\'t forget to escape me!","test3":"\\\\🚀🐱‍👤\\\\\\\\\'\'\'\\\\\\\\🚀🐱‍👤\\\\\\\\🚀🐱‍👤\\\\\'\'\'\'\'"}')"#;
assert_eq!(definitely_escaped_dangerous_json, result);
assert_eq!(escape_single_quoted_json_test, result);
}
// check arbitrary strings in the format callback function
#[quickcheck]
fn qc_formatting(f: CallbackFn, a: String) -> bool {
// call format callback
let fc = format(f, &a).unwrap();
fc.contains(&format!(
"window.__TAURI_INTERNALS__.runCallback({}, JSON.parse('{}'))",
f.0,
serde_json::Value::String(a.clone()),
)) || fc.contains(&format!(
r#"window.__TAURI_INTERNALS__.runCallback({}, {})"#,
f.0,
serde_json::Value::String(a),
))
}
// check arbitrary strings in format_result
#[quickcheck]
fn qc_format_res(result: Result<String, String>, c: CallbackFn, ec: CallbackFn) -> bool {
let resp = format_result(result.clone(), c, ec).expect("failed to format callback result");
let (function, value) = match result {
Ok(v) => (c, v),
Err(e) => (ec, e),
};
resp.contains(&format!(
r#"window.__TAURI_INTERNALS__.runCallback({}, {})"#,
function.0,
serde_json::Value::String(value),
))
}
#[test]
fn test_serialize_js_raw() {
assert_eq!(serialize_js_raw("null").unwrap(), "null");
assert_eq!(serialize_js_raw("5").unwrap(), "5");
assert_eq!(
serialize_js_raw("{ \"x\": [1, 2, 3] }").unwrap(),
"{ \"x\": [1, 2, 3] }"
);
#[derive(serde::Serialize)]
struct JsonObj {
value: String,
}
let raw_str = "T".repeat(MIN_JSON_PARSE_LEN);
assert_eq!(
serialize_js_raw(format!("\"{raw_str}\"")).unwrap(),
format!("\"{raw_str}\"")
);
assert_eq!(
serialize_js_raw(format!("{{\"value\":\"{raw_str}\"}}")).unwrap(),
format!("JSON.parse('{{\"value\":\"{raw_str}\"}}')")
);
assert_eq!(
serialize_js(&JsonObj {
value: format!("\"{raw_str}\"")
})
.unwrap(),
format!("JSON.parse('{{\"value\":\"\\\\\"{raw_str}\\\\\"\"}}')")
);
let dangerous_json = RawValue::from_string(
r#"{"test":"don\\🚀🐱‍👤\\'t forget to escape me!🚀🐱‍👤","te🚀🐱‍👤st2":"don't forget to escape me!","test3":"\\🚀🐱‍👤\\\\'''\\\\🚀🐱‍👤\\\\🚀🐱‍👤\\'''''"}"#.into()
).unwrap();
let definitely_escaped_dangerous_json = format!(
"JSON.parse('{}')",
dangerous_json
.get()
.replace('\\', "\\\\")
.replace('\'', "\\'")
);
let escape_single_quoted_json_test =
serialize_to_javascript::Serialized::new(&dangerous_json, &Default::default()).into_string();
let result = r#"JSON.parse('{"test":"don\\\\🚀🐱‍👤\\\\\'t forget to escape me!🚀🐱‍👤","te🚀🐱‍👤st2":"don\'t forget to escape me!","test3":"\\\\🚀🐱‍👤\\\\\\\\\'\'\'\\\\\\\\🚀🐱‍👤\\\\\\\\🚀🐱‍👤\\\\\'\'\'\'\'"}')"#;
assert_eq!(definitely_escaped_dangerous_json, result);
assert_eq!(escape_single_quoted_json_test, result);
}
// check arbitrary strings in the format callback function
#[quickcheck]
fn qc_formatting_raw(f: CallbackFn, a: JsonStr) -> bool {
let a = a.0;
// call format callback
let fc = format_raw(f, a.clone()).unwrap();
fc.contains(&format!(
r#"window.__TAURI_INTERNALS__.runCallback({}, JSON.parse('{}'))"#,
f.0, a
)) || fc.contains(&format!(
r#"window.__TAURI_INTERNALS__.runCallback({}, {})"#,
f.0, a
))
}
// check arbitrary strings in format_result
#[quickcheck]
fn qc_format_raw_res(result: Result<JsonStr, JsonStr>, c: CallbackFn, ec: CallbackFn) -> bool {
let result = result.map(|v| v.0).map_err(|e| e.0);
let resp = format_result_raw(result.clone(), c, ec).expect("failed to format callback result");
let (function, value) = match result {
Ok(v) => (c, v),
Err(e) => (ec, e),
};
resp.contains(&format!(
r#"window.__TAURI_INTERNALS__.runCallback({}, {})"#,
function.0, value
))
}
}

606
crates/tauri/src/ipc/mod.rs Normal file
View File

@@ -0,0 +1,606 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
//! Types and functions related to Inter Procedure Call(IPC).
//!
//! This module includes utilities to send messages to the JS layer of the webview.
use std::{
future::Future,
sync::{Arc, Mutex},
};
use http::HeaderMap;
use serde::{
Deserialize, Serialize,
de::{DeserializeOwned, IntoDeserializer},
};
use serde_json::Value as JsonValue;
pub use serialize_to_javascript::Options as SerializeOptions;
use tauri_macros::default_runtime;
use tauri_utils::acl::resolved::ResolvedCommand;
use crate::{Runtime, StateManager, webview::Webview};
mod authority;
#[cfg(feature = "dynamic-acl")]
mod capability_builder;
pub(crate) mod channel;
mod command;
pub(crate) mod format_callback;
pub(crate) mod protocol;
pub use authority::{
CommandScope, GlobalScope, Origin, RuntimeAuthority, ScopeObject, ScopeObjectMatch, ScopeValue,
};
#[cfg(feature = "dynamic-acl")]
pub use capability_builder::{CapabilityBuilder, RuntimeCapability};
pub use channel::{Channel, JavaScriptChannelId};
pub use command::{CommandArg, CommandItem, private};
/// A closure that is run every time Tauri receives a message it doesn't explicitly handle.
pub type InvokeHandler<R> = dyn Fn(Invoke<R>) -> bool + Send + Sync + 'static;
/// A closure that is responsible for respond a JS message.
pub type InvokeResponder<R> =
dyn Fn(&Webview<R>, &str, &InvokeResponse, CallbackFn, CallbackFn) + Send + Sync + 'static;
/// Similar to [`InvokeResponder`] but taking owned arguments.
pub type OwnedInvokeResponder<R> =
dyn FnOnce(Webview<R>, String, InvokeResponse, CallbackFn, CallbackFn) + Send + 'static;
/// Possible values of an IPC payload.
///
/// ### Android
/// On Android, [InvokeBody::Raw] is not supported. The enum will always contain [InvokeBody::Json].
/// When targeting Android Devices, consider passing raw bytes as a base64 [[std::string::String]], which is still more efficient than passing them as a number array in [InvokeBody::Json]
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(PartialEq))]
pub enum InvokeBody {
/// Json payload.
Json(JsonValue),
/// Bytes payload.
Raw(Vec<u8>),
}
impl Default for InvokeBody {
fn default() -> Self {
Self::Json(Default::default())
}
}
impl From<JsonValue> for InvokeBody {
fn from(value: JsonValue) -> Self {
Self::Json(value)
}
}
impl From<Vec<u8>> for InvokeBody {
fn from(value: Vec<u8>) -> Self {
Self::Raw(value)
}
}
impl InvokeBody {
#[cfg(mobile)]
pub(crate) fn into_json(self) -> JsonValue {
match self {
Self::Json(v) => v,
Self::Raw(v) => {
JsonValue::Array(v.into_iter().map(|n| JsonValue::Number(n.into())).collect())
}
}
}
}
/// Possible values of an IPC response.
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(PartialEq))]
pub enum InvokeResponseBody {
/// Json payload.
Json(String),
/// Bytes payload.
Raw(Vec<u8>),
}
impl From<String> for InvokeResponseBody {
fn from(value: String) -> Self {
Self::Json(value)
}
}
impl From<Vec<u8>> for InvokeResponseBody {
fn from(value: Vec<u8>) -> Self {
Self::Raw(value)
}
}
impl From<InvokeBody> for InvokeResponseBody {
fn from(value: InvokeBody) -> Self {
match value {
InvokeBody::Json(v) => Self::Json(serde_json::to_string(&v).unwrap()),
InvokeBody::Raw(v) => Self::Raw(v),
}
}
}
impl IpcResponse for InvokeResponseBody {
fn body(self) -> crate::Result<InvokeResponseBody> {
Ok(self)
}
}
impl InvokeResponseBody {
/// Attempts to deserialize the response.
pub fn deserialize<T: DeserializeOwned>(self) -> serde_json::Result<T> {
match self {
Self::Json(v) => serde_json::from_str(&v),
Self::Raw(v) => T::deserialize(v.into_deserializer()),
}
}
}
/// The IPC request.
///
/// Includes the `body` and `headers` parameters of a Tauri command invocation.
/// This allows commands to accept raw bytes - on all platforms except Android.
#[derive(Debug)]
pub struct Request<'a> {
body: &'a InvokeBody,
headers: &'a HeaderMap,
}
impl Request<'_> {
/// The request body.
pub fn body(&self) -> &InvokeBody {
self.body
}
/// The request headers.
pub fn headers(&self) -> &HeaderMap {
self.headers
}
}
impl<'a, R: Runtime> CommandArg<'a, R> for Request<'a> {
/// Returns the invoke [`Request`].
fn from_command(command: CommandItem<'a, R>) -> Result<Self, InvokeError> {
Ok(Self {
body: command.message.payload(),
headers: command.message.headers(),
})
}
}
/// Marks a type as a response to an IPC call.
pub trait IpcResponse {
/// Resolve the IPC response body.
fn body(self) -> crate::Result<InvokeResponseBody>;
}
impl<T: Serialize> IpcResponse for T {
fn body(self) -> crate::Result<InvokeResponseBody> {
serde_json::to_string(&self)
.map(Into::into)
.map_err(Into::into)
}
}
/// The IPC response.
pub struct Response {
body: InvokeResponseBody,
}
impl IpcResponse for Response {
fn body(self) -> crate::Result<InvokeResponseBody> {
Ok(self.body)
}
}
impl Response {
/// Defines a response with the given body.
pub fn new(body: impl Into<InvokeResponseBody>) -> Self {
Self { body: body.into() }
}
}
/// The message and resolver given to a custom command.
///
/// This struct is used internally by macros and is explicitly **NOT** stable.
#[default_runtime(crate::Wry, wry)]
pub struct Invoke<R: Runtime> {
/// The message passed.
pub message: InvokeMessage<R>,
/// The resolver of the message.
pub resolver: InvokeResolver<R>,
/// Resolved ACL for this IPC invoke.
pub acl: Option<Vec<ResolvedCommand>>,
}
/// Error response from an [`InvokeMessage`].
#[derive(Debug)]
pub struct InvokeError(pub serde_json::Value);
impl InvokeError {
/// Create an [`InvokeError`] as a string of the [`std::error::Error`] message.
#[inline(always)]
pub fn from_error<E: std::error::Error>(error: E) -> Self {
Self(serde_json::Value::String(error.to_string()))
}
/// Create an [`InvokeError`] as a string of the [`anyhow::Error`] message.
#[inline(always)]
pub fn from_anyhow(error: anyhow::Error) -> Self {
Self(serde_json::Value::String(format!("{error:#}")))
}
}
impl<T: Serialize> From<T> for InvokeError {
#[inline]
fn from(value: T) -> Self {
serde_json::to_value(value)
.map(Self)
.unwrap_or_else(Self::from_error)
}
}
impl From<crate::Error> for InvokeError {
#[inline(always)]
fn from(error: crate::Error) -> Self {
Self(serde_json::Value::String(error.to_string()))
}
}
/// Response from a [`InvokeMessage`] passed to the [`InvokeResolver`].
#[derive(Debug)]
pub enum InvokeResponse {
/// Resolve the promise.
Ok(InvokeResponseBody),
/// Reject the promise.
Err(InvokeError),
}
impl<T: IpcResponse, E: Into<InvokeError>> From<Result<T, E>> for InvokeResponse {
#[inline]
fn from(result: Result<T, E>) -> Self {
match result {
Ok(ok) => match ok.body() {
Ok(value) => Self::Ok(value),
Err(err) => Self::Err(InvokeError::from_error(err)),
},
Err(err) => Self::Err(err.into()),
}
}
}
impl From<InvokeError> for InvokeResponse {
fn from(error: InvokeError) -> Self {
Self::Err(error)
}
}
/// Resolver of a invoke message.
#[default_runtime(crate::Wry, wry)]
pub struct InvokeResolver<R: Runtime> {
webview: Webview<R>,
responder: Arc<Mutex<Option<Box<OwnedInvokeResponder<R>>>>>,
cmd: String,
pub(crate) callback: CallbackFn,
pub(crate) error: CallbackFn,
}
impl<R: Runtime> Clone for InvokeResolver<R> {
fn clone(&self) -> Self {
Self {
webview: self.webview.clone(),
responder: self.responder.clone(),
cmd: self.cmd.clone(),
callback: self.callback,
error: self.error,
}
}
}
impl<R: Runtime> InvokeResolver<R> {
pub(crate) fn new(
webview: Webview<R>,
responder: Arc<Mutex<Option<Box<OwnedInvokeResponder<R>>>>>,
cmd: String,
callback: CallbackFn,
error: CallbackFn,
) -> Self {
Self {
webview,
responder,
cmd,
callback,
error,
}
}
/// Reply to the invoke promise with an async task.
pub fn respond_async<T, F>(self, task: F)
where
T: IpcResponse,
F: Future<Output = Result<T, InvokeError>> + Send + 'static,
{
crate::async_runtime::spawn(async move {
Self::return_task(
self.webview,
self.responder,
task,
self.cmd,
self.callback,
self.error,
)
.await;
});
}
/// Reply to the invoke promise with an async task which is already serialized.
pub fn respond_async_serialized<F>(self, task: F)
where
F: Future<Output = Result<InvokeResponseBody, InvokeError>> + Send + 'static,
{
// Dynamic dispatch the call in dev for a faster compile time
// TODO: Revisit this and see if we can do this for the release build as well if the performance hit is not a problem
#[cfg(debug_assertions)]
{
self.respond_async_serialized_dyn(Box::pin(task))
}
#[cfg(not(debug_assertions))]
{
self.respond_async_serialized_inner(task)
}
}
/// Dynamic dispatch the [`Self::respond_async_serialized`] call
#[cfg(debug_assertions)]
fn respond_async_serialized_dyn(
self,
task: std::pin::Pin<
Box<dyn Future<Output = Result<InvokeResponseBody, InvokeError>> + Send + 'static>,
>,
) {
self.respond_async_serialized_inner(task)
}
/// Reply to the invoke promise with an async task which is already serialized.
fn respond_async_serialized_inner<F>(self, task: F)
where
F: Future<Output = Result<InvokeResponseBody, InvokeError>> + Send + 'static,
{
crate::async_runtime::spawn(async move {
let response = match task.await {
Ok(ok) => InvokeResponse::Ok(ok),
Err(err) => InvokeResponse::Err(err),
};
Self::return_result(
self.webview,
self.responder,
response,
self.cmd,
self.callback,
self.error,
)
});
}
/// Reply to the invoke promise with a serializable value.
pub fn respond<T: IpcResponse>(self, value: Result<T, InvokeError>) {
Self::return_result(
self.webview,
self.responder,
value.into(),
self.cmd,
self.callback,
self.error,
)
}
/// Resolve the invoke promise with a value.
pub fn resolve<T: IpcResponse>(self, value: T) {
self.respond(Ok(value))
}
/// Reject the invoke promise with a value.
pub fn reject<T: Serialize>(self, value: T) {
Self::return_result(
self.webview,
self.responder,
Result::<(), _>::Err(value).into(),
self.cmd,
self.callback,
self.error,
)
}
/// Reject the invoke promise with an [`InvokeError`].
pub fn invoke_error(self, error: InvokeError) {
Self::return_result(
self.webview,
self.responder,
error.into(),
self.cmd,
self.callback,
self.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, F>(
webview: Webview<R>,
responder: Arc<Mutex<Option<Box<OwnedInvokeResponder<R>>>>>,
task: F,
cmd: String,
success_callback: CallbackFn,
error_callback: CallbackFn,
) where
T: IpcResponse,
F: Future<Output = Result<T, InvokeError>> + Send + 'static,
{
let result = task.await;
Self::return_closure(
webview,
responder,
|| result,
cmd,
success_callback,
error_callback,
)
}
pub(crate) fn return_closure<T: IpcResponse, F: FnOnce() -> Result<T, InvokeError>>(
webview: Webview<R>,
responder: Arc<Mutex<Option<Box<OwnedInvokeResponder<R>>>>>,
f: F,
cmd: String,
success_callback: CallbackFn,
error_callback: CallbackFn,
) {
Self::return_result(
webview,
responder,
f().into(),
cmd,
success_callback,
error_callback,
)
}
pub(crate) fn return_result(
webview: Webview<R>,
responder: Arc<Mutex<Option<Box<OwnedInvokeResponder<R>>>>>,
response: InvokeResponse,
cmd: String,
success_callback: CallbackFn,
error_callback: CallbackFn,
) {
(responder.lock().unwrap().take().expect("resolver consumed"))(
webview,
cmd,
response,
success_callback,
error_callback,
);
}
}
/// An invoke message.
#[default_runtime(crate::Wry, wry)]
#[derive(Debug)]
pub struct InvokeMessage<R: Runtime> {
/// The webview that received the invoke message.
pub(crate) webview: Webview<R>,
/// The IPC command.
pub(crate) command: String,
/// The JSON argument passed on the invoke message.
pub(crate) payload: InvokeBody,
/// The request headers.
pub(crate) headers: HeaderMap,
}
impl<R: Runtime> Clone for InvokeMessage<R> {
fn clone(&self) -> Self {
Self {
webview: self.webview.clone(),
command: self.command.clone(),
payload: self.payload.clone(),
headers: self.headers.clone(),
}
}
}
impl<R: Runtime> InvokeMessage<R> {
/// Create an new [`InvokeMessage`] from a payload send by a webview.
pub(crate) fn new(
webview: Webview<R>,
command: String,
payload: InvokeBody,
headers: HeaderMap,
) -> Self {
Self {
webview,
command,
payload,
headers,
}
}
/// The invoke command.
#[inline(always)]
pub fn command(&self) -> &str {
&self.command
}
/// The webview that received the invoke.
#[inline(always)]
pub fn webview(&self) -> Webview<R> {
self.webview.clone()
}
/// A reference to webview that received the invoke.
#[inline(always)]
pub fn webview_ref(&self) -> &Webview<R> {
&self.webview
}
/// A reference to the payload the invoke received.
#[inline(always)]
pub fn payload(&self) -> &InvokeBody {
&self.payload
}
// TODO: make private or remove in v3
/// The state manager associated with the application
#[deprecated(note = "Use `Manager::state` to access the state: `self.webview_ref().state()`")]
#[inline(always)]
pub fn state(&self) -> Arc<StateManager> {
self.webview.manager.state.clone()
}
// TODO: make private or remove in v3
/// A reference to the state manager associated with application.
#[deprecated(note = "Use `Manager::state` to access the state: `self.webview_ref().state()`")]
#[inline(always)]
pub fn state_ref(&self) -> &StateManager {
&self.webview.manager.state
}
/// The request headers.
#[inline(always)]
pub fn headers(&self) -> &HeaderMap {
&self.headers
}
}
/// The `Callback` type is the return value of the `transformCallback` JavaScript function.
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub struct CallbackFn(pub u32);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deserialize_invoke_response_body() {
let json = InvokeResponseBody::Json("[1, 123, 1231]".to_string());
assert_eq!(json.deserialize::<Vec<u16>>().unwrap(), vec![1, 123, 1231]);
let json = InvokeResponseBody::Json("\"string value\"".to_string());
assert_eq!(json.deserialize::<String>().unwrap(), "string value");
let json = InvokeResponseBody::Json("\"string value\"".to_string());
assert!(json.deserialize::<Vec<u16>>().is_err());
let values = vec![1, 2, 3, 4, 5, 6, 1];
let raw = InvokeResponseBody::Raw(values.clone());
assert_eq!(raw.deserialize::<Vec<u8>>().unwrap(), values);
}
}

View File

@@ -0,0 +1,753 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::{borrow::Cow, sync::Arc};
use crate::{
Runtime,
ipc::InvokeResponseBody,
manager::AppManager,
webview::{InvokeRequest, UriSchemeProtocolHandler},
};
use http::{
HeaderValue, Method, Request, StatusCode,
header::{
ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_EXPOSE_HEADERS,
CONTENT_TYPE,
},
};
use url::Url;
use super::{CallbackFn, InvokeResponse};
const TAURI_CALLBACK_HEADER_NAME: &str = "Tauri-Callback";
const TAURI_ERROR_HEADER_NAME: &str = "Tauri-Error";
const TAURI_INVOKE_KEY_HEADER_NAME: &str = "Tauri-Invoke-Key";
const TAURI_RESPONSE_HEADER_NAME: &str = "Tauri-Response";
const TAURI_RESPONSE_HEADER_ERROR: &str = "error";
const TAURI_RESPONSE_HEADER_OK: &str = "ok";
pub fn message_handler<R: Runtime>(
manager: Arc<AppManager<R>>,
) -> crate::runtime::webview::WebviewIpcHandler<crate::EventLoopMessage, R> {
Box::new(move |webview, request| handle_ipc_message(request, &manager, &webview.label))
}
pub fn get<R: Runtime>(manager: Arc<AppManager<R>>) -> UriSchemeProtocolHandler {
Box::new(move |label, request, responder| {
#[cfg(feature = "tracing")]
let span = tracing::trace_span!(
"ipc::request",
kind = "custom-protocol",
request = tracing::field::Empty
)
.entered();
let respond = move |mut response: http::Response<Cow<'static, [u8]>>| {
response
.headers_mut()
.insert(ACCESS_CONTROL_ALLOW_ORIGIN, HeaderValue::from_static("*"));
response.headers_mut().insert(
ACCESS_CONTROL_EXPOSE_HEADERS,
HeaderValue::from_static(TAURI_RESPONSE_HEADER_NAME),
);
responder.respond(response);
};
match *request.method() {
Method::POST => {
if let Some(webview) = manager.get_webview(label) {
match parse_invoke_request(&manager, request) {
Ok(request) => {
#[cfg(feature = "tracing")]
span.record(
"request",
match &request.body {
super::InvokeBody::Json(j) => serde_json::to_string(j).unwrap(),
super::InvokeBody::Raw(b) => serde_json::to_string(b).unwrap(),
},
);
#[cfg(feature = "tracing")]
let request_span = tracing::trace_span!("ipc::request::handle", cmd = request.cmd);
webview.on_message(
request,
Box::new(move |_webview, _cmd, response, _callback, _error| {
#[cfg(feature = "tracing")]
let _respond_span = tracing::trace_span!(
parent: &request_span,
"ipc::request::respond"
)
.entered();
#[cfg(feature = "tracing")]
let response_span = match &response {
InvokeResponse::Ok(InvokeResponseBody::Json(v)) => tracing::trace_span!(
"ipc::request::response",
response = v,
mime_type = tracing::field::Empty
)
.entered(),
InvokeResponse::Ok(InvokeResponseBody::Raw(v)) => tracing::trace_span!(
"ipc::request::response",
response = format!("{v:?}"),
mime_type = tracing::field::Empty
)
.entered(),
InvokeResponse::Err(e) => tracing::trace_span!(
"ipc::request::response",
error = format!("{e:?}"),
mime_type = tracing::field::Empty
)
.entered(),
};
let response_header = match &response {
InvokeResponse::Ok(_) => TAURI_RESPONSE_HEADER_OK,
InvokeResponse::Err(_) => TAURI_RESPONSE_HEADER_ERROR,
};
let (mut response, mime_type) = match response {
InvokeResponse::Ok(InvokeResponseBody::Json(v)) => (
http::Response::new(v.into_bytes().into()),
mime::APPLICATION_JSON,
),
InvokeResponse::Ok(InvokeResponseBody::Raw(v)) => (
http::Response::new(v.into()),
mime::APPLICATION_OCTET_STREAM,
),
InvokeResponse::Err(e) => (
http::Response::new(serde_json::to_vec(&e.0).unwrap().into()),
mime::APPLICATION_JSON,
),
};
response
.headers_mut()
.insert(TAURI_RESPONSE_HEADER_NAME, response_header.parse().unwrap());
#[cfg(feature = "tracing")]
response_span.record("mime_type", mime_type.essence_str());
response.headers_mut().insert(
CONTENT_TYPE,
HeaderValue::from_str(mime_type.essence_str()).unwrap(),
);
respond(response);
}),
);
}
Err(e) => {
respond(
http::Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.header(CONTENT_TYPE, mime::TEXT_PLAIN.essence_str())
.body(e.into_bytes().into())
.unwrap(),
);
}
}
} else {
respond(
http::Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.header(CONTENT_TYPE, mime::TEXT_PLAIN.essence_str())
.body("failed to acquire webview reference".as_bytes().into())
.unwrap(),
);
}
}
Method::OPTIONS => {
let mut r = http::Response::new(Vec::new().into());
r.headers_mut()
.insert(ACCESS_CONTROL_ALLOW_HEADERS, HeaderValue::from_static("*"));
respond(r);
}
_ => {
let mut r = http::Response::new("only POST and OPTIONS are allowed".as_bytes().into());
*r.status_mut() = StatusCode::METHOD_NOT_ALLOWED;
r.headers_mut().insert(
CONTENT_TYPE,
HeaderValue::from_str(mime::TEXT_PLAIN.essence_str()).unwrap(),
);
respond(r);
}
}
})
}
fn handle_ipc_message<R: Runtime>(request: Request<String>, manager: &AppManager<R>, label: &str) {
let Some(webview) = manager.get_webview(label) else {
return;
};
#[cfg(feature = "tracing")]
let _span = tracing::trace_span!(
"ipc::request",
kind = "post-message",
uri = request.uri().to_string(),
request = request.body()
)
.entered();
use serde::{Deserialize, Deserializer};
#[derive(Default)]
pub(crate) struct HeaderMap(http::HeaderMap);
impl<'de> Deserialize<'de> for HeaderMap {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let map = std::collections::HashMap::<String, String>::deserialize(deserializer)?;
let mut headers = http::HeaderMap::default();
for (key, value) in map {
if let (Ok(key), Ok(value)) = (
http::header::HeaderName::from_bytes(key.as_bytes()),
http::HeaderValue::from_str(&value),
) {
headers.insert(key, value);
} else {
return Err(serde::de::Error::custom(format!(
"invalid header `{key}` `{value}`"
)));
}
}
Ok(Self(headers))
}
}
#[derive(Deserialize, Default)]
#[serde(rename_all = "camelCase")]
struct RequestOptions {
#[serde(default)]
headers: HeaderMap,
#[serde(default)]
custom_protocol_ipc_blocked: bool,
}
#[derive(Deserialize)]
struct Message {
cmd: String,
callback: CallbackFn,
error: CallbackFn,
payload: serde_json::Value,
options: Option<RequestOptions>,
#[serde(rename = "__TAURI_INVOKE_KEY__")]
invoke_key: String,
}
#[allow(unused_mut)]
let mut invoke_message: Option<crate::Result<Message>> = None;
#[cfg(feature = "isolation")]
{
#[derive(Deserialize)]
struct IsolationMessage<'a> {
cmd: String,
callback: CallbackFn,
error: CallbackFn,
payload: crate::utils::pattern::isolation::RawIsolationPayload<'a>,
options: Option<RequestOptions>,
#[serde(rename = "__TAURI_INVOKE_KEY__")]
invoke_key: String,
}
if let crate::Pattern::Isolation { crypto_keys, .. } = &*manager.pattern {
#[cfg(feature = "tracing")]
let _span = tracing::trace_span!("ipc::request::decrypt_isolation_payload").entered();
invoke_message.replace(
serde_json::from_str::<IsolationMessage<'_>>(request.body())
.map_err(Into::into)
.and_then(|message| {
let is_raw =
message.payload.content_type() == &mime::APPLICATION_OCTET_STREAM.to_string();
let payload = crypto_keys.decrypt(message.payload)?;
Ok(Message {
cmd: message.cmd,
callback: message.callback,
error: message.error,
payload: if is_raw {
payload.into()
} else {
serde_json::from_slice(&payload)?
},
options: message.options,
invoke_key: message.invoke_key,
})
}),
);
}
}
let message = invoke_message.unwrap_or_else(|| {
#[cfg(feature = "tracing")]
let _span = tracing::trace_span!("ipc::request::deserialize").entered();
serde_json::from_str::<Message>(request.body()).map_err(Into::into)
});
match message {
Ok(message) => {
let options = message.options.unwrap_or_default();
let request = InvokeRequest {
cmd: message.cmd,
callback: message.callback,
error: message.error,
url: Url::parse(&request.uri().to_string()).expect("invalid IPC request URL"),
body: message.payload.into(),
headers: options.headers.0,
invoke_key: message.invoke_key,
};
#[cfg(feature = "tracing")]
let request_span = tracing::trace_span!("ipc::request::handle", cmd = request.cmd);
webview.on_message(
request,
Box::new(move |webview, cmd, response, callback, error| {
use crate::ipc::Channel;
#[cfg(feature = "tracing")]
let _respond_span = tracing::trace_span!(
parent: &request_span,
"ipc::request::respond"
)
.entered();
fn responder_eval<R: Runtime>(
webview: &crate::Webview<R>,
js: crate::Result<String>,
error: CallbackFn,
) {
let eval_js = js.unwrap_or_else(|e| {
crate::ipc::format_callback::format(error, &e.to_string())
.expect("unable to serialize response error string to json")
});
let _ = webview.eval(eval_js);
}
let can_use_channel_for_response = cmd != crate::ipc::channel::FETCH_CHANNEL_DATA_COMMAND
&& !options.custom_protocol_ipc_blocked;
#[cfg(feature = "tracing")]
let mime_type = match &response {
InvokeResponse::Ok(InvokeResponseBody::Json(_)) => mime::APPLICATION_JSON,
InvokeResponse::Ok(InvokeResponseBody::Raw(_)) => mime::APPLICATION_OCTET_STREAM,
InvokeResponse::Err(_) => mime::APPLICATION_JSON,
};
#[cfg(feature = "tracing")]
let _response_span = match &response {
InvokeResponse::Ok(InvokeResponseBody::Json(v)) => tracing::trace_span!(
"ipc::request::response",
response = v,
mime_type = mime_type.essence_str()
)
.entered(),
InvokeResponse::Ok(InvokeResponseBody::Raw(v)) => tracing::trace_span!(
"ipc::request::response",
response = format!("{v:?}"),
mime_type = mime_type.essence_str()
)
.entered(),
InvokeResponse::Err(e) => tracing::trace_span!(
"ipc::request::response",
response = format!("{e:?}"),
mime_type = mime_type.essence_str()
)
.entered(),
};
match response {
InvokeResponse::Ok(InvokeResponseBody::Json(v)) => {
if !(cfg!(target_os = "macos") || cfg!(target_os = "ios"))
&& (v.starts_with('{') || v.starts_with('['))
&& can_use_channel_for_response
{
let _ =
Channel::from_callback_fn(webview, callback).send(InvokeResponseBody::Json(v));
} else {
responder_eval(
&webview,
crate::ipc::format_callback::format_result_raw(
Result::<_, String>::Ok(v),
callback,
error,
),
error,
)
}
}
InvokeResponse::Ok(InvokeResponseBody::Raw(v)) => {
if cfg!(target_os = "macos")
|| cfg!(target_os = "ios")
|| !can_use_channel_for_response
{
responder_eval(
&webview,
crate::ipc::format_callback::format_result(
Result::<_, ()>::Ok(v),
callback,
error,
),
error,
);
} else {
let _ =
Channel::from_callback_fn(webview, callback).send(InvokeResponseBody::Raw(v));
}
}
InvokeResponse::Err(e) => responder_eval(
&webview,
crate::ipc::format_callback::format_result(
Result::<(), _>::Err(&e.0),
callback,
error,
),
error,
),
}
}),
);
}
Err(e) => {
#[cfg(feature = "tracing")]
tracing::trace!("ipc.request.error {e}");
let _ = webview.eval(format!(
r#"console.error({})"#,
serde_json::Value::String(e.to_string())
));
}
}
}
fn parse_invoke_request<R: Runtime>(
#[allow(unused_variables)] manager: &AppManager<R>,
request: http::Request<Vec<u8>>,
) -> std::result::Result<InvokeRequest, String> {
#[allow(unused_mut)]
let (parts, mut body) = request.into_parts();
// skip leading `/`
let cmd = percent_encoding::percent_decode(&parts.uri.path().as_bytes()[1..])
.decode_utf8_lossy()
.to_string();
// on Android we cannot read the request body
// so we must ignore it because some commands use the IPC for faster response
let has_payload = !body.is_empty();
#[allow(unused_mut)]
let mut content_type = parts
.headers
.get(http::header::CONTENT_TYPE)
.and_then(|h| h.to_str().ok())
.map(|mime| mime.parse())
.unwrap_or(Ok(mime::APPLICATION_OCTET_STREAM))
.map_err(|_| "unknown content type")?;
#[cfg(feature = "isolation")]
if let crate::Pattern::Isolation { crypto_keys, .. } = &*manager.pattern {
// if the platform does not support request body, we ignore it
if has_payload {
#[cfg(feature = "tracing")]
let _span = tracing::trace_span!("ipc::request::decrypt_isolation_payload").entered();
(body, content_type) = crate::utils::pattern::isolation::RawIsolationPayload::try_from(&body)
.and_then(|raw| {
let content_type = raw
.content_type()
.parse()
.unwrap_or(mime::APPLICATION_OCTET_STREAM);
Ok((crypto_keys.decrypt(raw)?, content_type))
})
.map_err(|e| e.to_string())?;
}
}
let invoke_key = parts
.headers
.get(TAURI_INVOKE_KEY_HEADER_NAME)
.ok_or("missing Tauri-Invoke-Key header")?
.to_str()
.map_err(|_| "Tauri invoke key header value must be a string")?
.to_owned();
let url = Url::parse(
parts
.headers
.get("Origin")
.ok_or("missing Origin header")?
.to_str()
.map_err(|_| "Origin header value must be a string")?,
)
.map_err(|_| "Origin header is not a valid URL")?;
let callback = CallbackFn(
parts
.headers
.get(TAURI_CALLBACK_HEADER_NAME)
.ok_or("missing Tauri-Callback header")?
.to_str()
.map_err(|_| "Tauri callback header value must be a string")?
.parse()
.map_err(|_| "Tauri callback header value must be a numeric string")?,
);
let error = CallbackFn(
parts
.headers
.get(TAURI_ERROR_HEADER_NAME)
.ok_or("missing Tauri-Error header")?
.to_str()
.map_err(|_| "Tauri error header value must be a string")?
.parse()
.map_err(|_| "Tauri error header value must be a numeric string")?,
);
#[cfg(feature = "tracing")]
let span = tracing::trace_span!("ipc::request::deserialize").entered();
let body = if content_type == mime::APPLICATION_OCTET_STREAM {
body.into()
} else if content_type == mime::APPLICATION_JSON {
// if the platform does not support request body, we ignore it
if has_payload {
serde_json::from_slice::<serde_json::Value>(&body)
.map_err(|e| e.to_string())?
.into()
} else {
serde_json::Value::Object(Default::default()).into()
}
} else {
return Err(format!("content type {content_type} is not implemented"));
};
#[cfg(feature = "tracing")]
drop(span);
let payload = InvokeRequest {
cmd,
callback,
error,
url,
body,
headers: parts.headers,
invoke_key,
};
Ok(payload)
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use super::*;
use crate::{StateManager, Wry, ipc::InvokeBody, manager::AppManager, plugin::PluginStore};
use http::header::*;
use serde_json::json;
use tauri_macros::generate_context;
#[test]
fn parse_invoke_request() {
let context = generate_context!("test/fixture/src-tauri/tauri.conf.json", crate, test = true);
let manager: AppManager<Wry> = AppManager::with_handlers(
context,
PluginStore::default(),
Box::new(|_| false),
None,
#[cfg(any(target_os = "macos", target_os = "ios"))]
None,
Default::default(),
Default::default(),
StateManager::new(),
Default::default(),
#[cfg(all(desktop, feature = "tray-icon"))]
Default::default(),
Default::default(),
Default::default(),
Default::default(),
"".into(),
None,
crate::generate_invoke_key().unwrap(),
);
let cmd = "write_something";
let url = "tauri://localhost";
let invoke_key = "1234ahdsjkl123";
let callback = 12378123;
let error = 6243;
let mut headers = HeaderMap::from_iter(vec![
(
CONTENT_TYPE,
HeaderValue::from_str(mime::APPLICATION_OCTET_STREAM.as_ref()).unwrap(),
),
(
HeaderName::from_str(TAURI_INVOKE_KEY_HEADER_NAME).unwrap(),
HeaderValue::from_str(invoke_key).unwrap(),
),
(
HeaderName::from_str(TAURI_CALLBACK_HEADER_NAME).unwrap(),
HeaderValue::from_str(&callback.to_string()).unwrap(),
),
(
HeaderName::from_str(TAURI_ERROR_HEADER_NAME).unwrap(),
HeaderValue::from_str(&error.to_string()).unwrap(),
),
(ORIGIN, HeaderValue::from_str("tauri://localhost").unwrap()),
]);
let mut request = Request::builder().uri(format!("ipc://localhost/{cmd}"));
*request.headers_mut().unwrap() = headers.clone();
let body = vec![123, 31, 45];
let request = request.body(body.clone()).unwrap();
let invoke_request = super::parse_invoke_request(&manager, request).unwrap();
assert_eq!(invoke_request.cmd, cmd);
assert_eq!(invoke_request.callback.0, callback);
assert_eq!(invoke_request.error.0, error);
assert_eq!(invoke_request.invoke_key, invoke_key);
assert_eq!(invoke_request.url, url.parse().unwrap());
assert_eq!(invoke_request.headers, headers);
assert_eq!(invoke_request.body, InvokeBody::Raw(body));
let body = json!({
"key": 1,
"anotherKey": "asda",
});
headers.insert(
CONTENT_TYPE,
HeaderValue::from_str(mime::APPLICATION_JSON.as_ref()).unwrap(),
);
let mut request = Request::builder().uri(format!("ipc://localhost/{cmd}"));
*request.headers_mut().unwrap() = headers.clone();
let request = request.body(serde_json::to_vec(&body).unwrap()).unwrap();
let invoke_request = super::parse_invoke_request(&manager, request).unwrap();
assert_eq!(invoke_request.headers, headers);
assert_eq!(invoke_request.body, InvokeBody::Json(body));
}
#[test]
#[cfg(feature = "isolation")]
fn parse_invoke_request_isolation() {
let context = generate_context!(
"test/fixture/isolation/src-tauri/tauri.conf.json",
crate,
test = false
);
let crate::pattern::Pattern::Isolation { crypto_keys, .. } = &context.pattern else {
unreachable!()
};
let mut nonce = [0u8; 12];
getrandom::fill(&mut nonce).unwrap();
let body_raw = vec![1, 41, 65, 12, 78];
let body_bytes = crypto_keys.aes_gcm().encrypt(&nonce, &body_raw).unwrap();
let isolation_payload_raw = json!({
"nonce": nonce,
"payload": body_bytes,
"contentType": mime::APPLICATION_OCTET_STREAM.to_string(),
});
let body_json = json!({
"key": 1,
"anotherKey": "string"
});
let body_bytes = crypto_keys
.aes_gcm()
.encrypt(&nonce, &serde_json::to_vec(&body_json).unwrap())
.unwrap();
let isolation_payload_json = json!({
"nonce": nonce,
"payload": body_bytes,
"contentType": mime::APPLICATION_JSON.to_string(),
});
let manager: AppManager<Wry> = AppManager::with_handlers(
context,
PluginStore::default(),
Box::new(|_| false),
None,
#[cfg(any(target_os = "macos", target_os = "ios"))]
None,
Default::default(),
Default::default(),
StateManager::new(),
Default::default(),
#[cfg(all(desktop, feature = "tray-icon"))]
Default::default(),
Default::default(),
Default::default(),
Default::default(),
"".into(),
None,
crate::generate_invoke_key().unwrap(),
);
let cmd = "write_something";
let url = "tauri://localhost";
let invoke_key = "1234ahdsjkl123";
let callback = 12378123;
let error = 6243;
let headers = HeaderMap::from_iter(vec![
(
CONTENT_TYPE,
HeaderValue::from_str(mime::APPLICATION_JSON.as_ref()).unwrap(),
),
(
HeaderName::from_str(TAURI_INVOKE_KEY_HEADER_NAME).unwrap(),
HeaderValue::from_str(invoke_key).unwrap(),
),
(
HeaderName::from_str(TAURI_CALLBACK_HEADER_NAME).unwrap(),
HeaderValue::from_str(&callback.to_string()).unwrap(),
),
(
HeaderName::from_str(TAURI_ERROR_HEADER_NAME).unwrap(),
HeaderValue::from_str(&error.to_string()).unwrap(),
),
(ORIGIN, HeaderValue::from_str("tauri://localhost").unwrap()),
]);
let mut request = Request::builder().uri(format!("ipc://localhost/{cmd}"));
*request.headers_mut().unwrap() = headers.clone();
let body = serde_json::to_vec(&isolation_payload_raw).unwrap();
let request = request.body(body).unwrap();
let invoke_request = super::parse_invoke_request(&manager, request).unwrap();
assert_eq!(invoke_request.cmd, cmd);
assert_eq!(invoke_request.callback.0, callback);
assert_eq!(invoke_request.error.0, error);
assert_eq!(invoke_request.invoke_key, invoke_key);
assert_eq!(invoke_request.url, url.parse().unwrap());
assert_eq!(invoke_request.headers, headers);
assert_eq!(invoke_request.body, InvokeBody::Raw(body_raw));
let mut request = Request::builder().uri(format!("ipc://localhost/{cmd}"));
*request.headers_mut().unwrap() = headers.clone();
let body = serde_json::to_vec(&isolation_payload_json).unwrap();
let request = request.body(body).unwrap();
let invoke_request = super::parse_invoke_request(&manager, request).unwrap();
assert_eq!(invoke_request.headers, headers);
assert_eq!(invoke_request.body, InvokeBody::Json(body_json));
}
}

1267
crates/tauri/src/lib.rs Normal file

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,92 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::{
collections::HashMap,
fmt,
sync::{Arc, Mutex},
};
use crate::{
AppHandle, Manager, Resource, ResourceId, Runtime,
app::GlobalTrayIconEventListener,
image::Image,
tray::{TrayIcon, TrayIconEvent, TrayIconId},
};
pub struct TrayManager<R: Runtime> {
/// icon passed in from [`tauri_utils::config::TrayIconConfig::icon_path`],
/// used when creating tray icons from the configs.
pub(crate) icon: Option<Image<'static>>,
/// Tray icons
pub(crate) icons: Mutex<Vec<(TrayIconId, ResourceId)>>,
/// Global Tray icon event listeners.
pub(crate) global_event_listeners: Mutex<Vec<GlobalTrayIconEventListener<AppHandle<R>>>>,
/// Tray icon event listeners.
pub(crate) event_listeners: Mutex<HashMap<TrayIconId, GlobalTrayIconEventListener<TrayIcon<R>>>>,
}
impl<R: Runtime> fmt::Debug for TrayManager<R> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TrayManager")
.field("icon", &self.icon)
.finish()
}
}
impl<R: Runtime> TrayManager<R> {
pub fn on_tray_icon_event<F: Fn(&AppHandle<R>, TrayIconEvent) + Send + Sync + 'static>(
&self,
handler: F,
) {
self
.global_event_listeners
.lock()
.unwrap()
.push(Box::new(handler));
}
pub fn tray_by_id<'a, I>(&self, app: &AppHandle<R>, id: &'a I) -> Option<TrayIcon<R>>
where
I: ?Sized,
TrayIconId: PartialEq<&'a I>,
{
let icons = self.icons.lock().unwrap();
icons.iter().find_map(|(tray_icon_id, rid)| {
if tray_icon_id == &id {
let icon = app.resources_table().get::<TrayIcon<R>>(*rid).ok()?;
Some(Arc::unwrap_or_clone(icon))
} else {
None
}
})
}
pub fn tray_resource_by_id<'a, I>(&self, id: &'a I) -> Option<ResourceId>
where
I: ?Sized,
TrayIconId: PartialEq<&'a I>,
{
let icons = self.icons.lock().unwrap();
icons.iter().find_map(|(tray_icon_id, rid)| {
if tray_icon_id == &id {
Some(*rid)
} else {
None
}
})
}
pub fn remove_tray_by_id<'a, I>(&self, app: &AppHandle<R>, id: &'a I) -> Option<TrayIcon<R>>
where
I: ?Sized,
TrayIconId: PartialEq<&'a I>,
{
let rid = self.tray_resource_by_id(id)?;
let icon = app.resources_table().take::<TrayIcon<R>>(rid).ok()?;
let icon_to_return = icon.clone();
icon.close();
Some(Arc::unwrap_or_clone(icon_to_return))
}
}

View File

@@ -0,0 +1,793 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::{
borrow::Cow,
collections::{HashMap, HashSet},
fmt,
fs::create_dir_all,
sync::{Arc, Mutex, MutexGuard},
};
use serde::Serialize;
use serialize_to_javascript::{DefaultTemplate, Template, default_template};
use tauri_runtime::{
webview::{DetachedWebview, InitializationScript, PendingWebview},
window::DragDropEvent,
};
use tauri_utils::config::WebviewUrl;
use url::Url;
use crate::{
EventLoopMessage, EventTarget, Manager, Runtime, Scopes, UriSchemeContext, Webview, Window,
app::{GlobalWebviewEventListener, OnPageLoad, UriSchemeResponder, WebviewEvent},
ipc::InvokeHandler,
pattern::PatternJavascript,
sealed::ManagerBase,
webview::PageLoadPayload,
};
#[cfg(any(target_os = "macos", target_os = "ios"))]
use crate::app::OnWebContentProcessTerminate;
use super::{
window::{DRAG_DROP_EVENT, DRAG_ENTER_EVENT, DRAG_LEAVE_EVENT, DRAG_OVER_EVENT, DragDropPayload},
{AppManager, EmitPayload},
};
// we need to proxy the dev server on mobile because we can't use `localhost`, so we use the local IP address
// and we do not get a secure context without the custom protocol that proxies to the dev server
// additionally, we need the custom protocol to inject the initialization scripts on Android
// must also keep in sync with the `let mut response` assignment in prepare_uri_scheme_protocol
pub(crate) const PROXY_DEV_SERVER: bool = cfg!(all(dev, mobile));
pub(crate) const PROCESS_IPC_MESSAGE_FN: &str =
include_str!("../../scripts/process-ipc-message-fn.js");
#[cfg(feature = "isolation")]
#[derive(Template)]
#[default_template("../../scripts/isolation.js")]
pub(crate) struct IsolationJavascript<'a> {
pub(crate) isolation_src: &'a str,
pub(crate) style: &'a str,
}
#[derive(Template)]
#[default_template("../../scripts/ipc.js")]
pub(crate) struct IpcJavascript<'a> {
pub(crate) isolation_origin: &'a str,
}
/// Uses a custom URI scheme handler to resolve file requests
pub struct UriSchemeProtocol<R: Runtime> {
/// Handler for protocol
#[allow(clippy::type_complexity)]
pub handler:
Box<dyn Fn(UriSchemeContext<'_, R>, http::Request<Vec<u8>>, UriSchemeResponder) + Send + Sync>,
}
pub struct WebviewManager<R: Runtime> {
pub webviews: Mutex<HashMap<String, Webview<R>>>,
/// The JS message handler.
pub invoke_handler: Box<InvokeHandler<R>>,
/// The page load hook, invoked when the webview performs a navigation.
pub on_page_load: Option<Arc<OnPageLoad<R>>>,
/// The permission request hook, invoked when the webview requests a permission.
pub on_permission_request: Option<Arc<crate::webview::PermissionRequestHandler<R>>>,
/// The web content process termination hook.
#[cfg(any(target_os = "macos", target_os = "ios"))]
pub on_web_content_process_terminate: Option<Arc<OnWebContentProcessTerminate<R>>>,
/// The webview protocols available to all webviews.
pub uri_scheme_protocols: Mutex<HashMap<String, Arc<UriSchemeProtocol<R>>>>,
/// Webview event listeners to all webviews.
pub event_listeners: Arc<Vec<GlobalWebviewEventListener<R>>>,
/// The script that initializes the invoke system.
pub invoke_initialization_script: String,
/// A runtime generated invoke key.
pub(crate) invoke_key: String,
}
impl<R: Runtime> fmt::Debug for WebviewManager<R> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("WebviewManager")
.field(
"invoke_initialization_script",
&self.invoke_initialization_script,
)
.field("invoke_key", &self.invoke_key)
.finish()
}
}
impl<R: Runtime> WebviewManager<R> {
pub(crate) fn register_uri_scheme_protocol<N: Into<String>>(
&self,
uri_scheme: N,
protocol: Arc<UriSchemeProtocol<R>>,
) {
let uri_scheme = uri_scheme.into();
self
.uri_scheme_protocols
.lock()
.unwrap()
.insert(uri_scheme, protocol);
}
/// Get a locked handle to the webviews.
pub(crate) fn webviews_lock(&self) -> MutexGuard<'_, HashMap<String, Webview<R>>> {
self.webviews.lock().expect("poisoned webview manager")
}
fn prepare_pending_webview<M: Manager<R>>(
&self,
mut pending: PendingWebview<EventLoopMessage, R>,
label: &str,
window_label: &str,
manager: &M,
) -> crate::Result<PendingWebview<EventLoopMessage, R>> {
let app_manager = manager.manager();
let plugin_init_scripts = app_manager
.plugins
.lock()
.expect("poisoned plugin store")
.initialization_script();
let pattern_init = PatternJavascript {
pattern: (&*app_manager.pattern).into(),
}
.render_default(&Default::default())?;
let mut webview_attributes = pending.webview_attributes;
let use_https_scheme = webview_attributes.use_https_scheme;
let ipc_init = IpcJavascript {
isolation_origin: &match &*app_manager.pattern {
#[cfg(feature = "isolation")]
crate::Pattern::Isolation { schema, .. } => {
crate::pattern::format_real_schema(schema, use_https_scheme)
}
_ => "".to_owned(),
},
}
.render_default(&Default::default())?;
let mut all_initialization_scripts: Vec<InitializationScript> = vec![];
fn main_frame_script(script: String) -> InitializationScript {
InitializationScript {
script,
for_main_frame_only: true,
}
}
all_initialization_scripts.push(main_frame_script(
r"
Object.defineProperty(window, 'isTauri', {
value: true,
});
if (!window.__TAURI_INTERNALS__) {
Object.defineProperty(window, '__TAURI_INTERNALS__', {
value: {
plugins: {}
}
})
}
"
.to_owned(),
));
all_initialization_scripts.push(main_frame_script(self.invoke_initialization_script.clone()));
all_initialization_scripts.push(main_frame_script(format!(
r#"
Object.defineProperty(window.__TAURI_INTERNALS__, 'metadata', {{
value: {{
currentWindow: {{ label: {current_window_label} }},
currentWebview: {{ label: {current_webview_label} }}
}}
}})
"#,
current_window_label = serde_json::to_string(window_label)?,
current_webview_label = serde_json::to_string(&label)?,
)));
all_initialization_scripts.push(main_frame_script(self.initialization_script(
app_manager,
&ipc_init.into_string(),
&pattern_init.into_string(),
use_https_scheme,
)?));
all_initialization_scripts.extend(plugin_init_scripts);
#[cfg(feature = "isolation")]
if let crate::Pattern::Isolation { schema, .. } = &*app_manager.pattern {
all_initialization_scripts.push(main_frame_script(
IsolationJavascript {
isolation_src: &crate::pattern::format_real_schema(schema, use_https_scheme),
style: tauri_utils::pattern::isolation::IFRAME_STYLE,
}
.render_default(&Default::default())?
.into_string(),
));
}
if let Some(plugin_global_api_scripts) = &*app_manager.plugin_global_api_scripts {
for &script in plugin_global_api_scripts.iter() {
all_initialization_scripts.push(main_frame_script(script.to_owned()));
}
}
// Prepend `all_initialization_scripts` to `webview_attributes.initialization_scripts`
all_initialization_scripts.extend(webview_attributes.initialization_scripts);
webview_attributes.initialization_scripts = all_initialization_scripts;
pending.webview_attributes = webview_attributes;
let mut registered_scheme_protocols = Vec::new();
for (uri_scheme, protocol) in &*self.uri_scheme_protocols.lock().unwrap() {
registered_scheme_protocols.push(uri_scheme.clone());
let protocol = protocol.clone();
let app_handle = manager.app_handle().clone();
pending.register_uri_scheme_protocol(uri_scheme, move |webview_id, request, responder| {
let context = UriSchemeContext {
app_handle: &app_handle,
webview_label: webview_id,
};
(protocol.handler)(context, request, UriSchemeResponder(responder))
});
}
let window_url = Url::parse(&pending.url).unwrap();
let window_origin = if window_url.scheme() == "data" {
"null".into()
} else if (cfg!(windows) || cfg!(target_os = "android"))
&& window_url.scheme() != "http"
&& window_url.scheme() != "https"
{
crate::protocol::origin(window_url.scheme(), use_https_scheme)
} else if let Some(host) = window_url.host() {
format!(
"{}://{}{}",
window_url.scheme(),
host,
window_url
.port()
.map(|p| format!(":{p}"))
.unwrap_or_default()
)
} else {
"null".into()
};
if !registered_scheme_protocols.contains(&"tauri".into()) {
let web_resource_request_handler = pending.web_resource_request_handler.take();
let protocol = crate::protocol::tauri::get(
manager.manager_owned(),
window_origin.clone(),
web_resource_request_handler,
);
pending.register_uri_scheme_protocol("tauri", move |webview_id, request, responder| {
protocol(webview_id, request, UriSchemeResponder(responder))
});
registered_scheme_protocols.push("tauri".into());
}
if !registered_scheme_protocols.contains(&"ipc".into()) {
let protocol = crate::ipc::protocol::get(manager.manager_owned());
pending.register_uri_scheme_protocol("ipc", move |webview_id, request, responder| {
protocol(webview_id, request, UriSchemeResponder(responder))
});
registered_scheme_protocols.push("ipc".into());
}
let label = pending.label.clone();
let app_manager_ = manager.manager_owned();
let on_page_load_handler = pending.on_page_load_handler.take();
pending
.on_page_load_handler
.replace(Box::new(move |url, event| {
let payload = PageLoadPayload { url: &url, event };
if let Some(w) = app_manager_.get_webview(&label) {
if let Some(on_page_load) = &app_manager_.webview.on_page_load {
on_page_load(&w, &payload);
}
app_manager_
.plugins
.lock()
.unwrap()
.on_page_load(&w, &payload);
}
if let Some(handler) = &on_page_load_handler {
handler(url, event);
}
}));
let permission_request_handler = pending.permission_request_handler.take();
if permission_request_handler.is_some() || self.on_permission_request.is_some() {
let label_ = pending.label.clone();
let app_manager_ = manager.manager_owned();
pending
.permission_request_handler
.replace(Box::new(move |kind| {
if let Some(handler) = &permission_request_handler {
let response = handler(kind);
if response != crate::webview::PermissionResponse::Default {
return response;
}
}
if let Some(w) = app_manager_.get_webview(&label_) {
if let Some(on_permission_request) = &app_manager_.webview.on_permission_request {
return on_permission_request(w, kind);
}
}
crate::webview::PermissionResponse::Default
}));
}
#[cfg(any(target_os = "macos", target_os = "ios"))]
if pending.on_web_content_process_terminate_handler.is_none() {
let app_manager_ = manager.manager_owned();
if app_manager_
.webview
.on_web_content_process_terminate
.is_some()
{
let label_ = pending.label.clone();
pending
.on_web_content_process_terminate_handler
.replace(Box::new(move || {
if let Some(w) = app_manager_.get_webview(&label_) {
if let Some(on_web_content_process_terminate) =
&app_manager_.webview.on_web_content_process_terminate
{
on_web_content_process_terminate(&w);
}
}
}));
}
}
#[cfg(feature = "protocol-asset")]
if !registered_scheme_protocols.contains(&"asset".into()) {
let asset_scope = app_manager
.state
.get::<crate::Scopes>()
.asset_protocol
.clone();
let protocol = crate::protocol::asset::get(asset_scope, window_origin.clone());
pending.register_uri_scheme_protocol("asset", move |webview_id, request, responder| {
protocol(webview_id, request, UriSchemeResponder(responder))
});
}
#[cfg(feature = "isolation")]
if let crate::Pattern::Isolation {
assets,
schema,
key: _,
crypto_keys,
} = &*app_manager.pattern
{
let protocol = crate::protocol::isolation::get(
manager.manager_owned(),
schema,
assets.clone(),
*crypto_keys.aes_gcm().raw(),
window_origin,
use_https_scheme,
);
pending.register_uri_scheme_protocol(schema, move |webview_id, request, responder| {
protocol(webview_id, request, UriSchemeResponder(responder))
});
}
Ok(pending)
}
fn initialization_script(
&self,
app_manager: &AppManager<R>,
ipc_script: &str,
pattern_script: &str,
use_https_scheme: bool,
) -> crate::Result<String> {
#[derive(Template)]
#[default_template("../../scripts/init.js")]
struct InitJavascript<'a> {
#[raw]
pattern_script: &'a str,
#[raw]
ipc_script: &'a str,
#[raw]
core_script: &'a str,
#[raw]
event_initialization_script: &'a str,
#[raw]
freeze_prototype: &'a str,
}
#[derive(Template)]
#[default_template("../../scripts/core.js")]
struct CoreJavascript<'a> {
os_name: &'a str,
protocol_scheme: &'a str,
invoke_key: &'a str,
}
let freeze_prototype = if app_manager.config.app.security.freeze_prototype {
include_str!("../../scripts/freeze_prototype.js")
} else {
""
};
InitJavascript {
pattern_script,
ipc_script,
core_script: &CoreJavascript {
os_name: std::env::consts::OS,
protocol_scheme: if use_https_scheme { "https" } else { "http" },
invoke_key: self.invoke_key(),
}
.render_default(&Default::default())?
.into_string(),
event_initialization_script: &crate::event::event_initialization_script(
app_manager.listeners().function_name(),
app_manager.listeners().listeners_object_name(),
),
freeze_prototype,
}
.render_default(&Default::default())
.map(|s| s.into_string())
.map_err(Into::into)
}
pub fn prepare_webview<M: Manager<R>>(
&self,
manager: &M,
mut pending: PendingWebview<EventLoopMessage, R>,
window_label: &str,
) -> crate::Result<PendingWebview<EventLoopMessage, R>> {
if self.webviews_lock().contains_key(&pending.label) {
return Err(crate::Error::WebviewLabelAlreadyExists(pending.label));
}
let app_manager = manager.manager();
#[allow(unused_mut)] // mut url only for the data-url parsing
let mut url = match &pending.webview_attributes.url {
WebviewUrl::App(path) => {
let app_url = app_manager.get_app_url(pending.webview_attributes.use_https_scheme);
let url = if PROXY_DEV_SERVER && is_local_network_url(&app_url) {
Cow::Owned(Url::parse("tauri://localhost").unwrap())
} else {
app_url
};
// ignore "index.html" just to simplify the url
if path.to_str() != Some("index.html") {
url
.join(&path.to_string_lossy())
.map_err(crate::Error::InvalidUrl)
// this will never fail
.unwrap()
} else {
url.into_owned()
}
}
WebviewUrl::External(url) => {
let config_url = app_manager.get_app_url(pending.webview_attributes.use_https_scheme);
let is_app_url = config_url.make_relative(url).is_some();
let mut url = url.clone();
if is_app_url && PROXY_DEV_SERVER && is_local_network_url(&url) {
Url::parse("tauri://localhost").unwrap()
} else {
url
}
}
WebviewUrl::CustomProtocol(url) => url.clone(),
_ => unimplemented!(),
};
#[cfg(not(feature = "webview-data-url"))]
if url.scheme() == "data" {
return Err(crate::Error::InvalidWebviewUrl(
"data URLs are not supported without the `webview-data-url` feature.",
));
}
#[cfg(feature = "webview-data-url")]
if let Some(csp) = app_manager.csp() {
if url.scheme() == "data" {
if let Ok(data_url) = data_url::DataUrl::process(url.as_str()) {
let (body, _) = data_url.decode_to_vec().unwrap();
let html = String::from_utf8_lossy(&body).into_owned();
// naive way to check if it's an html
if html.contains('<') && html.contains('>') {
let document = tauri_utils::html2::parse_doc(html);
tauri_utils::html2::inject_csp(&document, &csp.to_string());
url.set_path(&format!("{},{}", mime::TEXT_HTML, document.html()));
}
}
}
}
pending.url = url.to_string();
#[cfg(target_os = "android")]
{
pending = pending.on_webview_created(move |ctx| {
let plugin_manager = ctx
.env
.call_method(
ctx.activity,
"getPluginManager",
"()Lapp/tauri/plugin/PluginManager;",
&[],
)?
.l()?;
// tell the manager the webview is ready
ctx.env.call_method(
plugin_manager,
"onWebViewCreated",
"(Landroid/webkit/WebView;)V",
&[ctx.webview.into()],
)?;
Ok(())
});
}
let label = pending.label.clone();
pending = self.prepare_pending_webview(pending, &label, window_label, manager)?;
pending.ipc_handler = Some(crate::ipc::protocol::message_handler(
manager.manager_owned(),
));
// in `windows`, we need to force a data_directory
// but we do respect user-specification
#[cfg(any(target_os = "linux", target_os = "windows"))]
if pending.webview_attributes.data_directory.is_none() {
let local_app_data = manager.path().resolve(
&app_manager.config.identifier,
crate::path::BaseDirectory::LocalData,
);
if let Ok(user_data_dir) = local_app_data {
pending.webview_attributes.data_directory = Some(user_data_dir);
}
}
// make sure the directory is created and available to prevent a panic
if let Some(user_data_dir) = &pending.webview_attributes.data_directory {
if !user_data_dir.exists() {
create_dir_all(user_data_dir)?;
}
}
#[cfg(all(desktop, not(target_os = "windows")))]
if pending.webview_attributes.zoom_hotkeys_enabled {
#[derive(Template)]
#[default_template("../webview/scripts/zoom-hotkey.js")]
struct HotkeyZoom<'a> {
os_name: &'a str,
}
pending
.webview_attributes
.initialization_scripts
.push(InitializationScript {
script: HotkeyZoom {
os_name: std::env::consts::OS,
}
.render_default(&Default::default())?
.into_string(),
for_main_frame_only: true,
})
}
#[cfg(feature = "isolation")]
let pattern = app_manager.pattern.clone();
let navigation_handler = pending.navigation_handler.take();
let app_manager = manager.manager_owned();
let label = pending.label.clone();
pending.navigation_handler = Some(Box::new(move |url| {
// always allow navigation events for the isolation iframe and do not emit them for consumers
#[cfg(feature = "isolation")]
if let crate::Pattern::Isolation { schema, .. } = &*pattern {
if url.scheme() == schema
&& url.domain() == Some(crate::pattern::ISOLATION_IFRAME_SRC_DOMAIN)
{
return true;
}
}
if let Some(handler) = &navigation_handler {
if !handler(url) {
return false;
}
}
let webview = app_manager.webview.webviews_lock().get(&label).cloned();
if let Some(w) = webview {
app_manager
.plugins
.lock()
.expect("poisoned plugin store")
.on_navigation(&w, url)
} else {
true
}
}));
Ok(pending)
}
pub(crate) fn attach_webview(
&self,
window: Window<R>,
webview: DetachedWebview<EventLoopMessage, R>,
use_https_scheme: bool,
) -> Webview<R> {
let webview = Webview::new(window, webview, use_https_scheme);
let webview_event_listeners = self.event_listeners.clone();
let webview_ = webview.clone();
webview.on_webview_event(move |event| {
let _ = on_webview_event(&webview_, event);
for handler in webview_event_listeners.iter() {
handler(&webview_, event);
}
});
// insert the webview into our manager
{
self
.webviews_lock()
.insert(webview.label().to_string(), webview.clone());
}
// let plugins know that a new webview has been added to the manager
let manager = webview.manager_owned();
let webview_ = webview.clone();
// run on main thread so the plugin store doesn't dead lock with the event loop handler in App
let _ = webview.run_on_main_thread(move || {
manager
.plugins
.lock()
.expect("poisoned plugin store")
.webview_created(webview_);
});
#[cfg(all(target_os = "ios", feature = "wry"))]
{
webview
.with_webview(|w| {
unsafe { crate::ios::on_webview_created(w.inner() as _, w.view_controller() as _) };
})
.expect("failed to run on_webview_created hook");
}
let event = crate::EventName::from_str("tauri://webview-created");
let payload = Some(crate::webview::CreatedEvent {
label: webview.label().into(),
});
let _ = webview
.manager
.emit(event, EmitPayload::Serialize(&payload));
webview
}
pub fn eval_script_all<S: Into<String>>(&self, script: S) -> crate::Result<()> {
let script = script.into();
let webviews = self.webviews_lock().values().cloned().collect::<Vec<_>>();
webviews
.iter()
.try_for_each(|webview| webview.eval(&script))
}
pub fn labels(&self) -> HashSet<String> {
self.webviews_lock().keys().cloned().collect()
}
pub(crate) fn invoke_key(&self) -> &str {
&self.invoke_key
}
}
impl<R: Runtime> Webview<R> {
/// Emits event to [`EventTarget::Window`] and [`EventTarget::WebviewWindow`]
fn emit_to_webview<S: Serialize>(
&self,
event: crate::EventName<&str>,
payload: &S,
) -> crate::Result<()> {
let window_label = self.label();
let payload = EmitPayload::Serialize(payload);
self
.manager()
.emit_filter(event, payload, |target| match target {
EventTarget::Webview { label } | EventTarget::WebviewWindow { label } => {
label == window_label
}
_ => false,
})
}
}
fn on_webview_event<R: Runtime>(webview: &Webview<R>, event: &WebviewEvent) -> crate::Result<()> {
match event {
WebviewEvent::DragDrop(event) => match event {
DragDropEvent::Enter { paths, position } => {
let payload = DragDropPayload {
paths: Some(paths),
position,
};
webview.emit_to_webview(DRAG_ENTER_EVENT, &payload)?
}
DragDropEvent::Over { position } => {
let payload = DragDropPayload {
position,
paths: None,
};
webview.emit_to_webview(DRAG_OVER_EVENT, &payload)?
}
DragDropEvent::Drop { paths, position } => {
let scopes = webview.state::<Scopes>();
for path in paths {
if path.is_file() {
let _ = scopes.allow_file(path);
} else {
let _ = scopes.allow_directory(path, false);
}
}
let payload = DragDropPayload {
paths: Some(paths),
position,
};
webview.emit_to_webview(DRAG_DROP_EVENT, &payload)?
}
DragDropEvent::Leave => webview.emit_to_webview(DRAG_LEAVE_EVENT, &())?,
_ => unimplemented!(),
},
}
Ok(())
}
fn is_local_network_url(url: &url::Url) -> bool {
match url.host() {
Some(url::Host::Domain(s)) => s == "localhost",
Some(url::Host::Ipv4(_)) | Some(url::Host::Ipv6(_)) => true,
None => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn local_network_url() {
assert!(is_local_network_url(&"http://localhost".parse().unwrap()));
assert!(is_local_network_url(
&"http://127.0.0.1:8080".parse().unwrap()
));
assert!(is_local_network_url(
&"https://192.168.3.17".parse().unwrap()
));
assert!(!is_local_network_url(&"https://tauri.app".parse().unwrap()));
}
}

View File

@@ -0,0 +1,285 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::{
collections::{HashMap, HashSet},
fmt,
path::PathBuf,
sync::{Arc, Mutex, MutexGuard},
};
use serde::Serialize;
use tauri_runtime::{
dpi::{PhysicalPosition, PhysicalSize},
window::WindowBuilder,
window::{DetachedWindow, DragDropEvent, PendingWindow},
};
use crate::{
AppHandle, EventLoopMessage, EventTarget, Manager, Runtime, Scopes, Window, WindowEvent,
app::GlobalWindowEventListener, event::EventName, image::Image, sealed::ManagerBase,
};
use super::EmitPayload;
const WINDOW_RESIZED_EVENT: EventName<&str> = EventName::from_str("tauri://resize");
const WINDOW_MOVED_EVENT: EventName<&str> = EventName::from_str("tauri://move");
const WINDOW_CLOSE_REQUESTED_EVENT: EventName<&str> =
EventName::from_str("tauri://close-requested");
const WINDOW_DESTROYED_EVENT: EventName<&str> = EventName::from_str("tauri://destroyed");
const WINDOW_FOCUS_EVENT: EventName<&str> = EventName::from_str("tauri://focus");
const WINDOW_BLUR_EVENT: EventName<&str> = EventName::from_str("tauri://blur");
const WINDOW_SCALE_FACTOR_CHANGED_EVENT: EventName<&str> =
EventName::from_str("tauri://scale-change");
const WINDOW_THEME_CHANGED: EventName<&str> = EventName::from_str("tauri://theme-changed");
pub(crate) const DRAG_ENTER_EVENT: EventName<&str> = EventName::from_str("tauri://drag-enter");
pub(crate) const DRAG_OVER_EVENT: EventName<&str> = EventName::from_str("tauri://drag-over");
pub(crate) const DRAG_DROP_EVENT: EventName<&str> = EventName::from_str("tauri://drag-drop");
pub(crate) const DRAG_LEAVE_EVENT: EventName<&str> = EventName::from_str("tauri://drag-leave");
#[cfg(mobile)]
pub(crate) const WINDOW_SUSPENDED_EVENT: EventName<&str> = EventName::from_str("tauri://suspended");
#[cfg(mobile)]
pub(crate) const WINDOW_RESUMED_EVENT: EventName<&str> = EventName::from_str("tauri://resumed");
pub struct WindowManager<R: Runtime> {
pub windows: Mutex<HashMap<String, Window<R>>>,
pub default_icon: Option<Image<'static>>,
/// Window event listeners to all windows.
pub event_listeners: Arc<Vec<GlobalWindowEventListener<R>>>,
}
impl<R: Runtime> fmt::Debug for WindowManager<R> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("WindowManager")
.field("default_window_icon", &self.default_icon)
.finish()
}
}
impl<R: Runtime> WindowManager<R> {
/// Get a locked handle to the windows.
pub(crate) fn windows_lock(&self) -> MutexGuard<'_, HashMap<String, Window<R>>> {
self.windows.lock().expect("poisoned window manager")
}
pub fn prepare_window(
&self,
mut pending: PendingWindow<EventLoopMessage, R>,
) -> crate::Result<PendingWindow<EventLoopMessage, R>> {
if self.windows_lock().contains_key(&pending.label) {
return Err(crate::Error::WindowLabelAlreadyExists(pending.label));
}
if !pending.window_builder.has_icon() {
if let Some(default_window_icon) = self.default_icon.clone() {
pending.window_builder = pending.window_builder.icon(default_window_icon.into())?;
}
}
Ok(pending)
}
pub(crate) fn attach_window(
&self,
app_handle: AppHandle<R>,
window: DetachedWindow<EventLoopMessage, R>,
#[cfg(desktop)] menu: Option<crate::window::WindowMenu<R>>,
) -> Window<R> {
let window = Window::new(
app_handle.manager.clone(),
window,
app_handle,
#[cfg(desktop)]
menu,
);
let window_ = window.clone();
let window_event_listeners = self.event_listeners.clone();
window.on_window_event(move |event| {
let _ = on_window_event(&window_, event);
for handler in window_event_listeners.iter() {
handler(&window_, event);
}
});
// insert the window into our manager
{
self
.windows_lock()
.insert(window.label().to_string(), window.clone());
}
// let plugins know that a new window has been added to the manager
let manager = window.manager.clone();
let window_ = window.clone();
// run on main thread so the plugin store doesn't dead lock with the event loop handler in App
let _ = window.run_on_main_thread(move || {
manager
.plugins
.lock()
.expect("poisoned plugin store")
.window_created(window_);
});
window
}
pub fn labels(&self) -> HashSet<String> {
self.windows_lock().keys().cloned().collect()
}
}
impl<R: Runtime> Window<R> {
/// Emits event to [`EventTarget::Window`] and [`EventTarget::WebviewWindow`]
fn emit_to_window<S: Serialize>(&self, event: EventName<&str>, payload: &S) -> crate::Result<()> {
let window_label = self.label();
let payload = EmitPayload::Serialize(payload);
self
.manager()
.emit_filter(event, payload, |target| match target {
EventTarget::Window { label } | EventTarget::WebviewWindow { label } => {
label == window_label
}
_ => false,
})
}
/// Checks whether has js listener for [`EventTarget::Window`] or [`EventTarget::WebviewWindow`]
fn has_js_listener(&self, event: EventName<&str>) -> bool {
let window_label = self.label();
let listeners = self.manager().listeners();
listeners.has_js_listener(event, |target| match target {
EventTarget::Window { label } | EventTarget::WebviewWindow { label } => label == window_label,
_ => false,
})
}
}
#[derive(Serialize, Clone)]
pub(crate) struct DragDropPayload<'a> {
#[serde(skip_serializing_if = "Option::is_none")]
pub paths: Option<&'a Vec<PathBuf>>,
pub position: &'a PhysicalPosition<f64>,
}
fn on_window_event<R: Runtime>(window: &Window<R>, event: &WindowEvent) -> crate::Result<()> {
match event {
WindowEvent::Resized(size) => window.emit_to_window(WINDOW_RESIZED_EVENT, size)?,
WindowEvent::Moved(position) => window.emit_to_window(WINDOW_MOVED_EVENT, position)?,
WindowEvent::CloseRequested { api } => {
if window.has_js_listener(WINDOW_CLOSE_REQUESTED_EVENT) {
api.prevent_close();
}
window.emit_to_window(WINDOW_CLOSE_REQUESTED_EVENT, &())?;
}
WindowEvent::Destroyed => {
window.emit_to_window(WINDOW_DESTROYED_EVENT, &())?;
}
WindowEvent::Focused(focused) => window.emit_to_window(
if *focused {
WINDOW_FOCUS_EVENT
} else {
WINDOW_BLUR_EVENT
},
&(),
)?,
WindowEvent::ScaleFactorChanged {
scale_factor,
new_inner_size,
..
} => window.emit_to_window(
WINDOW_SCALE_FACTOR_CHANGED_EVENT,
&ScaleFactorChanged {
scale_factor: *scale_factor,
size: *new_inner_size,
},
)?,
WindowEvent::DragDrop(event) => match event {
DragDropEvent::Enter { paths, position } => {
let payload = DragDropPayload {
paths: Some(paths),
position,
};
if window.is_webview_window() {
// use underlying manager, otherwise have to recheck EventName
window.manager().emit_to(
EventTarget::labeled(window.label()),
DRAG_ENTER_EVENT,
EmitPayload::Serialize(&payload),
)?
} else {
window.emit_to_window(DRAG_ENTER_EVENT, &payload)?
}
}
DragDropEvent::Over { position } => {
let payload = DragDropPayload {
position,
paths: None,
};
if window.is_webview_window() {
// use underlying manager, otherwise have to recheck EventName
window.manager().emit_to(
EventTarget::labeled(window.label()),
DRAG_OVER_EVENT,
EmitPayload::Serialize(&payload),
)?
} else {
window.emit_to_window(DRAG_OVER_EVENT, &payload)?
}
}
DragDropEvent::Drop { paths, position } => {
let scopes = window.state::<Scopes>();
for path in paths {
if path.is_file() {
let _ = scopes.allow_file(path);
} else {
let _ = scopes.allow_directory(path, true);
}
}
let payload = DragDropPayload {
paths: Some(paths),
position,
};
if window.is_webview_window() {
// use underlying manager, otherwise have to recheck EventName
window.manager().emit_to(
EventTarget::labeled(window.label()),
DRAG_DROP_EVENT,
EmitPayload::Serialize(&payload),
)?
} else {
window.emit_to_window(DRAG_DROP_EVENT, &payload)?
}
}
DragDropEvent::Leave => {
if window.is_webview_window() {
// use underlying manager, otherwise have to recheck EventName
window.manager().emit_to(
EventTarget::labeled(window.label()),
DRAG_LEAVE_EVENT,
EmitPayload::Serialize(&()),
)?
} else {
window.emit_to_window(DRAG_LEAVE_EVENT, &())?
}
}
_ => unimplemented!(),
},
WindowEvent::ThemeChanged(theme) => window.emit_to_window(WINDOW_THEME_CHANGED, &theme)?,
#[cfg(mobile)]
WindowEvent::Suspended => window.emit_to_window(WINDOW_SUSPENDED_EVENT, &())?,
#[cfg(mobile)]
WindowEvent::Resumed => window.emit_to_window(WINDOW_RESUMED_EVENT, &())?,
}
Ok(())
}
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct ScaleFactorChanged {
scale_factor: f64,
size: PhysicalSize<u32>,
}

View File

@@ -0,0 +1,90 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use crate::{Manager, Runtime, menu::CheckMenuItem, menu::MenuId};
/// A builder type for [`CheckMenuItem`]
pub struct CheckMenuItemBuilder {
id: Option<MenuId>,
text: String,
enabled: bool,
checked: bool,
accelerator: Option<String>,
}
impl CheckMenuItemBuilder {
/// Create a new menu item builder.
///
/// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic
/// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
pub fn new<S: AsRef<str>>(text: S) -> Self {
Self {
id: None,
text: text.as_ref().to_string(),
enabled: true,
checked: true,
accelerator: None,
}
}
/// Create a new menu item builder with the specified id.
///
/// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic
/// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
pub fn with_id<I: Into<MenuId>, S: AsRef<str>>(id: I, text: S) -> Self {
Self {
id: Some(id.into()),
text: text.as_ref().to_string(),
enabled: true,
checked: true,
accelerator: None,
}
}
/// Set the id for this menu item.
pub fn id<I: Into<MenuId>>(mut self, id: I) -> Self {
self.id.replace(id.into());
self
}
/// Set the enabled state for this menu item.
pub fn enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
/// Set the checked state for this menu item.
pub fn checked(mut self, checked: bool) -> Self {
self.checked = checked;
self
}
/// Set the accelerator for this menu item.
pub fn accelerator<S: AsRef<str>>(mut self, accelerator: S) -> Self {
self.accelerator.replace(accelerator.as_ref().to_string());
self
}
/// Build the menu item
pub fn build<R: Runtime, M: Manager<R>>(self, manager: &M) -> crate::Result<CheckMenuItem<R>> {
if let Some(id) = self.id {
CheckMenuItem::with_id(
manager,
id,
self.text,
self.enabled,
self.checked,
self.accelerator,
)
} else {
CheckMenuItem::new(
manager,
self.text,
self.enabled,
self.checked,
self.accelerator,
)
}
}
}

View File

@@ -0,0 +1,130 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use crate::{
Manager, Runtime,
image::Image,
menu::{IconMenuItem, MenuId, NativeIcon},
};
/// A builder type for [`IconMenuItem`]
pub struct IconMenuItemBuilder<'a> {
id: Option<MenuId>,
text: String,
enabled: bool,
icon: Option<Image<'a>>,
native_icon: Option<NativeIcon>,
accelerator: Option<String>,
}
impl<'a> IconMenuItemBuilder<'a> {
/// Create a new menu item builder.
///
/// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic
/// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
pub fn new<S: AsRef<str>>(text: S) -> Self {
Self {
id: None,
text: text.as_ref().to_string(),
enabled: true,
icon: None,
native_icon: None,
accelerator: None,
}
}
/// Create a new menu item builder with the specified id.
///
/// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic
/// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
pub fn with_id<I: Into<MenuId>, S: AsRef<str>>(id: I, text: S) -> Self {
Self {
id: Some(id.into()),
text: text.as_ref().to_string(),
enabled: true,
icon: None,
native_icon: None,
accelerator: None,
}
}
/// Set the id for this menu item.
pub fn id<I: Into<MenuId>>(mut self, id: I) -> Self {
self.id.replace(id.into());
self
}
/// Set the enabled state for this menu item.
pub fn enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
/// Set the accelerator for this menu item.
pub fn accelerator<S: AsRef<str>>(mut self, accelerator: S) -> Self {
self.accelerator.replace(accelerator.as_ref().to_string());
self
}
/// Set the icon for this menu item.
///
/// **Note:** This method conflicts with [`Self::native_icon`]
/// so calling one of them, will reset the other.
pub fn icon(mut self, icon: Image<'a>) -> Self {
self.icon.replace(icon);
self.native_icon = None;
self
}
/// Set the icon for this menu item.
///
/// **Note:** This method conflicts with [`Self::icon`]
/// so calling one of them, will reset the other.
pub fn native_icon(mut self, icon: NativeIcon) -> Self {
self.native_icon.replace(icon);
self.icon = None;
self
}
/// Build the menu item
pub fn build<R: Runtime, M: Manager<R>>(self, manager: &M) -> crate::Result<IconMenuItem<R>> {
if self.icon.is_some() {
if let Some(id) = self.id {
IconMenuItem::with_id(
manager,
id,
self.text,
self.enabled,
self.icon,
self.accelerator,
)
} else {
IconMenuItem::new(
manager,
self.text,
self.enabled,
self.icon,
self.accelerator,
)
}
} else if let Some(id) = self.id {
IconMenuItem::with_id_and_native_icon(
manager,
id,
self.text,
self.enabled,
self.native_icon,
self.accelerator,
)
} else {
IconMenuItem::with_native_icon(
manager,
self.text,
self.enabled,
self.native_icon,
self.accelerator,
)
}
}
}

View File

@@ -0,0 +1,675 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use crate::{Manager, Runtime, image::Image, menu::*};
/// A builder type for [`Menu`]
///
/// ## Platform-specific:
///
/// - **macOS**: if using [`MenuBuilder`] for the global menubar, it can only contain [`Submenu`]s
///
/// # Example
///
/// ```no_run
/// use tauri::menu::*;
/// tauri::Builder::default()
/// .setup(move |app| {
/// let handle = app.handle();
/// # let icon1 = tauri::image::Image::new(&[], 0, 0);
/// let menu = MenuBuilder::new(handle)
/// .item(&MenuItem::new(handle, "MenuItem 1", true, None::<&str>)?)
/// .items(&[
/// &CheckMenuItem::new(handle, "CheckMenuItem 1", true, true, None::<&str>)?,
/// &IconMenuItem::new(handle, "IconMenuItem 1", true, Some(icon1), None::<&str>)?,
/// ])
/// .separator()
/// .cut()
/// .copy()
/// .paste()
/// .separator()
/// .text("item2", "MenuItem 2")
/// .check("checkitem2", "CheckMenuItem 2")
/// .icon("iconitem2", "IconMenuItem 2", app.default_window_icon().cloned().unwrap())
/// .build()?;
/// app.set_menu(menu);
/// Ok(())
/// });
/// ```
pub struct MenuBuilder<'m, R: Runtime, M: Manager<R>> {
pub(crate) id: Option<MenuId>,
pub(crate) manager: &'m M,
pub(crate) items: Vec<crate::Result<MenuItemKind<R>>>,
}
impl<'m, R: Runtime, M: Manager<R>> MenuBuilder<'m, R, M> {
/// Create a new menu builder.
pub fn new(manager: &'m M) -> Self {
Self {
id: None,
items: Vec::new(),
manager,
}
}
/// Create a new menu builder with the specified id.
pub fn with_id<I: Into<MenuId>>(manager: &'m M, id: I) -> Self {
Self {
id: Some(id.into()),
items: Vec::new(),
manager,
}
}
/// Builds this menu
pub fn build(self) -> crate::Result<Menu<R>> {
let menu = if let Some(id) = self.id {
Menu::with_id(self.manager, id)?
} else {
Menu::new(self.manager)?
};
for item in self.items {
let item = item?;
menu.append(&item)?;
}
Ok(menu)
}
}
/// A builder type for [`Submenu`]
///
/// # Example
///
/// ```no_run
/// use tauri::menu::*;
/// tauri::Builder::default()
/// .setup(move |app| {
/// let handle = app.handle();
/// # let icon1 = tauri::image::Image::new(&[], 0, 0);
/// # let icon2 = icon1.clone();
/// let menu = Menu::new(handle)?;
/// let submenu = SubmenuBuilder::new(handle, "File")
/// .item(&MenuItem::new(handle, "MenuItem 1", true, None::<&str>)?)
/// .items(&[
/// &CheckMenuItem::new(handle, "CheckMenuItem 1", true, true, None::<&str>)?,
/// &IconMenuItem::new(handle, "IconMenuItem 1", true, Some(icon1), None::<&str>)?,
/// ])
/// .separator()
/// .cut()
/// .copy()
/// .paste()
/// .separator()
/// .text("item2", "MenuItem 2")
/// .check("checkitem2", "CheckMenuItem 2")
/// .icon("iconitem2", "IconMenuItem 2", app.default_window_icon().cloned().unwrap())
/// .build()?;
/// menu.append(&submenu)?;
/// app.set_menu(menu);
/// Ok(())
/// });
/// ```
pub struct SubmenuBuilder<'m, R: Runtime, M: Manager<R>> {
pub(crate) id: Option<MenuId>,
pub(crate) manager: &'m M,
pub(crate) text: String,
pub(crate) enabled: bool,
pub(crate) items: Vec<crate::Result<MenuItemKind<R>>>,
pub(crate) icon: Option<crate::image::Image<'m>>,
pub(crate) native_icon: Option<NativeIcon>,
}
impl<'m, R: Runtime, M: Manager<R>> SubmenuBuilder<'m, R, M> {
/// Create a new submenu builder.
///
/// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic
/// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
pub fn new<S: AsRef<str>>(manager: &'m M, text: S) -> Self {
Self {
id: None,
items: Vec::new(),
text: text.as_ref().to_string(),
enabled: true,
manager,
icon: None,
native_icon: None,
}
}
/// Create a new submenu builder with the specified id.
///
/// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic
/// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
pub fn with_id<I: Into<MenuId>, S: AsRef<str>>(manager: &'m M, id: I, text: S) -> Self {
Self {
id: Some(id.into()),
text: text.as_ref().to_string(),
enabled: true,
items: Vec::new(),
manager,
icon: None,
native_icon: None,
}
}
/// Set an icon for the submenu.
/// Calling this method resets the native_icon.
pub fn submenu_icon(mut self, icon: crate::image::Image<'m>) -> Self {
self.icon = Some(icon);
self.native_icon = None;
self
}
/// Set a native icon for the submenu.
/// Calling this method resets the icon.
pub fn submenu_native_icon(mut self, icon: NativeIcon) -> Self {
self.native_icon = Some(icon);
self.icon = None;
self
}
/// Set the enabled state for the submenu.
pub fn enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
/// Builds this submenu
pub fn build(self) -> crate::Result<Submenu<R>> {
let submenu = if let Some(id) = self.id {
if let Some(icon) = self.icon {
Submenu::with_id_and_icon(self.manager, id, self.text, self.enabled, Some(icon))?
} else if let Some(native_icon) = self.native_icon {
Submenu::with_id_and_native_icon(
self.manager,
id,
self.text,
self.enabled,
Some(native_icon),
)?
} else {
Submenu::with_id(self.manager, id, self.text, self.enabled)?
}
} else if let Some(icon) = self.icon {
Submenu::new_with_icon(self.manager, self.text, self.enabled, Some(icon))?
} else if let Some(native_icon) = self.native_icon {
Submenu::new_with_native_icon(self.manager, self.text, self.enabled, Some(native_icon))?
} else {
Submenu::new(self.manager, self.text, self.enabled)?
};
for item in self.items {
let item = item?;
submenu.append(&item)?;
}
Ok(submenu)
}
}
macro_rules! shared_menu_builder {
($menu: ty) => {
impl<'m, R: Runtime, M: Manager<R>> $menu {
/// Set the id for this menu.
pub fn id<I: Into<MenuId>>(mut self, id: I) -> Self {
self.id.replace(id.into());
self
}
/// Add this item to the menu.
pub fn item(mut self, item: &dyn IsMenuItem<R>) -> Self {
self.items.push(Ok(item.kind()));
self
}
/// Add these items to the menu.
pub fn items(mut self, items: &[&dyn IsMenuItem<R>]) -> Self {
for item in items {
self = self.item(*item);
}
self
}
/// Add a [MenuItem] to the menu.
pub fn text<I: Into<MenuId>, S: AsRef<str>>(mut self, id: I, text: S) -> Self {
self
.items
.push(MenuItem::with_id(self.manager, id, text, true, None::<&str>).map(|i| i.kind()));
self
}
/// Add a [CheckMenuItem] to the menu.
pub fn check<I: Into<MenuId>, S: AsRef<str>>(mut self, id: I, text: S) -> Self {
self.items.push(
CheckMenuItem::with_id(self.manager, id, text, true, true, None::<&str>)
.map(|i| i.kind()),
);
self
}
/// Add an [IconMenuItem] to the menu.
pub fn icon<I: Into<MenuId>, S: AsRef<str>>(
mut self,
id: I,
text: S,
icon: Image<'_>,
) -> Self {
self.items.push(
IconMenuItem::with_id(self.manager, id, text, true, Some(icon), None::<&str>)
.map(|i| i.kind()),
);
self
}
/// Add an [IconMenuItem] with a native icon to the menu.
///
/// ## Platform-specific:
///
/// - **Windows / Linux**: Unsupported.
pub fn native_icon<I: Into<MenuId>, S: AsRef<str>>(
mut self,
id: I,
text: S,
icon: NativeIcon,
) -> Self {
self.items.push(
IconMenuItem::with_id_and_native_icon(
self.manager,
id,
text,
true,
Some(icon),
None::<&str>,
)
.map(|i| i.kind()),
);
self
}
/// Add Separator menu item to the menu.
pub fn separator(mut self) -> Self {
self
.items
.push(PredefinedMenuItem::separator(self.manager).map(|i| i.kind()));
self
}
/// Add Copy menu item to the menu.
pub fn copy(mut self) -> Self {
self
.items
.push(PredefinedMenuItem::copy(self.manager, None).map(|i| i.kind()));
self
}
/// Add Copy menu item with specified text to the menu.
pub fn copy_with_text<S: AsRef<str>>(mut self, text: S) -> Self {
self
.items
.push(PredefinedMenuItem::copy(self.manager, Some(text.as_ref())).map(|i| i.kind()));
self
}
/// Add Cut menu item to the menu.
pub fn cut(mut self) -> Self {
self
.items
.push(PredefinedMenuItem::cut(self.manager, None).map(|i| i.kind()));
self
}
/// Add Cut menu item with specified text to the menu.
pub fn cut_with_text<S: AsRef<str>>(mut self, text: S) -> Self {
self
.items
.push(PredefinedMenuItem::cut(self.manager, Some(text.as_ref())).map(|i| i.kind()));
self
}
/// Add Paste menu item to the menu.
pub fn paste(mut self) -> Self {
self
.items
.push(PredefinedMenuItem::paste(self.manager, None).map(|i| i.kind()));
self
}
/// Add Paste menu item with specified text to the menu.
pub fn paste_with_text<S: AsRef<str>>(mut self, text: S) -> Self {
self
.items
.push(PredefinedMenuItem::paste(self.manager, Some(text.as_ref())).map(|i| i.kind()));
self
}
/// Add SelectAll menu item to the menu.
pub fn select_all(mut self) -> Self {
self
.items
.push(PredefinedMenuItem::select_all(self.manager, None).map(|i| i.kind()));
self
}
/// Add SelectAll menu item with specified text to the menu.
pub fn select_all_with_text<S: AsRef<str>>(mut self, text: S) -> Self {
self.items.push(
PredefinedMenuItem::select_all(self.manager, Some(text.as_ref())).map(|i| i.kind()),
);
self
}
/// Add Undo menu item to the menu.
///
/// ## Platform-specific:
///
/// - **Windows / Linux:** Unsupported.
pub fn undo(mut self) -> Self {
self
.items
.push(PredefinedMenuItem::undo(self.manager, None).map(|i| i.kind()));
self
}
/// Add Undo menu item with specified text to the menu.
///
/// ## Platform-specific:
///
/// - **Windows / Linux:** Unsupported.
pub fn undo_with_text<S: AsRef<str>>(mut self, text: S) -> Self {
self
.items
.push(PredefinedMenuItem::undo(self.manager, Some(text.as_ref())).map(|i| i.kind()));
self
}
/// Add Redo menu item to the menu.
///
/// ## Platform-specific:
///
/// - **Windows / Linux:** Unsupported.
pub fn redo(mut self) -> Self {
self
.items
.push(PredefinedMenuItem::redo(self.manager, None).map(|i| i.kind()));
self
}
/// Add Redo menu item with specified text to the menu.
///
/// ## Platform-specific:
///
/// - **Windows / Linux:** Unsupported.
pub fn redo_with_text<S: AsRef<str>>(mut self, text: S) -> Self {
self
.items
.push(PredefinedMenuItem::redo(self.manager, Some(text.as_ref())).map(|i| i.kind()));
self
}
/// Add Minimize window menu item to the menu.
///
/// ## Platform-specific:
///
/// - **Linux:** Unsupported.
pub fn minimize(mut self) -> Self {
self
.items
.push(PredefinedMenuItem::minimize(self.manager, None).map(|i| i.kind()));
self
}
/// Add Minimize window menu item with specified text to the menu.
///
/// ## Platform-specific:
///
/// - **Linux:** Unsupported.
pub fn minimize_with_text<S: AsRef<str>>(mut self, text: S) -> Self {
self
.items
.push(PredefinedMenuItem::minimize(self.manager, Some(text.as_ref())).map(|i| i.kind()));
self
}
/// Add Maximize window menu item to the menu.
///
/// ## Platform-specific:
///
/// - **Linux:** Unsupported.
pub fn maximize(mut self) -> Self {
self
.items
.push(PredefinedMenuItem::maximize(self.manager, None).map(|i| i.kind()));
self
}
/// Add Maximize window menu item with specified text to the menu.
///
/// ## Platform-specific:
///
/// - **Linux:** Unsupported.
pub fn maximize_with_text<S: AsRef<str>>(mut self, text: S) -> Self {
self
.items
.push(PredefinedMenuItem::maximize(self.manager, Some(text.as_ref())).map(|i| i.kind()));
self
}
/// Add Fullscreen menu item to the menu.
///
/// ## Platform-specific:
///
/// - **Windows / Linux:** Unsupported.
pub fn fullscreen(mut self) -> Self {
self
.items
.push(PredefinedMenuItem::fullscreen(self.manager, None).map(|i| i.kind()));
self
}
/// Add Fullscreen menu item with specified text to the menu.
///
/// ## Platform-specific:
///
/// - **Windows / Linux:** Unsupported.
pub fn fullscreen_with_text<S: AsRef<str>>(mut self, text: S) -> Self {
self.items.push(
PredefinedMenuItem::fullscreen(self.manager, Some(text.as_ref())).map(|i| i.kind()),
);
self
}
/// Add Hide window menu item to the menu.
///
/// ## Platform-specific:
///
/// - **Linux:** Unsupported.
pub fn hide(mut self) -> Self {
self
.items
.push(PredefinedMenuItem::hide(self.manager, None).map(|i| i.kind()));
self
}
/// Add Hide window menu item with specified text to the menu.
///
/// ## Platform-specific:
///
/// - **Linux:** Unsupported.
pub fn hide_with_text<S: AsRef<str>>(mut self, text: S) -> Self {
self
.items
.push(PredefinedMenuItem::hide(self.manager, Some(text.as_ref())).map(|i| i.kind()));
self
}
/// Add Hide other windows menu item to the menu.
///
/// ## Platform-specific:
///
/// - **Linux:** Unsupported.
pub fn hide_others(mut self) -> Self {
self
.items
.push(PredefinedMenuItem::hide_others(self.manager, None).map(|i| i.kind()));
self
}
/// Add Hide other windows menu item with specified text to the menu.
///
/// ## Platform-specific:
///
/// - **Linux:** Unsupported.
pub fn hide_others_with_text<S: AsRef<str>>(mut self, text: S) -> Self {
self.items.push(
PredefinedMenuItem::hide_others(self.manager, Some(text.as_ref())).map(|i| i.kind()),
);
self
}
/// Add Show all app windows menu item to the menu.
///
/// ## Platform-specific:
///
/// - **Windows / Linux:** Unsupported.
pub fn show_all(mut self) -> Self {
self
.items
.push(PredefinedMenuItem::show_all(self.manager, None).map(|i| i.kind()));
self
}
/// Add Show all app windows menu item with specified text to the menu.
///
/// ## Platform-specific:
///
/// - **Windows / Linux:** Unsupported.
pub fn show_all_with_text<S: AsRef<str>>(mut self, text: S) -> Self {
self
.items
.push(PredefinedMenuItem::show_all(self.manager, Some(text.as_ref())).map(|i| i.kind()));
self
}
/// Add Close window menu item to the menu.
///
/// ## Platform-specific:
///
/// - **Linux:** Unsupported.
pub fn close_window(mut self) -> Self {
self
.items
.push(PredefinedMenuItem::close_window(self.manager, None).map(|i| i.kind()));
self
}
/// Add Close window menu item with specified text to the menu.
///
/// ## Platform-specific:
///
/// - **Linux:** Unsupported.
pub fn close_window_with_text<S: AsRef<str>>(mut self, text: S) -> Self {
self.items.push(
PredefinedMenuItem::close_window(self.manager, Some(text.as_ref())).map(|i| i.kind()),
);
self
}
/// Add Quit app menu item to the menu.
///
/// ## Platform-specific:
///
/// - **Linux:** Unsupported.
pub fn quit(mut self) -> Self {
self
.items
.push(PredefinedMenuItem::quit(self.manager, None).map(|i| i.kind()));
self
}
/// Add Quit app menu item with specified text to the menu.
///
/// ## Platform-specific:
///
/// - **Linux:** Unsupported.
pub fn quit_with_text<S: AsRef<str>>(mut self, text: S) -> Self {
self
.items
.push(PredefinedMenuItem::quit(self.manager, Some(text.as_ref())).map(|i| i.kind()));
self
}
/// Add About app menu item to the menu.
pub fn about(mut self, metadata: Option<AboutMetadata<'_>>) -> Self {
self
.items
.push(PredefinedMenuItem::about(self.manager, None, metadata).map(|i| i.kind()));
self
}
/// Add About app menu item with specified text to the menu.
pub fn about_with_text<S: AsRef<str>>(
mut self,
text: S,
metadata: Option<AboutMetadata<'_>>,
) -> Self {
self.items.push(
PredefinedMenuItem::about(self.manager, Some(text.as_ref()), metadata).map(|i| i.kind()),
);
self
}
/// Add Services menu item to the menu.
///
/// ## Platform-specific:
///
/// - **Windows / Linux:** Unsupported.
pub fn services(mut self) -> Self {
self
.items
.push(PredefinedMenuItem::services(self.manager, None).map(|i| i.kind()));
self
}
/// Add Services menu item with specified text to the menu.
///
/// ## Platform-specific:
///
/// - **Windows / Linux:** Unsupported.
pub fn services_with_text<S: AsRef<str>>(mut self, text: S) -> Self {
self
.items
.push(PredefinedMenuItem::services(self.manager, Some(text.as_ref())).map(|i| i.kind()));
self
}
/// Add Bring All to Front menu item to the menu.
///
/// ## Platform-specific:
///
/// - **Windows / Linux:** Unsupported.
pub fn bring_all_to_front(mut self) -> Self {
self
.items
.push(PredefinedMenuItem::bring_all_to_front(self.manager, None).map(|i| i.kind()));
self
}
/// Add Bring All to Front menu item with specified text to the menu.
///
/// ## Platform-specific:
///
/// - **Windows / Linux:** Unsupported.
pub fn bring_all_to_front_with_text<S: AsRef<str>>(mut self, text: S) -> Self {
self.items.push(
PredefinedMenuItem::bring_all_to_front(self.manager, Some(text.as_ref()))
.map(|i| i.kind()),
);
self
}
}
};
}
shared_menu_builder!(MenuBuilder<'m, R, M>);
shared_menu_builder!(SubmenuBuilder<'m, R, M>);

View File

@@ -0,0 +1,17 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
#![cfg(desktop)]
//! A module containing menu builder types
mod menu;
pub use menu::MenuBuilder;
pub use menu::SubmenuBuilder;
mod normal;
pub use normal::MenuItemBuilder;
mod check;
pub use check::CheckMenuItemBuilder;
mod icon;
pub use icon::IconMenuItemBuilder;

View File

@@ -0,0 +1,68 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use crate::{Manager, Runtime, menu::MenuId, menu::MenuItem};
/// A builder type for [`MenuItem`]
pub struct MenuItemBuilder {
id: Option<MenuId>,
text: String,
enabled: bool,
accelerator: Option<String>,
}
impl MenuItemBuilder {
/// Create a new menu item builder.
///
/// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic
/// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
pub fn new<S: AsRef<str>>(text: S) -> Self {
Self {
id: None,
text: text.as_ref().to_string(),
enabled: true,
accelerator: None,
}
}
/// Create a new menu item builder with the specified id.
///
/// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic
/// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
pub fn with_id<I: Into<MenuId>, S: AsRef<str>>(id: I, text: S) -> Self {
Self {
id: Some(id.into()),
text: text.as_ref().to_string(),
enabled: true,
accelerator: None,
}
}
/// Set the id for this menu item.
pub fn id<I: Into<MenuId>>(mut self, id: I) -> Self {
self.id.replace(id.into());
self
}
/// Set the enabled state for this menu item.
pub fn enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
/// Set the accelerator for this menu item.
pub fn accelerator<S: AsRef<str>>(mut self, accelerator: S) -> Self {
self.accelerator.replace(accelerator.as_ref().to_string());
self
}
/// Build the menu item
pub fn build<R: Runtime, M: Manager<R>>(self, manager: &M) -> crate::Result<MenuItem<R>> {
if let Some(id) = self.id {
MenuItem::with_id(manager, id, self.text, self.enabled, self.accelerator)
} else {
MenuItem::new(manager, self.text, self.enabled, self.accelerator)
}
}
}

View File

@@ -0,0 +1,129 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::sync::Arc;
use super::run_item_main_thread;
use crate::menu::CheckMenuItemInner;
use crate::run_main_thread;
use crate::{AppHandle, Manager, Runtime, menu::MenuId};
use super::CheckMenuItem;
impl<R: Runtime> CheckMenuItem<R> {
/// Create a new menu item.
///
/// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic
/// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
pub fn new<M, T, A>(
manager: &M,
text: T,
enabled: bool,
checked: bool,
accelerator: Option<A>,
) -> crate::Result<Self>
where
M: Manager<R>,
T: AsRef<str>,
A: AsRef<str>,
{
let handle = manager.app_handle();
let app_handle = handle.clone();
let text = text.as_ref().to_owned();
let accelerator = accelerator.and_then(|s| s.as_ref().parse().ok());
let item = run_main_thread!(handle, || {
let item = muda::CheckMenuItem::new(text, enabled, checked, accelerator);
CheckMenuItemInner::new(app_handle, item)
})?;
Ok(Self(Arc::new(item)))
}
/// Create a new menu item with the specified id.
///
/// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic
/// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
pub fn with_id<M, I, T, A>(
manager: &M,
id: I,
text: T,
enabled: bool,
checked: bool,
accelerator: Option<A>,
) -> crate::Result<Self>
where
M: Manager<R>,
I: Into<MenuId>,
T: AsRef<str>,
A: AsRef<str>,
{
let handle = manager.app_handle();
let app_handle = handle.clone();
let id = id.into();
let text = text.as_ref().to_owned();
let accelerator = accelerator.and_then(|s| s.as_ref().parse().ok());
let item = run_main_thread!(handle, move || {
let item = muda::CheckMenuItem::with_id(id, text, enabled, checked, accelerator);
CheckMenuItemInner::new(app_handle, item)
})?;
Ok(Self(Arc::new(item)))
}
/// The application handle associated with this type.
pub fn app_handle(&self) -> &AppHandle<R> {
&self.0.app_handle
}
/// Returns a unique identifier associated with this menu item.
pub fn id(&self) -> &MenuId {
self.0.inner.id()
}
/// Get the text for this menu item.
pub fn text(&self) -> crate::Result<String> {
run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().text())
}
/// Set the text for this menu item. `text` could optionally contain
/// an `&` before a character to assign this character as the mnemonic
/// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
pub fn set_text<S: AsRef<str>>(&self, text: S) -> crate::Result<()> {
let text = text.as_ref().to_string();
run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().set_text(text))
}
/// Get whether this menu item is enabled or not.
pub fn is_enabled(&self) -> crate::Result<bool> {
run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().is_enabled())
}
/// Enable or disable this menu item.
pub fn set_enabled(&self, enabled: bool) -> crate::Result<()> {
run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().set_enabled(enabled))
}
/// Set this menu item accelerator.
pub fn set_accelerator<S: AsRef<str>>(&self, accelerator: Option<S>) -> crate::Result<()> {
let accel = accelerator.and_then(|s| s.as_ref().parse().ok());
run_item_main_thread!(self, |self_: Self| {
(*self_.0).as_ref().set_accelerator(accel)
})?
.map_err(Into::into)
}
/// Get whether this check menu item is checked or not.
pub fn is_checked(&self) -> crate::Result<bool> {
run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().is_checked())
}
/// Check or Uncheck this check menu item.
pub fn set_checked(&self, checked: bool) -> crate::Result<()> {
run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().set_checked(checked))
}
}

View File

@@ -0,0 +1,220 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::sync::Arc;
use super::run_item_main_thread;
use super::{IconMenuItem, NativeIcon};
use crate::menu::IconMenuItemInner;
use crate::run_main_thread;
use crate::{AppHandle, Manager, Runtime, image::Image, menu::MenuId};
impl<R: Runtime> IconMenuItem<R> {
/// Create a new menu item.
///
/// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic
/// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
pub fn new<M, T, A>(
manager: &M,
text: T,
enabled: bool,
icon: Option<Image<'_>>,
accelerator: Option<A>,
) -> crate::Result<Self>
where
M: Manager<R>,
T: AsRef<str>,
A: AsRef<str>,
{
let handle = manager.app_handle();
let app_handle = handle.clone();
let text = text.as_ref().to_owned();
let accelerator = accelerator.and_then(|s| s.as_ref().parse().ok());
let icon = match icon {
Some(i) => Some(i.try_into()?),
None => None,
};
let item = run_main_thread!(handle, || {
let item = muda::IconMenuItem::new(text, enabled, icon, accelerator);
IconMenuItemInner::new(app_handle, item)
})?;
Ok(Self(Arc::new(item)))
}
/// Create a new menu item with the specified id.
///
/// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic
/// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
pub fn with_id<M, I, T, A>(
manager: &M,
id: I,
text: T,
enabled: bool,
icon: Option<Image<'_>>,
accelerator: Option<A>,
) -> crate::Result<Self>
where
M: Manager<R>,
I: Into<MenuId>,
T: AsRef<str>,
A: AsRef<str>,
{
let handle = manager.app_handle();
let app_handle = handle.clone();
let id = id.into();
let text = text.as_ref().to_owned();
let accelerator = accelerator.and_then(|s| s.as_ref().parse().ok());
let icon = match icon {
Some(i) => Some(i.try_into()?),
None => None,
};
let item = run_main_thread!(handle, move || {
let item = muda::IconMenuItem::with_id(id, text, enabled, icon, accelerator);
IconMenuItemInner::new(app_handle, item)
})?;
Ok(Self(Arc::new(item)))
}
/// Create a new icon menu item but with a native icon.
///
/// See [`IconMenuItem::new`] for more info.
///
/// ## Platform-specific:
///
/// - **Windows / Linux**: Unsupported.
pub fn with_native_icon<M, T, A>(
manager: &M,
text: T,
enabled: bool,
native_icon: Option<NativeIcon>,
accelerator: Option<A>,
) -> crate::Result<Self>
where
M: Manager<R>,
T: AsRef<str>,
A: AsRef<str>,
{
let handle = manager.app_handle();
let app_handle = handle.clone();
let text = text.as_ref().to_owned();
let icon = native_icon.map(Into::into);
let accelerator = accelerator.and_then(|s| s.as_ref().parse().ok());
let item = run_main_thread!(handle, || {
let item = muda::IconMenuItem::with_native_icon(text, enabled, icon, accelerator);
IconMenuItemInner::new(app_handle, item)
})?;
Ok(Self(Arc::new(item)))
}
/// Create a new icon menu item with the specified id but with a native icon.
///
/// See [`IconMenuItem::new`] for more info.
///
/// ## Platform-specific:
///
/// - **Windows / Linux**: Unsupported.
pub fn with_id_and_native_icon<M, I, T, A>(
manager: &M,
id: I,
text: T,
enabled: bool,
native_icon: Option<NativeIcon>,
accelerator: Option<A>,
) -> crate::Result<Self>
where
M: Manager<R>,
I: Into<MenuId>,
T: AsRef<str>,
A: AsRef<str>,
{
let handle = manager.app_handle();
let app_handle = handle.clone();
let id = id.into();
let text = text.as_ref().to_owned();
let icon = native_icon.map(Into::into);
let accelerator = accelerator.and_then(|s| s.as_ref().parse().ok());
let item = run_main_thread!(handle, move || {
let item = muda::IconMenuItem::with_id_and_native_icon(id, text, enabled, icon, accelerator);
IconMenuItemInner::new(app_handle, item)
})?;
Ok(Self(Arc::new(item)))
}
/// The application handle associated with this type.
pub fn app_handle(&self) -> &AppHandle<R> {
&self.0.app_handle
}
/// Returns a unique identifier associated with this menu item.
pub fn id(&self) -> &MenuId {
self.0.inner.id()
}
/// Get the text for this menu item.
pub fn text(&self) -> crate::Result<String> {
run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().text())
}
/// Set the text for this menu item. `text` could optionally contain
/// an `&` before a character to assign this character as the mnemonic
/// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
pub fn set_text<S: AsRef<str>>(&self, text: S) -> crate::Result<()> {
let text = text.as_ref().to_string();
run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().set_text(text))
}
/// Get whether this menu item is enabled or not.
pub fn is_enabled(&self) -> crate::Result<bool> {
run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().is_enabled())
}
/// Enable or disable this menu item.
pub fn set_enabled(&self, enabled: bool) -> crate::Result<()> {
run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().set_enabled(enabled))
}
/// Set this menu item accelerator.
pub fn set_accelerator<S: AsRef<str>>(&self, accelerator: Option<S>) -> crate::Result<()> {
let accel = accelerator.and_then(|s| s.as_ref().parse().ok());
run_item_main_thread!(self, |self_: Self| {
(*self_.0).as_ref().set_accelerator(accel)
})?
.map_err(Into::into)
}
/// Change this menu item icon or remove it.
pub fn set_icon(&self, icon: Option<Image<'_>>) -> crate::Result<()> {
let icon = match icon {
Some(i) => Some(i.try_into()?),
None => None,
};
run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().set_icon(icon))
}
/// Change this menu item icon to a native image or remove it.
///
/// ## Platform-specific:
///
/// - **Windows / Linux**: Unsupported.
pub fn set_native_icon(&self, _icon: Option<NativeIcon>) -> crate::Result<()> {
#[cfg(target_os = "macos")]
return run_item_main_thread!(self, |self_: Self| {
(*self_.0).as_ref().set_native_icon(_icon.map(Into::into))
});
#[allow(unreachable_code)]
Ok(())
}
}

View File

@@ -0,0 +1,400 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::sync::Arc;
use super::run_item_main_thread;
use super::sealed::ContextMenuBase;
use super::{
AboutMetadata, IsMenuItem, Menu, MenuInner, MenuItemKind, PredefinedMenuItem, Submenu,
};
use crate::Window;
use crate::run_main_thread;
use crate::{AppHandle, Manager, Position, Runtime};
use muda::ContextMenu;
use muda::MenuId;
/// Expected submenu id of the Window menu for macOS.
pub const WINDOW_SUBMENU_ID: &str = "__tauri_window_menu__";
/// Expected submenu id of the Help menu for macOS.
pub const HELP_SUBMENU_ID: &str = "__tauri_help_menu__";
impl<R: Runtime> super::ContextMenu for Menu<R> {
#[cfg(target_os = "windows")]
fn hpopupmenu(&self) -> crate::Result<isize> {
run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().hpopupmenu())
}
fn popup<T: Runtime>(&self, window: Window<T>) -> crate::Result<()> {
self.popup_inner(window, None::<Position>)
}
fn popup_at<T: Runtime, P: Into<Position>>(
&self,
window: Window<T>,
position: P,
) -> crate::Result<()> {
self.popup_inner(window, Some(position))
}
}
impl<R: Runtime> ContextMenuBase for Menu<R> {
fn popup_inner<T: Runtime, P: Into<crate::Position>>(
&self,
window: crate::Window<T>,
position: Option<P>,
) -> crate::Result<()> {
let position = position.map(Into::into);
run_item_main_thread!(self, move |self_: Self| {
#[cfg(target_os = "macos")]
if let Ok(view) = window.ns_view() {
unsafe {
self_
.inner()
.show_context_menu_for_nsview(view as _, position);
}
}
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
if let Ok(w) = window.gtk_window() {
self_
.inner()
.show_context_menu_for_gtk_window(w.as_ref(), position);
}
#[cfg(windows)]
if let Ok(hwnd) = window.hwnd() {
unsafe {
self_
.inner()
.show_context_menu_for_hwnd(hwnd.0 as _, position);
}
}
})
}
fn inner_context(&self) -> &dyn muda::ContextMenu {
(*self.0).as_ref()
}
fn inner_context_owned(&self) -> Box<dyn muda::ContextMenu> {
Box::new((*self.0).as_ref().clone())
}
}
impl<R: Runtime> Menu<R> {
/// Creates a new menu.
pub fn new<M: Manager<R>>(manager: &M) -> crate::Result<Self> {
let handle = manager.app_handle();
let app_handle = handle.clone();
let menu = run_main_thread!(handle, || {
let menu = muda::Menu::new();
MenuInner::new(app_handle, menu)
})?;
Ok(Self(Arc::new(menu)))
}
/// Creates a new menu with the specified id.
pub fn with_id<M: Manager<R>, I: Into<MenuId>>(manager: &M, id: I) -> crate::Result<Self> {
let handle = manager.app_handle();
let app_handle = handle.clone();
let id = id.into();
let menu = run_main_thread!(handle, move || {
let menu = muda::Menu::with_id(id);
MenuInner::new(app_handle, menu)
})?;
Ok(Self(Arc::new(menu)))
}
/// Creates a new menu with given `items`. It calls [`Menu::new`] and [`Menu::append_items`] internally.
pub fn with_items<M: Manager<R>>(
manager: &M,
items: &[&dyn IsMenuItem<R>],
) -> crate::Result<Self> {
let menu = Self::new(manager)?;
menu.append_items(items)?;
Ok(menu)
}
/// Creates a new menu with the specified id and given `items`.
/// It calls [`Menu::new`] and [`Menu::append_items`] internally.
pub fn with_id_and_items<M: Manager<R>, I: Into<MenuId>>(
manager: &M,
id: I,
items: &[&dyn IsMenuItem<R>],
) -> crate::Result<Self> {
let menu = Self::with_id(manager, id)?;
menu.append_items(items)?;
Ok(menu)
}
/// Creates a menu filled with default menu items and submenus.
pub fn default(app_handle: &AppHandle<R>) -> crate::Result<Self> {
let pkg_info = app_handle.package_info();
let config = app_handle.config();
let about_metadata = AboutMetadata {
name: Some(pkg_info.name.clone()),
version: Some(pkg_info.version.to_string()),
copyright: config.bundle.copyright.clone(),
authors: config.bundle.publisher.clone().map(|p| vec![p]),
..Default::default()
};
let window_menu = Submenu::with_id_and_items(
app_handle,
WINDOW_SUBMENU_ID,
"Window",
true,
&[
&PredefinedMenuItem::minimize(app_handle, None)?,
&PredefinedMenuItem::maximize(app_handle, None)?,
#[cfg(target_os = "macos")]
&PredefinedMenuItem::separator(app_handle)?,
&PredefinedMenuItem::close_window(app_handle, None)?,
],
)?;
let help_menu = Submenu::with_id_and_items(
app_handle,
HELP_SUBMENU_ID,
"Help",
true,
&[
#[cfg(not(target_os = "macos"))]
&PredefinedMenuItem::about(app_handle, None, Some(about_metadata))?,
],
)?;
let menu = Menu::with_items(
app_handle,
&[
#[cfg(target_os = "macos")]
&Submenu::with_items(
app_handle,
pkg_info.name.clone(),
true,
&[
&PredefinedMenuItem::about(app_handle, None, Some(about_metadata))?,
&PredefinedMenuItem::separator(app_handle)?,
&PredefinedMenuItem::services(app_handle, None)?,
&PredefinedMenuItem::separator(app_handle)?,
&PredefinedMenuItem::hide(app_handle, None)?,
&PredefinedMenuItem::hide_others(app_handle, None)?,
&PredefinedMenuItem::separator(app_handle)?,
&PredefinedMenuItem::quit(app_handle, None)?,
],
)?,
#[cfg(not(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
)))]
&Submenu::with_items(
app_handle,
"File",
true,
&[
&PredefinedMenuItem::close_window(app_handle, None)?,
#[cfg(not(target_os = "macos"))]
&PredefinedMenuItem::quit(app_handle, None)?,
],
)?,
&Submenu::with_items(
app_handle,
"Edit",
true,
&[
&PredefinedMenuItem::undo(app_handle, None)?,
&PredefinedMenuItem::redo(app_handle, None)?,
&PredefinedMenuItem::separator(app_handle)?,
&PredefinedMenuItem::cut(app_handle, None)?,
&PredefinedMenuItem::copy(app_handle, None)?,
&PredefinedMenuItem::paste(app_handle, None)?,
&PredefinedMenuItem::select_all(app_handle, None)?,
],
)?,
#[cfg(target_os = "macos")]
&Submenu::with_items(
app_handle,
"View",
true,
&[&PredefinedMenuItem::fullscreen(app_handle, None)?],
)?,
&window_menu,
&help_menu,
],
)?;
Ok(menu)
}
pub(crate) fn inner(&self) -> &muda::Menu {
(*self.0).as_ref()
}
/// The application handle associated with this type.
pub fn app_handle(&self) -> &AppHandle<R> {
&self.0.app_handle
}
/// Returns a unique identifier associated with this menu.
pub fn id(&self) -> &MenuId {
self.0.inner.id()
}
/// Add a menu item to the end of this menu.
///
/// ## Platform-specific:
///
/// - **macOS:** Only [`Submenu`] can be added to the menu.
///
/// [`Submenu`]: super::Submenu
pub fn append(&self, item: &dyn IsMenuItem<R>) -> crate::Result<()> {
let kind = item.kind();
run_item_main_thread!(self, |self_: Self| {
(*self_.0).as_ref().append(kind.inner().inner_muda())
})?
.map_err(Into::into)
}
/// Add menu items to the end of this menu. It calls [`Menu::append`] in a loop internally.
///
/// ## Platform-specific:
///
/// - **macOS:** Only [`Submenu`] can be added to the menu
///
/// [`Submenu`]: super::Submenu
pub fn append_items(&self, items: &[&dyn IsMenuItem<R>]) -> crate::Result<()> {
for item in items {
self.append(*item)?
}
Ok(())
}
/// Add a menu item to the beginning of this menu.
///
/// ## Platform-specific:
///
/// - **macOS:** Only [`Submenu`] can be added to the menu
///
/// [`Submenu`]: super::Submenu
pub fn prepend(&self, item: &dyn IsMenuItem<R>) -> crate::Result<()> {
let kind = item.kind();
run_item_main_thread!(self, |self_: Self| {
(*self_.0).as_ref().prepend(kind.inner().inner_muda())
})?
.map_err(Into::into)
}
/// Add menu items to the beginning of this menu. It calls [`Menu::insert_items`] with position of `0` internally.
///
/// ## Platform-specific:
///
/// - **macOS:** Only [`Submenu`] can be added to the menu
///
/// [`Submenu`]: super::Submenu
pub fn prepend_items(&self, items: &[&dyn IsMenuItem<R>]) -> crate::Result<()> {
self.insert_items(items, 0)
}
/// Insert a menu item at the specified `position` in the menu.
///
/// ## Platform-specific:
///
/// - **macOS:** Only [`Submenu`] can be added to the menu
///
/// [`Submenu`]: super::Submenu
pub fn insert(&self, item: &dyn IsMenuItem<R>, position: usize) -> crate::Result<()> {
let kind = item.kind();
run_item_main_thread!(self, |self_: Self| (*self_.0)
.as_ref()
.insert(kind.inner().inner_muda(), position))?
.map_err(Into::into)
}
/// Insert menu items at the specified `position` in the menu.
///
/// ## Platform-specific:
///
/// - **macOS:** Only [`Submenu`] can be added to the menu
///
/// [`Submenu`]: super::Submenu
pub fn insert_items(&self, items: &[&dyn IsMenuItem<R>], position: usize) -> crate::Result<()> {
for (i, item) in items.iter().enumerate() {
self.insert(*item, position + i)?
}
Ok(())
}
/// Remove a menu item from this menu.
pub fn remove(&self, item: &dyn IsMenuItem<R>) -> crate::Result<()> {
let kind = item.kind();
run_item_main_thread!(self, |self_: Self| {
(*self_.0).as_ref().remove(kind.inner().inner_muda())
})?
.map_err(Into::into)
}
/// Remove the menu item at the specified position from this menu and returns it.
pub fn remove_at(&self, position: usize) -> crate::Result<Option<MenuItemKind<R>>> {
run_item_main_thread!(self, |self_: Self| {
(*self_.0)
.as_ref()
.remove_at(position)
.map(|i| MenuItemKind::from_muda(self_.0.app_handle.clone(), i))
})
}
/// Retrieves the menu item matching the given identifier.
pub fn get<'a, I>(&self, id: &'a I) -> Option<MenuItemKind<R>>
where
I: ?Sized,
MenuId: PartialEq<&'a I>,
{
self
.items()
.unwrap_or_default()
.into_iter()
.find(|i| i.id() == &id)
}
/// Returns a list of menu items that has been added to this menu.
pub fn items(&self) -> crate::Result<Vec<MenuItemKind<R>>> {
run_item_main_thread!(self, |self_: Self| {
(*self_.0)
.as_ref()
.items()
.into_iter()
.map(|i| MenuItemKind::from_muda(self_.0.app_handle.clone(), i))
.collect::<Vec<_>>()
})
}
/// Set this menu as the application menu.
///
/// This is an alias for [`AppHandle::set_menu`].
pub fn set_as_app_menu(&self) -> crate::Result<Option<Menu<R>>> {
self.0.app_handle.set_menu(self.clone())
}
/// Set this menu as the window menu.
///
/// This is an alias for [`Window::set_menu`].
pub fn set_as_window_menu(&self, window: &Window<R>) -> crate::Result<Option<Menu<R>>> {
window.set_menu(self.clone())
}
}

View File

@@ -0,0 +1,771 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
//! Menu types and utilities.
mod builders;
mod check;
mod icon;
#[allow(clippy::module_inception)]
mod menu;
mod normal;
pub(crate) mod plugin;
mod predefined;
mod submenu;
use std::{mem::ManuallyDrop, sync::Arc};
pub use builders::*;
pub use menu::{HELP_SUBMENU_ID, WINDOW_SUBMENU_ID};
use serde::{Deserialize, Serialize};
use crate::menu::plugin::remove_menu_channel;
use crate::{AppHandle, Runtime, image::Image};
pub use muda::MenuId;
macro_rules! run_item_main_thread {
($self:ident, $ex:expr) => {{
use std::sync::mpsc::channel;
let (tx, rx) = channel();
let self_ = $self.clone();
let task = move || {
let f = $ex;
let _ = tx.send(f(self_));
};
$self
.app_handle()
.run_on_main_thread(task)
.and_then(|_| rx.recv().map_err(|_| crate::Error::FailedToReceiveMessage))
}};
}
pub(crate) use run_item_main_thread;
/// Describes a menu event emitted when a menu item is activated
#[derive(Debug, Clone, Serialize)]
pub struct MenuEvent {
/// Id of the menu item which triggered this event
pub id: MenuId,
}
impl MenuEvent {
/// Returns the id of the menu item which triggered this event
pub fn id(&self) -> &MenuId {
&self.id
}
}
impl From<muda::MenuEvent> for MenuEvent {
fn from(value: muda::MenuEvent) -> Self {
Self { id: value.id }
}
}
macro_rules! gen_wrappers {
(
$(
$(#[$attr:meta])*
$type:ident($inner:ident$(, $kind:ident)?)
),*
) => {
$(
#[tauri_macros::default_runtime(crate::Wry, wry)]
pub(crate) struct $inner<R: $crate::Runtime> {
// This [`ManuallyDrop`] is used to [`ManuallyDrop::take`] in [`Self::drop`] to drop it on main thread
inner: ManuallyDrop<::muda::$type>,
app_handle: $crate::AppHandle<R>,
}
impl<R: $crate::Runtime> $inner<R> {
fn new(app_handle: $crate::AppHandle<R>, menu: ::muda::$type) -> Self {
Self {
inner: ManuallyDrop::new(menu),
app_handle,
}
}
}
/// # Safety
///
/// We make sure it always runs on the main thread.
unsafe impl<R: $crate::Runtime> Sync for $inner<R> {}
unsafe impl<R: $crate::Runtime> Send for $inner<R> {}
impl<R: Runtime> $crate::Resource for $type<R> {}
impl<R: Runtime> Drop for $inner<R> {
fn drop(&mut self) {
remove_menu_channel(&self.app_handle, self.inner.id());
// SAFETY: we will not access `self.inner` after this
let inner = unsafe { ManuallyDrop::take(&mut self.inner) };
// SAFETY: inner was created on main thread and is being dropped on main thread
let inner = $crate::UnsafeSend(inner);
let _ = self.app_handle.run_on_main_thread(move || {
drop(inner.take());
});
}
}
impl<R: Runtime> AsRef<::muda::$type> for $inner<R> {
fn as_ref(&self) -> &::muda::$type {
&self.inner
}
}
$(#[$attr])*
pub struct $type<R: $crate::Runtime>(::std::sync::Arc<$inner<R>>);
impl<R: $crate::Runtime> Clone for $type<R> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
$(
impl<R: $crate::Runtime> $crate::menu::sealed::IsMenuItemBase for $type<R> {
fn inner_muda(&self) -> &dyn muda::IsMenuItem {
(*self.0).as_ref()
}
}
impl<R: $crate::Runtime> $crate::menu::IsMenuItem<R> for $type<R> {
fn kind(&self) -> MenuItemKind<R> {
MenuItemKind::$kind(self.clone())
}
fn id(&self) -> &MenuId {
self.0.inner.id()
}
}
)*
)*
};
}
gen_wrappers!(
/// A type that is either a menu bar on the window
/// on Windows and Linux or as a global menu in the menubar on macOS.
///
/// ## Platform-specific:
///
/// - **macOS**: if using [`Menu`] for the global menubar, it can only contain [`Submenu`]s
Menu(MenuInner),
/// A menu item inside a [`Menu`] or [`Submenu`] and contains only text.
MenuItem(MenuItemInner, MenuItem),
/// A type that is a submenu inside a [`Menu`] or [`Submenu`]
Submenu(SubmenuInner, Submenu),
/// A predefined (native) menu item which has a predefined behavior by the OS or by this crate.
PredefinedMenuItem(PredefinedMenuItemInner, Predefined),
/// A menu item inside a [`Menu`] or [`Submenu`]
/// and usually contains a text and a check mark or a similar toggle
/// that corresponds to a checked and unchecked states.
CheckMenuItem(CheckMenuItemInner, Check),
/// A menu item inside a [`Menu`] or [`Submenu`]
/// and usually contains an icon and a text.
IconMenuItem(IconMenuItemInner, Icon)
);
/// Application metadata for the [`PredefinedMenuItem::about`].
#[derive(Debug, Clone, Default)]
pub struct AboutMetadata<'a> {
/// Sets the application name.
pub name: Option<String>,
/// The application version.
pub version: Option<String>,
/// The short version, e.g. "1.0".
///
/// ## Platform-specific
///
/// - **Windows / Linux:** Appended to the end of `version` in parentheses.
pub short_version: Option<String>,
/// The authors of the application.
///
/// ## Platform-specific
///
/// - **macOS:** Unsupported.
pub authors: Option<Vec<String>>,
/// Application comments.
///
/// ## Platform-specific
///
/// - **macOS:** Unsupported.
pub comments: Option<String>,
/// The copyright of the application.
pub copyright: Option<String>,
/// The license of the application.
///
/// ## Platform-specific
///
/// - **macOS:** Unsupported.
pub license: Option<String>,
/// The application website.
///
/// ## Platform-specific
///
/// - **macOS:** Unsupported.
pub website: Option<String>,
/// The website label.
///
/// ## Platform-specific
///
/// - **macOS:** Unsupported.
pub website_label: Option<String>,
/// The credits.
///
/// ## Platform-specific
///
/// - **Windows / Linux:** Unsupported.
pub credits: Option<String>,
/// The application icon.
///
/// ## Platform-specific
///
/// - **Windows:** Unsupported.
pub icon: Option<Image<'a>>,
}
/// A builder type for [`AboutMetadata`].
#[derive(Clone, Debug, Default)]
pub struct AboutMetadataBuilder<'a>(AboutMetadata<'a>);
impl<'a> AboutMetadataBuilder<'a> {
/// Create a new about metadata builder.
pub fn new() -> Self {
Default::default()
}
/// Sets the application name.
pub fn name<S: Into<String>>(mut self, name: Option<S>) -> Self {
self.0.name = name.map(|s| s.into());
self
}
/// Sets the application version.
pub fn version<S: Into<String>>(mut self, version: Option<S>) -> Self {
self.0.version = version.map(|s| s.into());
self
}
/// Sets the short version, e.g. "1.0".
///
/// ## Platform-specific
///
/// - **Windows / Linux:** Appended to the end of `version` in parentheses.
pub fn short_version<S: Into<String>>(mut self, short_version: Option<S>) -> Self {
self.0.short_version = short_version.map(|s| s.into());
self
}
/// Sets the authors of the application.
///
/// ## Platform-specific
///
/// - **macOS:** Unsupported.
pub fn authors(mut self, authors: Option<Vec<String>>) -> Self {
self.0.authors = authors;
self
}
/// Application comments.
///
/// ## Platform-specific
///
/// - **macOS:** Unsupported.
pub fn comments<S: Into<String>>(mut self, comments: Option<S>) -> Self {
self.0.comments = comments.map(|s| s.into());
self
}
/// Sets the copyright of the application.
pub fn copyright<S: Into<String>>(mut self, copyright: Option<S>) -> Self {
self.0.copyright = copyright.map(|s| s.into());
self
}
/// Sets the license of the application.
///
/// ## Platform-specific
///
/// - **macOS:** Unsupported.
pub fn license<S: Into<String>>(mut self, license: Option<S>) -> Self {
self.0.license = license.map(|s| s.into());
self
}
/// Sets the application website.
///
/// ## Platform-specific
///
/// - **macOS:** Unsupported.
pub fn website<S: Into<String>>(mut self, website: Option<S>) -> Self {
self.0.website = website.map(|s| s.into());
self
}
/// Sets the website label.
///
/// ## Platform-specific
///
/// - **macOS:** Unsupported.
pub fn website_label<S: Into<String>>(mut self, website_label: Option<S>) -> Self {
self.0.website_label = website_label.map(|s| s.into());
self
}
/// Sets the credits.
///
/// ## Platform-specific
///
/// - **Windows / Linux:** Unsupported.
pub fn credits<S: Into<String>>(mut self, credits: Option<S>) -> Self {
self.0.credits = credits.map(|s| s.into());
self
}
/// Sets the application icon.
///
/// ## Platform-specific
///
/// - **Windows:** Unsupported.
pub fn icon(mut self, icon: Option<Image<'a>>) -> Self {
self.0.icon = icon;
self
}
/// Construct the final [`AboutMetadata`]
pub fn build(self) -> AboutMetadata<'a> {
self.0
}
}
impl TryFrom<AboutMetadata<'_>> for muda::AboutMetadata {
type Error = crate::Error;
fn try_from(value: AboutMetadata<'_>) -> Result<Self, Self::Error> {
let icon = match value.icon {
Some(i) => Some(i.try_into()?),
None => None,
};
Ok(Self {
authors: value.authors,
name: value.name,
version: value.version,
short_version: value.short_version,
comments: value.comments,
copyright: value.copyright,
license: value.license,
website: value.website,
website_label: value.website_label,
credits: value.credits,
icon,
})
}
}
/// A native Icon to be used for the menu item
///
/// ## Platform-specific:
///
/// - **Windows / Linux**: Unsupported.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
pub enum NativeIcon {
/// An add item template image.
Add,
/// Advanced preferences toolbar icon for the preferences window.
Advanced,
/// A Bluetooth template image.
Bluetooth,
/// Bookmarks image suitable for a template.
Bookmarks,
/// A caution image.
Caution,
/// A color panel toolbar icon.
ColorPanel,
/// A column view mode template image.
ColumnView,
/// A computer icon.
Computer,
/// An enter full-screen mode template image.
EnterFullScreen,
/// Permissions for all users.
Everyone,
/// An exit full-screen mode template image.
ExitFullScreen,
/// A cover flow view mode template image.
FlowView,
/// A folder image.
Folder,
/// A burnable folder icon.
FolderBurnable,
/// A smart folder icon.
FolderSmart,
/// A link template image.
FollowLinkFreestanding,
/// A font panel toolbar icon.
FontPanel,
/// A `go back` template image.
GoLeft,
/// A `go forward` template image.
GoRight,
/// Home image suitable for a template.
Home,
/// An iChat Theater template image.
IChatTheater,
/// An icon view mode template image.
IconView,
/// An information toolbar icon.
Info,
/// A template image used to denote invalid data.
InvalidDataFreestanding,
/// A generic left-facing triangle template image.
LeftFacingTriangle,
/// A list view mode template image.
ListView,
/// A locked padlock template image.
LockLocked,
/// An unlocked padlock template image.
LockUnlocked,
/// A horizontal dash, for use in menus.
MenuMixedState,
/// A check mark template image, for use in menus.
MenuOnState,
/// A MobileMe icon.
MobileMe,
/// A drag image for multiple items.
MultipleDocuments,
/// A network icon.
Network,
/// A path button template image.
Path,
/// General preferences toolbar icon for the preferences window.
PreferencesGeneral,
/// A Quick Look template image.
QuickLook,
/// A refresh template image.
RefreshFreestanding,
/// A refresh template image.
Refresh,
/// A remove item template image.
Remove,
/// A reveal contents template image.
RevealFreestanding,
/// A generic right-facing triangle template image.
RightFacingTriangle,
/// A share view template image.
Share,
/// A slideshow template image.
Slideshow,
/// A badge for a `smart` item.
SmartBadge,
/// Small green indicator, similar to iChat's available image.
StatusAvailable,
/// Small clear indicator.
StatusNone,
/// Small yellow indicator, similar to iChat's idle image.
StatusPartiallyAvailable,
/// Small red indicator, similar to iChat's unavailable image.
StatusUnavailable,
/// A stop progress template image.
StopProgressFreestanding,
/// A stop progress button template image.
StopProgress,
/// An image of the empty trash can.
TrashEmpty,
/// An image of the full trash can.
TrashFull,
/// Permissions for a single user.
User,
/// User account toolbar icon for the preferences window.
UserAccounts,
/// Permissions for a group of users.
UserGroup,
/// Permissions for guests.
UserGuest,
}
impl From<NativeIcon> for muda::NativeIcon {
fn from(value: NativeIcon) -> Self {
match value {
NativeIcon::Add => muda::NativeIcon::Add,
NativeIcon::Advanced => muda::NativeIcon::Advanced,
NativeIcon::Bluetooth => muda::NativeIcon::Bluetooth,
NativeIcon::Bookmarks => muda::NativeIcon::Bookmarks,
NativeIcon::Caution => muda::NativeIcon::Caution,
NativeIcon::ColorPanel => muda::NativeIcon::ColorPanel,
NativeIcon::ColumnView => muda::NativeIcon::ColumnView,
NativeIcon::Computer => muda::NativeIcon::Computer,
NativeIcon::EnterFullScreen => muda::NativeIcon::EnterFullScreen,
NativeIcon::Everyone => muda::NativeIcon::Everyone,
NativeIcon::ExitFullScreen => muda::NativeIcon::ExitFullScreen,
NativeIcon::FlowView => muda::NativeIcon::FlowView,
NativeIcon::Folder => muda::NativeIcon::Folder,
NativeIcon::FolderBurnable => muda::NativeIcon::FolderBurnable,
NativeIcon::FolderSmart => muda::NativeIcon::FolderSmart,
NativeIcon::FollowLinkFreestanding => muda::NativeIcon::FollowLinkFreestanding,
NativeIcon::FontPanel => muda::NativeIcon::FontPanel,
NativeIcon::GoLeft => muda::NativeIcon::GoLeft,
NativeIcon::GoRight => muda::NativeIcon::GoRight,
NativeIcon::Home => muda::NativeIcon::Home,
NativeIcon::IChatTheater => muda::NativeIcon::IChatTheater,
NativeIcon::IconView => muda::NativeIcon::IconView,
NativeIcon::Info => muda::NativeIcon::Info,
NativeIcon::InvalidDataFreestanding => muda::NativeIcon::InvalidDataFreestanding,
NativeIcon::LeftFacingTriangle => muda::NativeIcon::LeftFacingTriangle,
NativeIcon::ListView => muda::NativeIcon::ListView,
NativeIcon::LockLocked => muda::NativeIcon::LockLocked,
NativeIcon::LockUnlocked => muda::NativeIcon::LockUnlocked,
NativeIcon::MenuMixedState => muda::NativeIcon::MenuMixedState,
NativeIcon::MenuOnState => muda::NativeIcon::MenuOnState,
NativeIcon::MobileMe => muda::NativeIcon::MobileMe,
NativeIcon::MultipleDocuments => muda::NativeIcon::MultipleDocuments,
NativeIcon::Network => muda::NativeIcon::Network,
NativeIcon::Path => muda::NativeIcon::Path,
NativeIcon::PreferencesGeneral => muda::NativeIcon::PreferencesGeneral,
NativeIcon::QuickLook => muda::NativeIcon::QuickLook,
NativeIcon::RefreshFreestanding => muda::NativeIcon::RefreshFreestanding,
NativeIcon::Refresh => muda::NativeIcon::Refresh,
NativeIcon::Remove => muda::NativeIcon::Remove,
NativeIcon::RevealFreestanding => muda::NativeIcon::RevealFreestanding,
NativeIcon::RightFacingTriangle => muda::NativeIcon::RightFacingTriangle,
NativeIcon::Share => muda::NativeIcon::Share,
NativeIcon::Slideshow => muda::NativeIcon::Slideshow,
NativeIcon::SmartBadge => muda::NativeIcon::SmartBadge,
NativeIcon::StatusAvailable => muda::NativeIcon::StatusAvailable,
NativeIcon::StatusNone => muda::NativeIcon::StatusNone,
NativeIcon::StatusPartiallyAvailable => muda::NativeIcon::StatusPartiallyAvailable,
NativeIcon::StatusUnavailable => muda::NativeIcon::StatusUnavailable,
NativeIcon::StopProgressFreestanding => muda::NativeIcon::StopProgressFreestanding,
NativeIcon::StopProgress => muda::NativeIcon::StopProgress,
NativeIcon::TrashEmpty => muda::NativeIcon::TrashEmpty,
NativeIcon::TrashFull => muda::NativeIcon::TrashFull,
NativeIcon::User => muda::NativeIcon::User,
NativeIcon::UserAccounts => muda::NativeIcon::UserAccounts,
NativeIcon::UserGroup => muda::NativeIcon::UserGroup,
NativeIcon::UserGuest => muda::NativeIcon::UserGuest,
}
}
}
/// An enumeration of all menu item kinds that could be added to
/// a [`Menu`] or [`Submenu`]
pub enum MenuItemKind<R: Runtime> {
/// Normal menu item
MenuItem(MenuItem<R>),
/// Submenu menu item
Submenu(Submenu<R>),
/// Predefined menu item
Predefined(PredefinedMenuItem<R>),
/// Check menu item
Check(CheckMenuItem<R>),
/// Icon menu item
Icon(IconMenuItem<R>),
}
impl<R: Runtime> MenuItemKind<R> {
/// Returns a unique identifier associated with this menu item.
pub fn id(&self) -> &MenuId {
match self {
MenuItemKind::MenuItem(i) => i.id(),
MenuItemKind::Submenu(i) => i.id(),
MenuItemKind::Predefined(i) => i.id(),
MenuItemKind::Check(i) => i.id(),
MenuItemKind::Icon(i) => i.id(),
}
}
pub(crate) fn inner(&self) -> &dyn IsMenuItem<R> {
match self {
MenuItemKind::MenuItem(i) => i,
MenuItemKind::Submenu(i) => i,
MenuItemKind::Predefined(i) => i,
MenuItemKind::Check(i) => i,
MenuItemKind::Icon(i) => i,
}
}
pub(crate) fn from_muda(app_handle: AppHandle<R>, i: muda::MenuItemKind) -> Self {
match i {
muda::MenuItemKind::MenuItem(i) => {
Self::MenuItem(MenuItem(Arc::new(MenuItemInner::new(app_handle, i))))
}
muda::MenuItemKind::Submenu(i) => {
Self::Submenu(Submenu(Arc::new(SubmenuInner::new(app_handle, i))))
}
muda::MenuItemKind::Predefined(i) => Self::Predefined(PredefinedMenuItem(Arc::new(
PredefinedMenuItemInner::new(app_handle, i),
))),
muda::MenuItemKind::Check(i) => Self::Check(CheckMenuItem(Arc::new(
CheckMenuItemInner::new(app_handle, i),
))),
muda::MenuItemKind::Icon(i) => Self::Icon(IconMenuItem(Arc::new(IconMenuItemInner::new(
app_handle, i,
)))),
}
}
/// Casts this item to a [`MenuItem`], and returns `None` if it wasn't.
pub fn as_menuitem(&self) -> Option<&MenuItem<R>> {
match self {
MenuItemKind::MenuItem(i) => Some(i),
_ => None,
}
}
/// Casts this item to a [`MenuItem`], and panics if it wasn't.
pub fn as_menuitem_unchecked(&self) -> &MenuItem<R> {
match self {
MenuItemKind::MenuItem(i) => i,
_ => panic!("Not a MenuItem"),
}
}
/// Casts this item to a [`Submenu`], and returns `None` if it wasn't.
pub fn as_submenu(&self) -> Option<&Submenu<R>> {
match self {
MenuItemKind::Submenu(i) => Some(i),
_ => None,
}
}
/// Casts this item to a [`Submenu`], and panics if it wasn't.
pub fn as_submenu_unchecked(&self) -> &Submenu<R> {
match self {
MenuItemKind::Submenu(i) => i,
_ => panic!("Not a Submenu"),
}
}
/// Casts this item to a [`PredefinedMenuItem`], and returns `None` if it wasn't.
pub fn as_predefined_menuitem(&self) -> Option<&PredefinedMenuItem<R>> {
match self {
MenuItemKind::Predefined(i) => Some(i),
_ => None,
}
}
/// Casts this item to a [`PredefinedMenuItem`], and panics if it wasn't.
pub fn as_predefined_menuitem_unchecked(&self) -> &PredefinedMenuItem<R> {
match self {
MenuItemKind::Predefined(i) => i,
_ => panic!("Not a PredefinedMenuItem"),
}
}
/// Casts this item to a [`CheckMenuItem`], and returns `None` if it wasn't.
pub fn as_check_menuitem(&self) -> Option<&CheckMenuItem<R>> {
match self {
MenuItemKind::Check(i) => Some(i),
_ => None,
}
}
/// Casts this item to a [`CheckMenuItem`], and panics if it wasn't.
pub fn as_check_menuitem_unchecked(&self) -> &CheckMenuItem<R> {
match self {
MenuItemKind::Check(i) => i,
_ => panic!("Not a CheckMenuItem"),
}
}
/// Casts this item to a [`IconMenuItem`], and returns `None` if it wasn't.
pub fn as_icon_menuitem(&self) -> Option<&IconMenuItem<R>> {
match self {
MenuItemKind::Icon(i) => Some(i),
_ => None,
}
}
/// Casts this item to a [`IconMenuItem`], and panics if it wasn't.
pub fn as_icon_menuitem_unchecked(&self) -> &IconMenuItem<R> {
match self {
MenuItemKind::Icon(i) => i,
_ => panic!("Not an IconMenuItem"),
}
}
}
impl<R: Runtime> Clone for MenuItemKind<R> {
fn clone(&self) -> Self {
match self {
Self::MenuItem(i) => Self::MenuItem(i.clone()),
Self::Submenu(i) => Self::Submenu(i.clone()),
Self::Predefined(i) => Self::Predefined(i.clone()),
Self::Check(i) => Self::Check(i.clone()),
Self::Icon(i) => Self::Icon(i.clone()),
}
}
}
impl<R: Runtime> sealed::IsMenuItemBase for MenuItemKind<R> {
fn inner_muda(&self) -> &dyn muda::IsMenuItem {
self.inner().inner_muda()
}
}
impl<R: Runtime> IsMenuItem<R> for MenuItemKind<R> {
fn kind(&self) -> MenuItemKind<R> {
self.clone()
}
fn id(&self) -> &MenuId {
self.id()
}
}
/// A trait that defines a generic item in a menu, which may be one of [`MenuItemKind`]
///
/// # Safety
///
/// This trait is ONLY meant to be implemented internally by the crate.
pub trait IsMenuItem<R: Runtime>: sealed::IsMenuItemBase {
/// Returns the kind of this menu item.
fn kind(&self) -> MenuItemKind<R>;
/// Returns a unique identifier associated with this menu.
fn id(&self) -> &MenuId;
}
/// A helper trait with methods to help creating a context menu.
///
/// # Safety
///
/// This trait is ONLY meant to be implemented internally by the crate.
pub trait ContextMenu: sealed::ContextMenuBase + Send + Sync {
/// Get the popup [`HMENU`] for this menu.
///
/// The returned [`HMENU`] is valid as long as the [`ContextMenu`] is.
///
/// [`HMENU`]: https://learn.microsoft.com/en-us/windows/win32/winprog/windows-data-types#HMENU
#[cfg(windows)]
#[cfg_attr(docsrs, doc(cfg(windows)))]
fn hpopupmenu(&self) -> crate::Result<isize>;
/// Popup this menu as a context menu on the specified window at the cursor position.
fn popup<R: crate::Runtime>(&self, window: crate::Window<R>) -> crate::Result<()>;
/// Popup this menu as a context menu on the specified window at the specified position.
///
/// The position is relative to the window's top-left corner.
fn popup_at<R: crate::Runtime, P: Into<crate::Position>>(
&self,
window: crate::Window<R>,
position: P,
) -> crate::Result<()>;
}
pub(crate) mod sealed {
pub trait IsMenuItemBase {
fn inner_muda(&self) -> &dyn muda::IsMenuItem;
}
pub trait ContextMenuBase {
fn inner_context(&self) -> &dyn muda::ContextMenu;
fn inner_context_owned(&self) -> Box<dyn muda::ContextMenu>;
fn popup_inner<R: crate::Runtime, P: Into<crate::Position>>(
&self,
window: crate::Window<R>,
position: Option<P>,
) -> crate::Result<()>;
}
}
#[cfg(windows)]
pub(crate) fn map_to_menu_theme(theme: tauri_utils::Theme) -> muda::MenuTheme {
match theme {
tauri_utils::Theme::Light => muda::MenuTheme::Light,
tauri_utils::Theme::Dark => muda::MenuTheme::Dark,
_ => muda::MenuTheme::Auto,
}
}

View File

@@ -0,0 +1,117 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::sync::Arc;
use super::run_item_main_thread;
use crate::menu::MenuItemInner;
use crate::run_main_thread;
use crate::{AppHandle, Manager, Runtime, menu::MenuId};
use super::MenuItem;
impl<R: Runtime> MenuItem<R> {
/// Create a new menu item.
///
/// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic
/// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
pub fn new<M, T, A>(
manager: &M,
text: T,
enabled: bool,
accelerator: Option<A>,
) -> crate::Result<Self>
where
M: Manager<R>,
T: AsRef<str>,
A: AsRef<str>,
{
let handle = manager.app_handle();
let app_handle = handle.clone();
let text = text.as_ref().to_owned();
let accelerator = accelerator.and_then(|s| s.as_ref().parse().ok());
let item = run_main_thread!(handle, || {
let item = muda::MenuItem::new(text, enabled, accelerator);
MenuItemInner::new(app_handle, item)
})?;
Ok(Self(Arc::new(item)))
}
/// Create a new menu item with the specified id.
///
/// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic
/// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
pub fn with_id<M, I, T, A>(
manager: &M,
id: I,
text: T,
enabled: bool,
accelerator: Option<A>,
) -> crate::Result<Self>
where
M: Manager<R>,
I: Into<MenuId>,
T: AsRef<str>,
A: AsRef<str>,
{
let handle = manager.app_handle();
let app_handle = handle.clone();
let id = id.into();
let accelerator = accelerator.and_then(|s| s.as_ref().parse().ok());
let text = text.as_ref().to_owned();
let item = run_main_thread!(handle, move || {
let item = muda::MenuItem::with_id(id, text, enabled, accelerator);
MenuItemInner::new(app_handle, item)
})?;
Ok(Self(Arc::new(item)))
}
/// The application handle associated with this type.
pub fn app_handle(&self) -> &AppHandle<R> {
&self.0.app_handle
}
/// Returns a unique identifier associated with this menu item.
pub fn id(&self) -> &MenuId {
self.0.inner.id()
}
/// Get the text for this menu item.
pub fn text(&self) -> crate::Result<String> {
run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().text())
}
/// Set the text for this menu item. `text` could optionally contain
/// an `&` before a character to assign this character as the mnemonic
/// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
pub fn set_text<S: AsRef<str>>(&self, text: S) -> crate::Result<()> {
let text = text.as_ref().to_string();
run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().set_text(text))
}
/// Get whether this menu item is enabled or not.
pub fn is_enabled(&self) -> crate::Result<bool> {
run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().is_enabled())
}
/// Enable or disable this menu item.
pub fn set_enabled(&self, enabled: bool) -> crate::Result<()> {
run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().set_enabled(enabled))
}
/// Set this menu item accelerator.
pub fn set_accelerator<S: AsRef<str>>(&self, accelerator: Option<S>) -> crate::Result<()> {
let accel = accelerator.and_then(|s| s.as_ref().parse().ok());
run_item_main_thread!(self, |self_: Self| {
(*self_.0).as_ref().set_accelerator(accel)
})?
.map_err(Into::into)
}
}

View File

@@ -0,0 +1,941 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::{collections::HashMap, sync::Mutex};
use serde::{Deserialize, Serialize};
use tauri_runtime::dpi::Position;
use super::{sealed::ContextMenuBase, *};
use crate::{
Manager, ResourceTable, RunEvent, Runtime, State, Webview, Window, command,
image::JsImage,
ipc::{Channel, channel::JavaScriptChannelId},
plugin::{Builder, TauriPlugin},
resources::ResourceId,
sealed::ManagerBase,
};
use tauri_macros::do_menu_item;
#[derive(Deserialize, Serialize)]
pub(crate) enum ItemKind {
Menu,
MenuItem,
Predefined,
Submenu,
Check,
Icon,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct AboutMetadata {
pub name: Option<String>,
pub version: Option<String>,
pub short_version: Option<String>,
pub authors: Option<Vec<String>>,
pub comments: Option<String>,
pub copyright: Option<String>,
pub license: Option<String>,
pub website: Option<String>,
pub website_label: Option<String>,
pub credits: Option<String>,
pub icon: Option<JsImage>,
}
impl AboutMetadata {
pub fn into_metadata(
self,
resources_table: &ResourceTable,
) -> crate::Result<super::AboutMetadata<'_>> {
let icon = match self.icon {
Some(i) => Some(i.into_img(resources_table)?.as_ref().clone()),
None => None,
};
Ok(super::AboutMetadata {
name: self.name,
version: self.version,
short_version: self.short_version,
authors: self.authors,
comments: self.comments,
copyright: self.copyright,
license: self.license,
website: self.website,
website_label: self.website_label,
credits: self.credits,
icon,
})
}
}
#[allow(clippy::large_enum_variant)]
#[derive(Deserialize)]
enum Predefined {
Separator,
Copy,
Cut,
Paste,
SelectAll,
Undo,
Redo,
Minimize,
Maximize,
Fullscreen,
Hide,
HideOthers,
ShowAll,
CloseWindow,
Quit,
About(Option<AboutMetadata>),
Services,
BringAllToFront,
}
#[derive(Deserialize)]
struct SubmenuPayload {
id: Option<MenuId>,
text: String,
enabled: Option<bool>,
items: Vec<MenuItemPayloadKind>,
icon: Option<Icon>,
}
impl SubmenuPayload {
pub fn create_item<R: Runtime>(
self,
webview: &Webview<R>,
resources_table: &ResourceTable,
) -> crate::Result<Submenu<R>> {
let mut builder = if let Some(id) = self.id {
SubmenuBuilder::with_id(webview, id, self.text)
} else {
SubmenuBuilder::new(webview, self.text)
};
if let Some(enabled) = self.enabled {
builder = builder.enabled(enabled);
}
if let Some(icon) = self.icon {
builder = match icon {
Icon::Native(native_icon) => builder.submenu_native_icon(native_icon),
Icon::Icon(js_icon) => {
builder.submenu_icon(js_icon.into_img(resources_table)?.as_ref().clone())
}
};
}
for item in self.items {
builder = item.with_item(webview, resources_table, |i| Ok(builder.item(i)))?;
}
builder.build()
}
}
#[derive(Deserialize)]
struct CheckMenuItemPayload {
handler: Option<JavaScriptChannelId>,
id: Option<MenuId>,
text: String,
checked: bool,
enabled: Option<bool>,
accelerator: Option<String>,
}
impl CheckMenuItemPayload {
pub fn create_item<R: Runtime>(self, webview: &Webview<R>) -> crate::Result<CheckMenuItem<R>> {
let mut builder = if let Some(id) = self.id {
CheckMenuItemBuilder::with_id(id, self.text)
} else {
CheckMenuItemBuilder::new(self.text)
};
if let Some(accelerator) = self.accelerator {
builder = builder.accelerator(accelerator);
}
if let Some(enabled) = self.enabled {
builder = builder.enabled(enabled);
}
let item = builder.checked(self.checked).build(webview)?;
if let Some(handler) = self.handler {
let handler = handler.channel_on(webview.clone());
webview
.state::<MenuChannels>()
.0
.lock()
.unwrap()
.insert(item.id().clone(), handler);
}
Ok(item)
}
}
#[derive(Deserialize)]
#[serde(untagged)]
enum Icon {
Native(NativeIcon),
Icon(JsImage),
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct IconMenuItemPayload {
handler: Option<JavaScriptChannelId>,
id: Option<MenuId>,
text: String,
icon: Icon,
enabled: Option<bool>,
accelerator: Option<String>,
}
impl IconMenuItemPayload {
pub fn create_item<R: Runtime>(
self,
webview: &Webview<R>,
resources_table: &ResourceTable,
) -> crate::Result<IconMenuItem<R>> {
let mut builder = if let Some(id) = self.id {
IconMenuItemBuilder::with_id(id, self.text)
} else {
IconMenuItemBuilder::new(self.text)
};
if let Some(accelerator) = self.accelerator {
builder = builder.accelerator(accelerator);
}
if let Some(enabled) = self.enabled {
builder = builder.enabled(enabled);
}
builder = match self.icon {
Icon::Native(native_icon) => builder.native_icon(native_icon),
Icon::Icon(icon) => builder.icon(icon.into_img(resources_table)?.as_ref().clone()),
};
let item = builder.build(webview)?;
if let Some(handler) = self.handler {
let handler = handler.channel_on(webview.clone());
webview
.state::<MenuChannels>()
.0
.lock()
.unwrap()
.insert(item.id().clone(), handler);
}
Ok(item)
}
}
#[derive(Deserialize)]
struct MenuItemPayload {
handler: Option<JavaScriptChannelId>,
id: Option<MenuId>,
text: String,
enabled: Option<bool>,
accelerator: Option<String>,
}
impl MenuItemPayload {
pub fn create_item<R: Runtime>(self, webview: &Webview<R>) -> crate::Result<MenuItem<R>> {
let mut builder = if let Some(id) = self.id {
MenuItemBuilder::with_id(id, self.text)
} else {
MenuItemBuilder::new(self.text)
};
if let Some(accelerator) = self.accelerator {
builder = builder.accelerator(accelerator);
}
if let Some(enabled) = self.enabled {
builder = builder.enabled(enabled);
}
let item = builder.build(webview)?;
if let Some(handler) = self.handler {
let handler = handler.channel_on(webview.clone());
webview
.state::<MenuChannels>()
.0
.lock()
.unwrap()
.insert(item.id().clone(), handler);
}
Ok(item)
}
}
#[derive(Deserialize)]
struct PredefinedMenuItemPayload {
item: Predefined,
text: Option<String>,
}
impl PredefinedMenuItemPayload {
pub fn create_item<R: Runtime>(
self,
webview: &Webview<R>,
resources_table: &ResourceTable,
) -> crate::Result<PredefinedMenuItem<R>> {
match self.item {
Predefined::Separator => PredefinedMenuItem::separator(webview),
Predefined::Copy => PredefinedMenuItem::copy(webview, self.text.as_deref()),
Predefined::Cut => PredefinedMenuItem::cut(webview, self.text.as_deref()),
Predefined::Paste => PredefinedMenuItem::paste(webview, self.text.as_deref()),
Predefined::SelectAll => PredefinedMenuItem::select_all(webview, self.text.as_deref()),
Predefined::Undo => PredefinedMenuItem::undo(webview, self.text.as_deref()),
Predefined::Redo => PredefinedMenuItem::redo(webview, self.text.as_deref()),
Predefined::Minimize => PredefinedMenuItem::minimize(webview, self.text.as_deref()),
Predefined::Maximize => PredefinedMenuItem::maximize(webview, self.text.as_deref()),
Predefined::Fullscreen => PredefinedMenuItem::fullscreen(webview, self.text.as_deref()),
Predefined::Hide => PredefinedMenuItem::hide(webview, self.text.as_deref()),
Predefined::HideOthers => PredefinedMenuItem::hide_others(webview, self.text.as_deref()),
Predefined::ShowAll => PredefinedMenuItem::show_all(webview, self.text.as_deref()),
Predefined::CloseWindow => PredefinedMenuItem::close_window(webview, self.text.as_deref()),
Predefined::Quit => PredefinedMenuItem::quit(webview, self.text.as_deref()),
Predefined::About(metadata) => {
let metadata = match metadata {
Some(m) => Some(m.into_metadata(resources_table)?),
None => None,
};
PredefinedMenuItem::about(webview, self.text.as_deref(), metadata)
}
Predefined::Services => PredefinedMenuItem::services(webview, self.text.as_deref()),
Predefined::BringAllToFront => {
PredefinedMenuItem::bring_all_to_front(webview, self.text.as_deref())
}
}
}
}
#[derive(Deserialize)]
#[serde(untagged)]
// Note, order matters for untagged enum deserialization
enum MenuItemPayloadKind {
ExistingItem((ResourceId, ItemKind)),
Predefined(PredefinedMenuItemPayload),
Check(CheckMenuItemPayload),
Icon(IconMenuItemPayload),
Submenu(SubmenuPayload),
MenuItem(MenuItemPayload),
}
impl MenuItemPayloadKind {
pub fn with_item<T, R: Runtime, F: FnOnce(&dyn IsMenuItem<R>) -> crate::Result<T>>(
self,
webview: &Webview<R>,
resources_table: &ResourceTable,
f: F,
) -> crate::Result<T> {
match self {
Self::ExistingItem((rid, kind)) => {
do_menu_item!(resources_table, rid, kind, |i| f(&*i))
}
Self::Submenu(i) => f(&i.create_item(webview, resources_table)?),
Self::Predefined(i) => f(&i.create_item(webview, resources_table)?),
Self::Check(i) => f(&i.create_item(webview)?),
Self::Icon(i) => f(&i.create_item(webview, resources_table)?),
Self::MenuItem(i) => f(&i.create_item(webview)?),
}
}
}
#[derive(Deserialize, Default)]
#[serde(rename_all = "camelCase")]
struct NewOptions {
id: Option<MenuId>,
text: Option<String>,
enabled: Option<bool>,
checked: Option<bool>,
accelerator: Option<String>,
#[serde(rename = "item")]
predefined_item: Option<Predefined>,
icon: Option<Icon>,
items: Option<Vec<MenuItemPayloadKind>>,
}
#[command(root = "crate")]
fn new<R: Runtime>(
webview: Webview<R>,
kind: ItemKind,
options: Option<NewOptions>,
channels: State<'_, MenuChannels>,
handler: Channel<MenuId>,
) -> crate::Result<(ResourceId, MenuId)> {
let options = options.unwrap_or_default();
let mut resources_table = webview.resources_table();
let (rid, id) = match kind {
ItemKind::Menu => {
let mut builder = MenuBuilder::new(&webview);
if let Some(id) = options.id {
builder = builder.id(id);
}
if let Some(items) = options.items {
for item in items {
builder = item.with_item(&webview, &resources_table, |i| Ok(builder.item(i)))?;
}
}
let menu = builder.build()?;
let id = menu.id().clone();
let rid = resources_table.add(menu);
(rid, id)
}
ItemKind::Submenu => {
let submenu = SubmenuPayload {
id: options.id,
text: options.text.unwrap_or_default(),
enabled: options.enabled,
items: options.items.unwrap_or_default(),
icon: options.icon,
}
.create_item(&webview, &resources_table)?;
let id = submenu.id().clone();
let rid = resources_table.add(submenu);
(rid, id)
}
ItemKind::MenuItem => {
let item = MenuItemPayload {
// handler managed in this function instead
handler: None,
id: options.id,
text: options.text.unwrap_or_default(),
enabled: options.enabled,
accelerator: options.accelerator,
}
.create_item(&webview)?;
let id = item.id().clone();
let rid = resources_table.add(item);
(rid, id)
}
ItemKind::Predefined => {
let item = PredefinedMenuItemPayload {
item: options.predefined_item.unwrap(),
text: options.text,
}
.create_item(&webview, &resources_table)?;
let id = item.id().clone();
let rid = resources_table.add(item);
(rid, id)
}
ItemKind::Check => {
let item = CheckMenuItemPayload {
// handler managed in this function instead
handler: None,
id: options.id,
text: options.text.unwrap_or_default(),
checked: options.checked.unwrap_or_default(),
enabled: options.enabled,
accelerator: options.accelerator,
}
.create_item(&webview)?;
let id = item.id().clone();
let rid = resources_table.add(item);
(rid, id)
}
ItemKind::Icon => {
let item = IconMenuItemPayload {
// handler managed in this function instead
handler: None,
id: options.id,
text: options.text.unwrap_or_default(),
icon: options.icon.unwrap_or(Icon::Native(NativeIcon::User)),
enabled: options.enabled,
accelerator: options.accelerator,
}
.create_item(&webview, &resources_table)?;
let id = item.id().clone();
let rid = resources_table.add(item);
(rid, id)
}
};
channels.0.lock().unwrap().insert(id.clone(), handler);
Ok((rid, id))
}
#[command(root = "crate")]
fn append<R: Runtime>(
webview: Webview<R>,
rid: ResourceId,
kind: ItemKind,
items: Vec<MenuItemPayloadKind>,
) -> crate::Result<()> {
let resources_table = webview.resources_table();
match kind {
ItemKind::Menu => {
let menu = resources_table.get::<Menu<R>>(rid)?;
for item in items {
item.with_item(&webview, &resources_table, |i| menu.append(i))?;
}
}
ItemKind::Submenu => {
let submenu = resources_table.get::<Submenu<R>>(rid)?;
for item in items {
item.with_item(&webview, &resources_table, |i| submenu.append(i))?;
}
}
_ => return Err(crate::Error::UnexpectedMenuKind),
};
Ok(())
}
#[command(root = "crate")]
fn prepend<R: Runtime>(
webview: Webview<R>,
rid: ResourceId,
kind: ItemKind,
items: Vec<MenuItemPayloadKind>,
) -> crate::Result<()> {
let resources_table = webview.resources_table();
match kind {
ItemKind::Menu => {
let menu = resources_table.get::<Menu<R>>(rid)?;
for item in items {
item.with_item(&webview, &resources_table, |i| menu.prepend(i))?;
}
}
ItemKind::Submenu => {
let submenu = resources_table.get::<Submenu<R>>(rid)?;
for item in items {
item.with_item(&webview, &resources_table, |i| submenu.prepend(i))?;
}
}
_ => return Err(crate::Error::UnexpectedMenuKind),
};
Ok(())
}
#[command(root = "crate")]
fn insert<R: Runtime>(
webview: Webview<R>,
rid: ResourceId,
kind: ItemKind,
items: Vec<MenuItemPayloadKind>,
mut position: usize,
) -> crate::Result<()> {
let resources_table = webview.resources_table();
match kind {
ItemKind::Menu => {
let menu = resources_table.get::<Menu<R>>(rid)?;
for item in items {
item.with_item(&webview, &resources_table, |i| menu.insert(i, position))?;
position += 1
}
}
ItemKind::Submenu => {
let submenu = resources_table.get::<Submenu<R>>(rid)?;
for item in items {
item.with_item(&webview, &resources_table, |i| submenu.insert(i, position))?;
position += 1
}
}
_ => return Err(crate::Error::UnexpectedMenuKind),
};
Ok(())
}
#[command(root = "crate")]
fn remove<R: Runtime>(
webview: Webview<R>,
rid: ResourceId,
kind: ItemKind,
item: (ResourceId, ItemKind),
) -> crate::Result<()> {
let resources_table = webview.resources_table();
let (item_rid, item_kind) = item;
match kind {
ItemKind::Menu => {
let menu = resources_table.get::<Menu<R>>(rid)?;
do_menu_item!(resources_table, item_rid, item_kind, |i| menu.remove(&*i))?;
}
ItemKind::Submenu => {
let submenu = resources_table.get::<Submenu<R>>(rid)?;
do_menu_item!(resources_table, item_rid, item_kind, |i| submenu
.remove(&*i))?;
}
_ => return Err(crate::Error::UnexpectedMenuKind),
};
Ok(())
}
macro_rules! make_item_resource {
($resources_table:ident, $item:ident) => {{
let id = $item.id().clone();
let (rid, kind) = match $item {
MenuItemKind::MenuItem(i) => ($resources_table.add(i), ItemKind::MenuItem),
MenuItemKind::Submenu(i) => ($resources_table.add(i), ItemKind::Submenu),
MenuItemKind::Predefined(i) => ($resources_table.add(i), ItemKind::Predefined),
MenuItemKind::Check(i) => ($resources_table.add(i), ItemKind::Check),
MenuItemKind::Icon(i) => ($resources_table.add(i), ItemKind::Icon),
};
(rid, id, kind)
}};
}
#[command(root = "crate")]
fn remove_at<R: Runtime>(
webview: Webview<R>,
rid: ResourceId,
kind: ItemKind,
position: usize,
) -> crate::Result<Option<(ResourceId, MenuId, ItemKind)>> {
let mut resources_table = webview.resources_table();
match kind {
ItemKind::Menu => {
let menu = resources_table.get::<Menu<R>>(rid)?;
if let Some(item) = menu.remove_at(position)? {
return Ok(Some(make_item_resource!(resources_table, item)));
}
}
ItemKind::Submenu => {
let submenu = resources_table.get::<Submenu<R>>(rid)?;
if let Some(item) = submenu.remove_at(position)? {
return Ok(Some(make_item_resource!(resources_table, item)));
}
}
_ => return Err(crate::Error::UnexpectedMenuKind),
};
Ok(None)
}
#[command(root = "crate")]
fn items<R: Runtime>(
webview: Webview<R>,
rid: ResourceId,
kind: ItemKind,
) -> crate::Result<Vec<(ResourceId, MenuId, ItemKind)>> {
let mut resources_table = webview.resources_table();
let items = match kind {
ItemKind::Menu => resources_table.get::<Menu<R>>(rid)?.items()?,
ItemKind::Submenu => resources_table.get::<Submenu<R>>(rid)?.items()?,
_ => return Err(crate::Error::UnexpectedMenuKind),
};
Ok(
items
.into_iter()
.map(|i| make_item_resource!(resources_table, i))
.collect::<Vec<_>>(),
)
}
#[command(root = "crate")]
fn get<R: Runtime>(
webview: Webview<R>,
rid: ResourceId,
kind: ItemKind,
id: MenuId,
) -> crate::Result<Option<(ResourceId, MenuId, ItemKind)>> {
let mut resources_table = webview.resources_table();
match kind {
ItemKind::Menu => {
let menu = resources_table.get::<Menu<R>>(rid)?;
if let Some(item) = menu.get(&id) {
return Ok(Some(make_item_resource!(resources_table, item)));
}
}
ItemKind::Submenu => {
let submenu = resources_table.get::<Submenu<R>>(rid)?;
if let Some(item) = submenu.get(&id) {
return Ok(Some(make_item_resource!(resources_table, item)));
}
}
_ => return Err(crate::Error::UnexpectedMenuKind),
};
Ok(None)
}
#[command(root = "crate")]
async fn popup<R: Runtime>(
webview: Webview<R>,
current_window: Window<R>,
rid: ResourceId,
kind: ItemKind,
window: Option<String>,
at: Option<Position>,
) -> crate::Result<()> {
let window = window
.map(|w| webview.manager().get_window(&w))
.unwrap_or(Some(current_window));
if let Some(window) = window {
let resources_table = webview.resources_table();
match kind {
ItemKind::Menu => {
let menu = resources_table.get::<Menu<R>>(rid)?;
menu.popup_inner(window, at)?;
}
ItemKind::Submenu => {
let submenu = resources_table.get::<Submenu<R>>(rid)?;
submenu.popup_inner(window, at)?;
}
_ => return Err(crate::Error::UnexpectedMenuKind),
};
}
Ok(())
}
#[command(root = "crate")]
fn create_default<R: Runtime>(
app: AppHandle<R>,
webview: Webview<R>,
) -> crate::Result<(ResourceId, MenuId)> {
let mut resources_table = webview.resources_table();
let menu = Menu::default(&app)?;
let id = menu.id().clone();
let rid = resources_table.add(menu);
Ok((rid, id))
}
#[command(root = "crate")]
async fn set_as_app_menu<R: Runtime>(
webview: Webview<R>,
rid: ResourceId,
) -> crate::Result<Option<(ResourceId, MenuId)>> {
let mut resources_table = webview.resources_table();
let menu = resources_table.get::<Menu<R>>(rid)?;
if let Some(menu) = menu.set_as_app_menu()? {
let id = menu.id().clone();
let rid = resources_table.add(menu);
return Ok(Some((rid, id)));
}
Ok(None)
}
#[command(root = "crate")]
async fn set_as_window_menu<R: Runtime>(
webview: Webview<R>,
current_window: Window<R>,
rid: ResourceId,
window: Option<String>,
) -> crate::Result<Option<(ResourceId, MenuId)>> {
let window = window
.map(|w| webview.manager().get_window(&w))
.unwrap_or(Some(current_window));
if let Some(window) = window {
let mut resources_table = webview.resources_table();
let menu = resources_table.get::<Menu<R>>(rid)?;
if let Some(menu) = menu.set_as_window_menu(&window)? {
let id = menu.id().clone();
let rid = resources_table.add(menu);
return Ok(Some((rid, id)));
}
}
Ok(None)
}
#[command(root = "crate")]
fn text<R: Runtime>(webview: Webview<R>, rid: ResourceId, kind: ItemKind) -> crate::Result<String> {
let resources_table = webview.resources_table();
do_menu_item!(resources_table, rid, kind, |i| i.text())
}
#[command(root = "crate")]
fn set_text<R: Runtime>(
webview: Webview<R>,
rid: ResourceId,
kind: ItemKind,
text: String,
) -> crate::Result<()> {
let resources_table = webview.resources_table();
do_menu_item!(resources_table, rid, kind, |i| i.set_text(text))
}
#[command(root = "crate")]
fn is_enabled<R: Runtime>(
webview: Webview<R>,
rid: ResourceId,
kind: ItemKind,
) -> crate::Result<bool> {
let resources_table = webview.resources_table();
do_menu_item!(resources_table, rid, kind, |i| i.is_enabled(), !Predefined)
}
#[command(root = "crate")]
fn set_enabled<R: Runtime>(
webview: Webview<R>,
rid: ResourceId,
kind: ItemKind,
enabled: bool,
) -> crate::Result<()> {
let resources_table = webview.resources_table();
do_menu_item!(
resources_table,
rid,
kind,
|i| i.set_enabled(enabled),
!Predefined
)
}
#[command(root = "crate")]
fn set_accelerator<R: Runtime>(
webview: Webview<R>,
rid: ResourceId,
kind: ItemKind,
accelerator: Option<String>,
) -> crate::Result<()> {
let resources_table = webview.resources_table();
do_menu_item!(
resources_table,
rid,
kind,
|i| i.set_accelerator(accelerator),
!Predefined | !Submenu
)
}
#[command(root = "crate")]
fn set_as_windows_menu_for_nsapp<R: Runtime>(
webview: Webview<R>,
rid: ResourceId,
) -> crate::Result<()> {
#[cfg(target_os = "macos")]
{
let resources_table = webview.resources_table();
let submenu = resources_table.get::<Submenu<R>>(rid)?;
submenu.set_as_windows_menu_for_nsapp()?;
}
let _ = rid;
let _ = webview;
Ok(())
}
#[command(root = "crate")]
fn set_as_help_menu_for_nsapp<R: Runtime>(
webview: Webview<R>,
rid: ResourceId,
) -> crate::Result<()> {
#[cfg(target_os = "macos")]
{
let resources_table = webview.resources_table();
let submenu = resources_table.get::<Submenu<R>>(rid)?;
submenu.set_as_help_menu_for_nsapp()?;
}
let _ = rid;
let _ = webview;
Ok(())
}
#[command(root = "crate")]
fn is_checked<R: Runtime>(webview: Webview<R>, rid: ResourceId) -> crate::Result<bool> {
let resources_table = webview.resources_table();
let check_item = resources_table.get::<CheckMenuItem<R>>(rid)?;
check_item.is_checked()
}
#[command(root = "crate")]
fn set_checked<R: Runtime>(
webview: Webview<R>,
rid: ResourceId,
checked: bool,
) -> crate::Result<()> {
let resources_table = webview.resources_table();
let check_item = resources_table.get::<CheckMenuItem<R>>(rid)?;
check_item.set_checked(checked)
}
#[command(root = "crate")]
fn set_icon<R: Runtime>(
webview: Webview<R>,
rid: ResourceId,
kind: ItemKind,
icon: Option<Icon>,
) -> crate::Result<()> {
let resources_table = webview.resources_table();
do_menu_item!(
resources_table,
rid,
kind,
|icon_item| match icon {
Some(Icon::Native(icon)) => icon_item.set_native_icon(Some(icon)),
Some(Icon::Icon(icon)) => {
icon_item.set_icon(Some(icon.into_img(&resources_table)?.as_ref().clone()))
}
None => {
icon_item.set_icon(None)?;
icon_item.set_native_icon(None)?;
Ok(())
}
},
Icon | Submenu
)
}
struct MenuChannels(Mutex<HashMap<MenuId, Channel<MenuId>>>);
// Called in `Menu`'s `Drop` to clean up the event handlers
pub(crate) fn remove_menu_channel<R: Runtime>(app: &AppHandle<R>, id: &MenuId) {
app.state::<MenuChannels>().0.lock().unwrap().remove(id);
}
pub(crate) fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("menu")
.setup(|app, _api| {
app.manage(MenuChannels(Mutex::default()));
Ok(())
})
.on_event(|app, e| {
if let RunEvent::MenuEvent(e) = e {
// Cloning the channel out in case the menu gets dropped during the channel send through `channel_interceptor`
let channel = app
.state::<MenuChannels>()
.0
.lock()
.unwrap()
.get(&e.id)
.cloned();
if let Some(channel) = channel {
let _ = channel.send(e.id.clone());
}
}
})
.invoke_handler(crate::generate_handler![
#![plugin(menu)]
new,
append,
prepend,
insert,
remove,
remove_at,
items,
get,
popup,
create_default,
set_as_app_menu,
set_as_window_menu,
text,
set_text,
is_enabled,
set_enabled,
set_accelerator,
set_as_windows_menu_for_nsapp,
set_as_help_menu_for_nsapp,
is_checked,
set_checked,
set_icon,
])
.build()
}

View File

@@ -0,0 +1,360 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::sync::Arc;
use super::run_item_main_thread;
use super::{AboutMetadata, PredefinedMenuItem};
use crate::menu::PredefinedMenuItemInner;
use crate::run_main_thread;
use crate::{AppHandle, Manager, Runtime, menu::MenuId};
impl<R: Runtime> PredefinedMenuItem<R> {
/// Separator menu item
pub fn separator<M: Manager<R>>(manager: &M) -> crate::Result<Self> {
let handle = manager.app_handle();
let app_handle = handle.clone();
let item = run_main_thread!(handle, || {
let item = muda::PredefinedMenuItem::separator();
PredefinedMenuItemInner::new(app_handle, item)
})?;
Ok(Self(Arc::new(item)))
}
/// Copy menu item
pub fn copy<M: Manager<R>>(manager: &M, text: Option<&str>) -> crate::Result<Self> {
let handle = manager.app_handle();
let app_handle = handle.clone();
let text = text.map(|t| t.to_owned());
let item = run_main_thread!(handle, || {
let item = muda::PredefinedMenuItem::copy(text.as_deref());
PredefinedMenuItemInner::new(app_handle, item)
})?;
Ok(Self(Arc::new(item)))
}
/// Cut menu item
pub fn cut<M: Manager<R>>(manager: &M, text: Option<&str>) -> crate::Result<Self> {
let handle = manager.app_handle();
let app_handle = handle.clone();
let text = text.map(|t| t.to_owned());
let item = run_main_thread!(handle, || {
let item = muda::PredefinedMenuItem::cut(text.as_deref());
PredefinedMenuItemInner::new(app_handle, item)
})?;
Ok(Self(Arc::new(item)))
}
/// Paste menu item
pub fn paste<M: Manager<R>>(manager: &M, text: Option<&str>) -> crate::Result<Self> {
let handle = manager.app_handle();
let app_handle = handle.clone();
let text = text.map(|t| t.to_owned());
let item = run_main_thread!(handle, || {
let item = muda::PredefinedMenuItem::paste(text.as_deref());
PredefinedMenuItemInner::new(app_handle, item)
})?;
Ok(Self(Arc::new(item)))
}
/// SelectAll menu item
pub fn select_all<M: Manager<R>>(manager: &M, text: Option<&str>) -> crate::Result<Self> {
let handle = manager.app_handle();
let app_handle = handle.clone();
let text = text.map(|t| t.to_owned());
let item = run_main_thread!(handle, || {
let item = muda::PredefinedMenuItem::select_all(text.as_deref());
PredefinedMenuItemInner::new(app_handle, item)
})?;
Ok(Self(Arc::new(item)))
}
/// Undo menu item
///
/// ## Platform-specific:
///
/// - **Windows / Linux:** Unsupported.
pub fn undo<M: Manager<R>>(manager: &M, text: Option<&str>) -> crate::Result<Self> {
let handle = manager.app_handle();
let app_handle = handle.clone();
let text = text.map(|t| t.to_owned());
let item = run_main_thread!(handle, || {
let item = muda::PredefinedMenuItem::undo(text.as_deref());
PredefinedMenuItemInner::new(app_handle, item)
})?;
Ok(Self(Arc::new(item)))
}
/// Redo menu item
///
/// ## Platform-specific:
///
/// - **Windows / Linux:** Unsupported.
pub fn redo<M: Manager<R>>(manager: &M, text: Option<&str>) -> crate::Result<Self> {
let handle = manager.app_handle();
let app_handle = handle.clone();
let text = text.map(|t| t.to_owned());
let item = run_main_thread!(handle, || {
let item = muda::PredefinedMenuItem::redo(text.as_deref());
PredefinedMenuItemInner::new(app_handle, item)
})?;
Ok(Self(Arc::new(item)))
}
/// Minimize window menu item
///
/// ## Platform-specific:
///
/// - **Linux:** Unsupported.
pub fn minimize<M: Manager<R>>(manager: &M, text: Option<&str>) -> crate::Result<Self> {
let handle = manager.app_handle();
let app_handle = handle.clone();
let text = text.map(|t| t.to_owned());
let item = run_main_thread!(handle, || {
let item = muda::PredefinedMenuItem::minimize(text.as_deref());
PredefinedMenuItemInner::new(app_handle, item)
})?;
Ok(Self(Arc::new(item)))
}
/// Maximize window menu item
///
/// ## Platform-specific:
///
/// - **Linux:** Unsupported.
pub fn maximize<M: Manager<R>>(manager: &M, text: Option<&str>) -> crate::Result<Self> {
let handle = manager.app_handle();
let app_handle = handle.clone();
let text = text.map(|t| t.to_owned());
let item = run_main_thread!(handle, || {
let item = muda::PredefinedMenuItem::maximize(text.as_deref());
PredefinedMenuItemInner::new(app_handle, item)
})?;
Ok(Self(Arc::new(item)))
}
/// Fullscreen menu item
///
/// ## Platform-specific:
///
/// - **Windows / Linux:** Unsupported.
pub fn fullscreen<M: Manager<R>>(manager: &M, text: Option<&str>) -> crate::Result<Self> {
let handle = manager.app_handle();
let app_handle = handle.clone();
let text = text.map(|t| t.to_owned());
let item = run_main_thread!(handle, || {
let item = muda::PredefinedMenuItem::fullscreen(text.as_deref());
PredefinedMenuItemInner::new(app_handle, item)
})?;
Ok(Self(Arc::new(item)))
}
/// Hide window menu item
///
/// ## Platform-specific:
///
/// - **Linux:** Unsupported.
pub fn hide<M: Manager<R>>(manager: &M, text: Option<&str>) -> crate::Result<Self> {
let handle = manager.app_handle();
let app_handle = handle.clone();
let text = text.map(|t| t.to_owned());
let item = run_main_thread!(handle, || {
let item = muda::PredefinedMenuItem::hide(text.as_deref());
PredefinedMenuItemInner::new(app_handle, item)
})?;
Ok(Self(Arc::new(item)))
}
/// Hide other windows menu item
///
/// ## Platform-specific:
///
/// - **Linux:** Unsupported.
pub fn hide_others<M: Manager<R>>(manager: &M, text: Option<&str>) -> crate::Result<Self> {
let handle = manager.app_handle();
let app_handle = handle.clone();
let text = text.map(|t| t.to_owned());
let item = run_main_thread!(handle, || {
let item = muda::PredefinedMenuItem::hide_others(text.as_deref());
PredefinedMenuItemInner::new(app_handle, item)
})?;
Ok(Self(Arc::new(item)))
}
/// Show all app windows menu item
///
/// ## Platform-specific:
///
/// - **Windows / Linux:** Unsupported.
pub fn show_all<M: Manager<R>>(manager: &M, text: Option<&str>) -> crate::Result<Self> {
let handle = manager.app_handle();
let app_handle = handle.clone();
let text = text.map(|t| t.to_owned());
let item = run_main_thread!(handle, || {
let item = muda::PredefinedMenuItem::show_all(text.as_deref());
PredefinedMenuItemInner::new(app_handle, item)
})?;
Ok(Self(Arc::new(item)))
}
/// Close window menu item
///
/// ## Platform-specific:
///
/// - **Linux:** Unsupported.
pub fn close_window<M: Manager<R>>(manager: &M, text: Option<&str>) -> crate::Result<Self> {
let handle = manager.app_handle();
let app_handle = handle.clone();
let text = text.map(|t| t.to_owned());
let item = run_main_thread!(handle, || {
let item = muda::PredefinedMenuItem::close_window(text.as_deref());
PredefinedMenuItemInner::new(app_handle, item)
})?;
Ok(Self(Arc::new(item)))
}
/// Quit app menu item
///
/// ## Platform-specific:
///
/// - **Linux:** Unsupported.
pub fn quit<M: Manager<R>>(manager: &M, text: Option<&str>) -> crate::Result<Self> {
let handle = manager.app_handle();
let app_handle = handle.clone();
let text = text.map(|t| t.to_owned());
let item = run_main_thread!(handle, || {
let item = muda::PredefinedMenuItem::quit(text.as_deref());
PredefinedMenuItemInner::new(app_handle, item)
})?;
Ok(Self(Arc::new(item)))
}
/// About app menu item
pub fn about<M: Manager<R>>(
manager: &M,
text: Option<&str>,
metadata: Option<AboutMetadata<'_>>,
) -> crate::Result<Self> {
let handle = manager.app_handle();
let app_handle = handle.clone();
let text = text.map(|t| t.to_owned());
let metadata = match metadata {
Some(m) => Some(m.try_into()?),
None => None,
};
let item = run_main_thread!(handle, || {
let item = muda::PredefinedMenuItem::about(text.as_deref(), metadata);
PredefinedMenuItemInner::new(app_handle, item)
})?;
Ok(Self(Arc::new(item)))
}
/// Services menu item
///
/// ## Platform-specific:
///
/// - **Windows / Linux:** Unsupported.
pub fn services<M: Manager<R>>(manager: &M, text: Option<&str>) -> crate::Result<Self> {
let handle = manager.app_handle();
let app_handle = handle.clone();
let text = text.map(|t| t.to_owned());
let item = run_main_thread!(handle, || {
let item = muda::PredefinedMenuItem::services(text.as_deref());
PredefinedMenuItemInner::new(app_handle, item)
})?;
Ok(Self(Arc::new(item)))
}
/// Bring All to Front menu item
///
/// ## Platform-specific:
///
/// - **Windows / Linux:** Unsupported.
pub fn bring_all_to_front<M: Manager<R>>(manager: &M, text: Option<&str>) -> crate::Result<Self> {
let handle = manager.app_handle();
let app_handle = handle.clone();
let text = text.map(|t| t.to_owned());
let item = run_main_thread!(handle, || {
let item = muda::PredefinedMenuItem::bring_all_to_front(text.as_deref());
PredefinedMenuItemInner::new(app_handle, item)
})?;
Ok(Self(Arc::new(item)))
}
/// Returns a unique identifier associated with this menu item.
pub fn id(&self) -> &MenuId {
self.0.inner.id()
}
/// Get the text for this menu item.
pub fn text(&self) -> crate::Result<String> {
run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().text())
}
/// Set the text for this menu item. `text` could optionally contain
/// an `&` before a character to assign this character as the mnemonic
/// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
pub fn set_text<S: AsRef<str>>(&self, text: S) -> crate::Result<()> {
let text = text.as_ref().to_string();
run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().set_text(text))
}
/// The application handle associated with this type.
pub fn app_handle(&self) -> &AppHandle<R> {
&self.0.app_handle
}
}

View File

@@ -0,0 +1,419 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::sync::Arc;
use super::Submenu;
use super::run_item_main_thread;
use super::{IsMenuItem, MenuItemKind, sealed::ContextMenuBase};
use crate::menu::NativeIcon;
use crate::menu::SubmenuInner;
use crate::run_main_thread;
use crate::{AppHandle, Manager, Position, Runtime, Window};
use muda::{ContextMenu, Icon as MudaIcon, MenuId};
impl<R: Runtime> super::ContextMenu for Submenu<R> {
#[cfg(target_os = "windows")]
fn hpopupmenu(&self) -> crate::Result<isize> {
run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().hpopupmenu())
}
fn popup<T: Runtime>(&self, window: Window<T>) -> crate::Result<()> {
self.popup_inner(window, None::<Position>)
}
fn popup_at<T: Runtime, P: Into<Position>>(
&self,
window: Window<T>,
position: P,
) -> crate::Result<()> {
self.popup_inner(window, Some(position))
}
}
impl<R: Runtime> ContextMenuBase for Submenu<R> {
fn popup_inner<T: Runtime, P: Into<crate::Position>>(
&self,
window: crate::Window<T>,
position: Option<P>,
) -> crate::Result<()> {
let position = position.map(Into::into);
run_item_main_thread!(self, move |self_: Self| {
#[cfg(target_os = "macos")]
if let Ok(view) = window.ns_view() {
unsafe {
self_
.inner()
.show_context_menu_for_nsview(view as _, position);
}
}
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
if let Ok(w) = window.gtk_window() {
self_
.inner()
.show_context_menu_for_gtk_window(w.as_ref(), position);
}
#[cfg(windows)]
if let Ok(hwnd) = window.hwnd() {
unsafe {
self_
.inner()
.show_context_menu_for_hwnd(hwnd.0 as _, position);
}
}
})
}
fn inner_context(&self) -> &dyn muda::ContextMenu {
(*self.0).as_ref()
}
fn inner_context_owned(&self) -> Box<dyn muda::ContextMenu> {
Box::new((*self.0).as_ref().clone())
}
}
impl<R: Runtime> Submenu<R> {
/// Creates a new submenu.
pub fn new<M: Manager<R>, S: AsRef<str>>(
manager: &M,
text: S,
enabled: bool,
) -> crate::Result<Self> {
let handle = manager.app_handle();
let app_handle = handle.clone();
let text = text.as_ref().to_owned();
let submenu = run_main_thread!(handle, || {
let submenu = muda::Submenu::new(text, enabled);
SubmenuInner::new(app_handle, submenu)
})?;
Ok(Self(Arc::new(submenu)))
}
/// Create a new submenu with an icon.
pub fn new_with_icon<M: Manager<R>, S: AsRef<str>>(
manager: &M,
text: S,
enabled: bool,
icon: Option<crate::image::Image<'_>>,
) -> crate::Result<Self> {
let handle = manager.app_handle();
let app_handle = handle.clone();
let text = text.as_ref().to_owned();
let icon_data = icon.map(|i| (i.rgba().to_vec(), i.width(), i.height()));
let submenu = run_main_thread!(handle, move || {
let submenu = muda::Submenu::new(text, enabled);
if let Some((rgba, width, height)) = icon_data {
submenu.set_icon(Some(MudaIcon::from_rgba(rgba, width, height).unwrap()));
}
SubmenuInner::new(app_handle, submenu)
})?;
Ok(Self(Arc::new(submenu)))
}
/// Create a new submenu with a native icon.
pub fn new_with_native_icon<M: Manager<R>, S: AsRef<str>>(
manager: &M,
text: S,
enabled: bool,
icon: Option<NativeIcon>,
) -> crate::Result<Self> {
let handle = manager.app_handle();
let app_handle = handle.clone();
let text = text.as_ref().to_owned();
let submenu = run_main_thread!(handle, || {
let submenu = muda::Submenu::new(text, enabled);
if let Some(icon) = icon {
submenu.set_native_icon(Some(icon.into()));
}
SubmenuInner::new(app_handle, submenu)
})?;
Ok(Self(Arc::new(submenu)))
}
/// Creates a new submenu with the specified id.
pub fn with_id<M: Manager<R>, I: Into<MenuId>, S: AsRef<str>>(
manager: &M,
id: I,
text: S,
enabled: bool,
) -> crate::Result<Self> {
let handle = manager.app_handle();
let app_handle = handle.clone();
let id = id.into();
let text = text.as_ref().to_owned();
let submenu = run_main_thread!(handle, move || {
let submenu = muda::Submenu::with_id(id, text, enabled);
SubmenuInner::new(app_handle, submenu)
})?;
Ok(Self(Arc::new(submenu)))
}
/// Create a new submenu with an id and an icon.
pub fn with_id_and_icon<M: Manager<R>, I: Into<MenuId>, S: AsRef<str>>(
manager: &M,
id: I,
text: S,
enabled: bool,
icon: Option<crate::image::Image<'_>>,
) -> crate::Result<Self> {
let handle = manager.app_handle();
let app_handle = handle.clone();
let id = id.into();
let text = text.as_ref().to_owned();
let icon_data = icon.map(|i| (i.rgba().to_vec(), i.width(), i.height()));
let submenu = run_main_thread!(handle, move || {
let submenu = muda::Submenu::with_id(id, text, enabled);
if let Some((rgba, width, height)) = icon_data {
submenu.set_icon(Some(MudaIcon::from_rgba(rgba, width, height).unwrap()));
}
SubmenuInner::new(app_handle, submenu)
})?;
Ok(Self(Arc::new(submenu)))
}
/// Create a new submenu with an id and a native icon.
pub fn with_id_and_native_icon<M: Manager<R>, I: Into<MenuId>, S: AsRef<str>>(
manager: &M,
id: I,
text: S,
enabled: bool,
icon: Option<NativeIcon>,
) -> crate::Result<Self> {
let handle = manager.app_handle();
let app_handle = handle.clone();
let id = id.into();
let text = text.as_ref().to_owned();
let submenu = run_main_thread!(handle, move || {
let submenu = muda::Submenu::with_id(id, text, enabled);
if let Some(icon) = icon {
submenu.set_native_icon(Some(icon.into()));
}
SubmenuInner::new(app_handle, submenu)
})?;
Ok(Self(Arc::new(submenu)))
}
/// Creates a new menu with given `items`. It calls [`Submenu::new`] and [`Submenu::append_items`] internally.
pub fn with_items<M: Manager<R>, S: AsRef<str>>(
manager: &M,
text: S,
enabled: bool,
items: &[&dyn IsMenuItem<R>],
) -> crate::Result<Self> {
let menu = Self::new(manager, text, enabled)?;
menu.append_items(items)?;
Ok(menu)
}
/// Creates a new menu with the specified id and given `items`.
/// It calls [`Submenu::new`] and [`Submenu::append_items`] internally.
pub fn with_id_and_items<M: Manager<R>, I: Into<MenuId>, S: AsRef<str>>(
manager: &M,
id: I,
text: S,
enabled: bool,
items: &[&dyn IsMenuItem<R>],
) -> crate::Result<Self> {
let menu = Self::with_id(manager, id, text, enabled)?;
menu.append_items(items)?;
Ok(menu)
}
pub(crate) fn inner(&self) -> &muda::Submenu {
(*self.0).as_ref()
}
/// The application handle associated with this type.
pub fn app_handle(&self) -> &AppHandle<R> {
&self.0.app_handle
}
/// Returns a unique identifier associated with this submenu.
pub fn id(&self) -> &MenuId {
self.0.inner.id()
}
/// Add a menu item to the end of this submenu.
pub fn append(&self, item: &dyn IsMenuItem<R>) -> crate::Result<()> {
let kind = item.kind();
run_item_main_thread!(self, |self_: Self| {
(*self_.0).as_ref().append(kind.inner().inner_muda())
})?
.map_err(Into::into)
}
/// Add menu items to the end of this submenu. It calls [`Submenu::append`] in a loop internally.
pub fn append_items(&self, items: &[&dyn IsMenuItem<R>]) -> crate::Result<()> {
for item in items {
self.append(*item)?
}
Ok(())
}
/// Add a menu item to the beginning of this submenu.
pub fn prepend(&self, item: &dyn IsMenuItem<R>) -> crate::Result<()> {
let kind = item.kind();
run_item_main_thread!(self, |self_: Self| {
(*self_.0).as_ref().prepend(kind.inner().inner_muda())
})?
.map_err(Into::into)
}
/// Add menu items to the beginning of this submenu. It calls [`Submenu::insert_items`] with position of `0` internally.
pub fn prepend_items(&self, items: &[&dyn IsMenuItem<R>]) -> crate::Result<()> {
self.insert_items(items, 0)
}
/// Insert a menu item at the specified `position` in this submenu.
pub fn insert(&self, item: &dyn IsMenuItem<R>, position: usize) -> crate::Result<()> {
let kind = item.kind();
run_item_main_thread!(self, |self_: Self| {
(*self_.0)
.as_ref()
.insert(kind.inner().inner_muda(), position)
})?
.map_err(Into::into)
}
/// Insert menu items at the specified `position` in this submenu.
pub fn insert_items(&self, items: &[&dyn IsMenuItem<R>], position: usize) -> crate::Result<()> {
for (i, item) in items.iter().enumerate() {
self.insert(*item, position + i)?
}
Ok(())
}
/// Remove a menu item from this submenu.
pub fn remove(&self, item: &dyn IsMenuItem<R>) -> crate::Result<()> {
let kind = item.kind();
run_item_main_thread!(self, |self_: Self| {
(*self_.0).as_ref().remove(kind.inner().inner_muda())
})?
.map_err(Into::into)
}
/// Remove the menu item at the specified position from this submenu and returns it.
pub fn remove_at(&self, position: usize) -> crate::Result<Option<MenuItemKind<R>>> {
run_item_main_thread!(self, |self_: Self| {
(*self_.0)
.as_ref()
.remove_at(position)
.map(|i| MenuItemKind::from_muda(self_.0.app_handle.clone(), i))
})
}
/// Retrieves the menu item matching the given identifier.
pub fn get<'a, I>(&self, id: &'a I) -> Option<MenuItemKind<R>>
where
I: ?Sized,
MenuId: PartialEq<&'a I>,
{
self
.items()
.unwrap_or_default()
.into_iter()
.find(|i| i.id() == &id)
}
/// Returns a list of menu items that has been added to this submenu.
pub fn items(&self) -> crate::Result<Vec<MenuItemKind<R>>> {
run_item_main_thread!(self, |self_: Self| {
(*self_.0)
.as_ref()
.items()
.into_iter()
.map(|i| MenuItemKind::from_muda(self_.0.app_handle.clone(), i))
.collect::<Vec<_>>()
})
}
/// Get the text for this submenu.
pub fn text(&self) -> crate::Result<String> {
run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().text())
}
/// Set the text for this submenu. `text` could optionally contain
/// an `&` before a character to assign this character as the mnemonic
/// for this submenu. To display a `&` without assigning a mnemonic, use `&&`.
pub fn set_text<S: AsRef<str>>(&self, text: S) -> crate::Result<()> {
let text = text.as_ref().to_string();
run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().set_text(text))
}
/// Get whether this submenu is enabled or not.
pub fn is_enabled(&self) -> crate::Result<bool> {
run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().is_enabled())
}
/// Enable or disable this submenu.
pub fn set_enabled(&self, enabled: bool) -> crate::Result<()> {
run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().set_enabled(enabled))
}
/// Set this submenu as the Window menu for the application on macOS.
///
/// This will cause macOS to automatically add window-switching items and
/// certain other items to the menu.
#[cfg(target_os = "macos")]
pub fn set_as_windows_menu_for_nsapp(&self) -> crate::Result<()> {
run_item_main_thread!(self, |self_: Self| {
(*self_.0).as_ref().set_as_windows_menu_for_nsapp()
})?;
Ok(())
}
/// Set this submenu as the Help menu for the application on macOS.
///
/// This will cause macOS to automatically add a search box to the menu.
///
/// If no menu is set as the Help menu, macOS will automatically use any menu
/// which has a title matching the localized word "Help".
#[cfg(target_os = "macos")]
pub fn set_as_help_menu_for_nsapp(&self) -> crate::Result<()> {
run_item_main_thread!(self, |self_: Self| {
(*self_.0).as_ref().set_as_help_menu_for_nsapp()
})?;
Ok(())
}
/// Change this submenu icon or remove it.
pub fn set_icon(&self, icon: Option<crate::image::Image<'_>>) -> crate::Result<()> {
let icon = match icon {
Some(i) => Some(i.try_into()?),
None => None,
};
run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().set_icon(icon))
}
/// Change this submenu icon to a native image or remove it.
///
/// ## Platform-specific:
///
/// - **Windows / Linux**: Unsupported.
pub fn set_native_icon(&self, _icon: Option<NativeIcon>) -> crate::Result<()> {
#[cfg(target_os = "macos")]
return run_item_main_thread!(self, |self_: Self| {
(*self_.0).as_ref().set_native_icon(_icon.map(Into::into))
});
#[allow(unreachable_code)]
Ok(())
}
}

View File

@@ -0,0 +1,187 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use super::Result;
use crate::{Runtime, plugin::PluginHandle};
use std::path::{Path, PathBuf};
/// A helper class to access the mobile path APIs.
pub struct PathResolver<R: Runtime>(pub(crate) PluginHandle<R>);
impl<R: Runtime> Clone for PathResolver<R> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
#[derive(serde::Deserialize)]
struct PathResponse {
path: PathBuf,
}
#[derive(serde::Serialize)]
struct GetFileNameFromUriRequest<'a> {
uri: &'a str,
}
#[derive(serde::Deserialize)]
struct GetFileNameFromUriResponse {
name: Option<String>,
}
impl<R: Runtime> PathResolver<R> {
/// Returns the final component of the `Path`, if there is one.
///
/// If the path is a normal file, this is the file name. If it's the path of a directory, this
/// is the directory name.
///
/// Returns [`None`] if the path terminates in `..`.
///
/// On Android this also supports checking the file name of content URIs, such as the values returned by the dialog plugin.
///
/// If you are dealing with plain file system paths or not worried about Android content URIs, prefer [`Path::file_name`].
pub fn file_name(&self, path: &str) -> Option<String> {
if path.starts_with("content://") || path.starts_with("file://") {
self
.0
.run_mobile_plugin::<GetFileNameFromUriResponse>(
"getFileNameFromUri",
GetFileNameFromUriRequest { uri: path },
)
.map(|r| r.name)
.unwrap_or_else(|e| {
log::error!("failed to get file name from URI: {e}");
None
})
} else {
Path::new(path)
.file_name()
.map(|name| name.to_string_lossy().into_owned())
}
}
fn call_resolve(&self, dir: &str) -> Result<PathBuf> {
self
.0
.run_mobile_plugin::<PathResponse>(dir, ())
.map(|r| r.path)
.map_err(Into::into)
}
/// Returns the path to the user's audio directory.
pub fn audio_dir(&self) -> Result<PathBuf> {
self.call_resolve("getAudioDir")
}
/// Returns the path to the user's cache directory.
pub fn cache_dir(&self) -> Result<PathBuf> {
self.call_resolve("getExternalCacheDir")
}
/// Returns the path to the user's config directory.
pub fn config_dir(&self) -> Result<PathBuf> {
self.call_resolve("getConfigDir")
}
/// Returns the path to the user's data directory.
pub fn data_dir(&self) -> Result<PathBuf> {
self.call_resolve("getDataDir")
}
/// Returns the path to the user's local data directory.
pub fn local_data_dir(&self) -> Result<PathBuf> {
self.call_resolve("getDataDir")
}
/// Returns the path to the user's document directory.
pub fn document_dir(&self) -> Result<PathBuf> {
self.call_resolve("getDocumentDir")
}
/// Returns the path to the user's download directory.
pub fn download_dir(&self) -> Result<PathBuf> {
self.call_resolve("getDownloadDir")
}
/// Returns the path to the user's picture directory.
pub fn picture_dir(&self) -> Result<PathBuf> {
self.call_resolve("getPictureDir")
}
/// Returns the path to the user's public directory.
pub fn public_dir(&self) -> Result<PathBuf> {
self.call_resolve("getPublicDir")
}
/// Returns the path to the user's video directory.
///
/// Resolves to the app-specific Movies directory (`getExternalFilesDir(DIRECTORY_MOVIES)`),
/// typically `.../files/Movies`.
///
/// ## Migration
///
/// Previously this resolved to external cache storage ([`cache_dir`], typically `.../cache`).
/// Files written to the old location will not be discovered at the new path and must be migrated
/// or paths updated accordingly.
pub fn video_dir(&self) -> Result<PathBuf> {
self.call_resolve("getVideoDir")
}
/// Returns the path to the resource directory of this app.
pub fn resource_dir(&self) -> Result<PathBuf> {
self.call_resolve("getResourcesDir")
}
/// Returns the path to the suggested directory for your app's config files.
///
/// Resolves to [`config_dir`]`/${bundle_identifier}`.
pub fn app_config_dir(&self) -> Result<PathBuf> {
self.call_resolve("getConfigDir")
}
/// Returns the path to the suggested directory for your app's data files.
///
/// Resolves to [`data_dir`]`/${bundle_identifier}`.
pub fn app_data_dir(&self) -> Result<PathBuf> {
self.call_resolve("getDataDir")
}
/// Returns the path to the suggested directory for your app's local data files.
///
/// Resolves to [`local_data_dir`]`/${bundle_identifier}`.
pub fn app_local_data_dir(&self) -> Result<PathBuf> {
self.call_resolve("getDataDir")
}
/// Returns the path to the suggested directory for your app's cache files.
///
/// Resolves to [`cache_dir`]`/${bundle_identifier}`.
pub fn app_cache_dir(&self) -> Result<PathBuf> {
self.call_resolve("getCacheDir")
}
/// Returns the path to the suggested directory for your app's log files.
pub fn app_log_dir(&self) -> Result<PathBuf> {
self
.call_resolve("getConfigDir")
.map(|dir| dir.join("logs"))
}
/// A temporary directory. Resolves to [`std::env::temp_dir`].
pub fn temp_dir(&self) -> Result<PathBuf> {
Ok(std::env::temp_dir())
}
/// Returns the path to the user's home directory.
///
/// ## Platform-specific
///
/// - **Linux:** Resolves to `$HOME`.
/// - **macOS:** Resolves to `$HOME`.
/// - **Windows:** Resolves to `{FOLDERID_Profile}`.
/// - **iOS**: Cannot be written to directly, use one of the app paths instead.
pub fn home_dir(&self) -> Result<PathBuf> {
self.call_resolve("getHomeDir")
}
}

View File

@@ -0,0 +1,296 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use super::{Error, Result};
use crate::{AppHandle, Manager, Runtime};
use std::path::{Path, PathBuf};
/// The path resolver is a helper class for general and application-specific path APIs.
pub struct PathResolver<R: Runtime>(pub(crate) AppHandle<R>);
impl<R: Runtime> Clone for PathResolver<R> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<R: Runtime> PathResolver<R> {
/// Returns the final component of the `Path`, if there is one.
///
/// If the path is a normal file, this is the file name. If it's the path of a directory, this
/// is the directory name.
///
/// Returns [`None`] if the path terminates in `..`.
///
/// On Android this also supports checking the file name of content URIs, such as the values returned by the dialog plugin.
///
/// If you are dealing with plain file system paths or not worried about Android content URIs, prefer [`Path::file_name`].
pub fn file_name(&self, path: &str) -> Option<String> {
Path::new(path)
.file_name()
.map(|name| name.to_string_lossy().into_owned())
}
/// Returns the path to the user's audio directory.
///
/// ## Platform-specific
///
/// - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_MUSIC_DIR`.
/// - **macOS:** Resolves to `$HOME/Music`.
/// - **Windows:** Resolves to `{FOLDERID_Music}`.
pub fn audio_dir(&self) -> Result<PathBuf> {
dirs::audio_dir().ok_or(Error::UnknownPath)
}
/// Returns the path to the user's cache directory.
///
/// ## Platform-specific
///
/// - **Linux:** Resolves to `$XDG_CACHE_HOME` or `$HOME/.cache`.
/// - **macOS:** Resolves to `$HOME/Library/Caches`.
/// - **Windows:** Resolves to `{FOLDERID_LocalAppData}`.
pub fn cache_dir(&self) -> Result<PathBuf> {
dirs::cache_dir().ok_or(Error::UnknownPath)
}
/// Returns the path to the user's config directory.
///
/// ## Platform-specific
///
/// - **Linux:** Resolves to `$XDG_CONFIG_HOME` or `$HOME/.config`.
/// - **macOS:** Resolves to `$HOME/Library/Application Support`.
/// - **Windows:** Resolves to `{FOLDERID_RoamingAppData}`.
pub fn config_dir(&self) -> Result<PathBuf> {
dirs::config_dir().ok_or(Error::UnknownPath)
}
/// Returns the path to the user's data directory.
///
/// ## Platform-specific
///
/// - **Linux:** Resolves to `$XDG_DATA_HOME` or `$HOME/.local/share`.
/// - **macOS:** Resolves to `$HOME/Library/Application Support`.
/// - **Windows:** Resolves to `{FOLDERID_RoamingAppData}`.
pub fn data_dir(&self) -> Result<PathBuf> {
dirs::data_dir().ok_or(Error::UnknownPath)
}
/// Returns the path to the user's local data directory.
///
/// ## Platform-specific
///
/// - **Linux:** Resolves to `$XDG_DATA_HOME` or `$HOME/.local/share`.
/// - **macOS:** Resolves to `$HOME/Library/Application Support`.
/// - **Windows:** Resolves to `{FOLDERID_LocalAppData}`.
pub fn local_data_dir(&self) -> Result<PathBuf> {
dirs::data_local_dir().ok_or(Error::UnknownPath)
}
/// Returns the path to the user's desktop directory.
///
/// ## Platform-specific
///
/// - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_DESKTOP_DIR`.
/// - **macOS:** Resolves to `$HOME/Desktop`.
/// - **Windows:** Resolves to `{FOLDERID_Desktop}`.
pub fn desktop_dir(&self) -> Result<PathBuf> {
dirs::desktop_dir().ok_or(Error::UnknownPath)
}
/// Returns the path to the user's document directory.
///
/// ## Platform-specific
///
/// - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_DOCUMENTS_DIR`.
/// - **macOS:** Resolves to `$HOME/Documents`.
/// - **Windows:** Resolves to `{FOLDERID_Documents}`.
pub fn document_dir(&self) -> Result<PathBuf> {
dirs::document_dir().ok_or(Error::UnknownPath)
}
/// Returns the path to the user's download directory.
///
/// ## Platform-specific
///
/// - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_DOWNLOAD_DIR`.
/// - **macOS:** Resolves to `$HOME/Downloads`.
/// - **Windows:** Resolves to `{FOLDERID_Downloads}`.
pub fn download_dir(&self) -> Result<PathBuf> {
dirs::download_dir().ok_or(Error::UnknownPath)
}
/// Returns the path to the user's executable directory.
///
/// ## Platform-specific
///
/// - **Linux:** Resolves to `$XDG_BIN_HOME/../bin` or `$XDG_DATA_HOME/../bin` or `$HOME/.local/bin`.
/// - **macOS:** Not supported.
/// - **Windows:** Not supported.
pub fn executable_dir(&self) -> Result<PathBuf> {
dirs::executable_dir().ok_or(Error::UnknownPath)
}
/// Returns the path to the user's font directory.
///
/// ## Platform-specific
///
/// - **Linux:** Resolves to `$XDG_DATA_HOME/fonts` or `$HOME/.local/share/fonts`.
/// - **macOS:** Resolves to `$HOME/Library/Fonts`.
/// - **Windows:** Not supported.
pub fn font_dir(&self) -> Result<PathBuf> {
dirs::font_dir().ok_or(Error::UnknownPath)
}
/// Returns the path to the user's home directory.
///
/// ## Platform-specific
///
/// - **Linux:** Resolves to `$HOME`.
/// - **macOS:** Resolves to `$HOME`.
/// - **Windows:** Resolves to `{FOLDERID_Profile}`.
/// - **iOS**: Cannot be written to directly, use one of the app paths instead.
pub fn home_dir(&self) -> Result<PathBuf> {
dirs::home_dir().ok_or(Error::UnknownPath)
}
/// Returns the path to the user's picture directory.
///
/// ## Platform-specific
///
/// - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_PICTURES_DIR`.
/// - **macOS:** Resolves to `$HOME/Pictures`.
/// - **Windows:** Resolves to `{FOLDERID_Pictures}`.
pub fn picture_dir(&self) -> Result<PathBuf> {
dirs::picture_dir().ok_or(Error::UnknownPath)
}
/// Returns the path to the user's public directory.
///
/// ## Platform-specific
///
/// - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_PUBLICSHARE_DIR`.
/// - **macOS:** Resolves to `$HOME/Public`.
/// - **Windows:** Resolves to `{FOLDERID_Public}`.
pub fn public_dir(&self) -> Result<PathBuf> {
dirs::public_dir().ok_or(Error::UnknownPath)
}
/// Returns the path to the user's runtime directory.
///
/// ## Platform-specific
///
/// - **Linux:** Resolves to `$XDG_RUNTIME_DIR`.
/// - **macOS:** Not supported.
/// - **Windows:** Not supported.
pub fn runtime_dir(&self) -> Result<PathBuf> {
dirs::runtime_dir().ok_or(Error::UnknownPath)
}
/// Returns the path to the user's template directory.
///
/// ## Platform-specific
///
/// - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_TEMPLATES_DIR`.
/// - **macOS:** Not supported.
/// - **Windows:** Resolves to `{FOLDERID_Templates}`.
pub fn template_dir(&self) -> Result<PathBuf> {
dirs::template_dir().ok_or(Error::UnknownPath)
}
/// Returns the path to the user's video dir
///
/// ## Platform-specific
///
/// - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_VIDEOS_DIR`.
/// - **macOS:** Resolves to `$HOME/Movies`.
/// - **Windows:** Resolves to `{FOLDERID_Videos}`.
pub fn video_dir(&self) -> Result<PathBuf> {
dirs::video_dir().ok_or(Error::UnknownPath)
}
/// Returns the path to the resource directory of this app.
///
/// ## Platform-specific
///
/// Although we provide the exact path where this function resolves to,
/// this is not a contract and things might change in the future
///
/// - **Windows:** Resolves to the directory that contains the main executable.
/// - **Linux:** When running in an AppImage, the `APPDIR` variable will be set to
/// the mounted location of the app, and the resource dir will be `${APPDIR}/usr/lib/${exe_name}`.
/// If not running in an AppImage, the path is `/usr/lib/${exe_name}`.
/// When running the app from `src-tauri/target/(debug|release)/`, the path is `${exe_dir}/../lib/${exe_name}`.
/// - **macOS:** Resolves to `${exe_dir}/../Resources` (inside .app).
/// - **iOS:** Resolves to `${exe_dir}/assets`.
/// - **Android:** Currently the resources are stored in the APK as assets so it's not a normal file system path,
/// we return a special URI prefix `asset://localhost/` here that can be used with the [file system plugin](https://tauri.app/plugin/file-system/),
/// with that, you can read the files through [`FsExt::fs`](https://docs.rs/tauri-plugin-fs/latest/tauri_plugin_fs/trait.FsExt.html#tymethod.fs)
/// like this: `app.fs().read_to_string(app.path().resource_dir().unwrap().join("resource"));`
pub fn resource_dir(&self) -> Result<PathBuf> {
crate::utils::platform::resource_dir(self.0.package_info(), &self.0.env())
.map_err(|_| Error::UnknownPath)
}
/// Returns the path to the suggested directory for your app's config files.
///
/// Resolves to [`config_dir`](Self::config_dir)`/${bundle_identifier}`.
pub fn app_config_dir(&self) -> Result<PathBuf> {
dirs::config_dir()
.ok_or(Error::UnknownPath)
.map(|dir| dir.join(&self.0.config().identifier))
}
/// Returns the path to the suggested directory for your app's data files.
///
/// Resolves to [`data_dir`](Self::data_dir)`/${bundle_identifier}`.
pub fn app_data_dir(&self) -> Result<PathBuf> {
dirs::data_dir()
.ok_or(Error::UnknownPath)
.map(|dir| dir.join(&self.0.config().identifier))
}
/// Returns the path to the suggested directory for your app's local data files.
///
/// Resolves to [`local_data_dir`](Self::local_data_dir)`/${bundle_identifier}`.
pub fn app_local_data_dir(&self) -> Result<PathBuf> {
dirs::data_local_dir()
.ok_or(Error::UnknownPath)
.map(|dir| dir.join(&self.0.config().identifier))
}
/// Returns the path to the suggested directory for your app's cache files.
///
/// Resolves to [`cache_dir`](Self::cache_dir)`/${bundle_identifier}`.
pub fn app_cache_dir(&self) -> Result<PathBuf> {
dirs::cache_dir()
.ok_or(Error::UnknownPath)
.map(|dir| dir.join(&self.0.config().identifier))
}
/// Returns the path to the suggested directory for your app's log files.
///
/// ## Platform-specific
///
/// - **Linux:** Resolves to [`local_data_dir`](Self::local_data_dir)`/${bundle_identifier}/logs`.
/// - **macOS:** Resolves to [`home_dir`](Self::home_dir)`/Library/Logs/${bundle_identifier}`
/// - **Windows:** Resolves to [`local_data_dir`](Self::local_data_dir)`/${bundle_identifier}/logs`.
pub fn app_log_dir(&self) -> Result<PathBuf> {
#[cfg(target_os = "macos")]
let path = dirs::home_dir()
.ok_or(Error::UnknownPath)
.map(|dir| dir.join("Library/Logs").join(&self.0.config().identifier));
#[cfg(not(target_os = "macos"))]
let path = dirs::data_local_dir()
.ok_or(Error::UnknownPath)
.map(|dir| dir.join(&self.0.config().identifier).join("logs"));
path
}
/// A temporary directory. Resolves to [`std::env::temp_dir`].
pub fn temp_dir(&self) -> Result<PathBuf> {
Ok(std::env::temp_dir())
}
}

View File

@@ -0,0 +1,10 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
Object.defineProperty(window.__TAURI_INTERNALS__.plugins, 'path', {
value: {
sep: __TEMPLATE_sep__,
delimiter: __TEMPLATE_delimiter__
}
})

View File

@@ -0,0 +1,386 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::{
path::{Component, Display, Path, PathBuf},
str::FromStr,
};
use crate::Runtime;
use serde::{Deserialize, Deserializer, Serialize, de::Error as DeError};
use serde_repr::{Deserialize_repr, Serialize_repr};
pub(crate) mod plugin;
use crate::error::*;
#[cfg(target_os = "android")]
mod android;
#[cfg(not(target_os = "android"))]
mod desktop;
#[cfg(target_os = "android")]
pub use android::PathResolver;
#[cfg(not(target_os = "android"))]
pub use desktop::PathResolver;
/// A wrapper for [`PathBuf`] that prevents path traversal.
///
/// # Examples
///
/// ```
/// # use tauri::path::SafePathBuf;
/// assert!(SafePathBuf::new("../secret.txt".into()).is_err());
/// assert!(SafePathBuf::new("/home/user/stuff/../secret.txt".into()).is_err());
///
/// assert!(SafePathBuf::new("./file.txt".into()).is_ok());
/// assert!(SafePathBuf::new("/home/user/secret.txt".into()).is_ok());
/// ```
#[derive(Clone, Debug, Serialize)]
pub struct SafePathBuf(PathBuf);
impl SafePathBuf {
/// Validates the path for directory traversal vulnerabilities and returns a new [`SafePathBuf`] instance if it is safe.
pub fn new(path: PathBuf) -> std::result::Result<Self, &'static str> {
if path.components().any(|x| matches!(x, Component::ParentDir)) {
return Err("cannot traverse directory, rewrite the path without the use of `../`");
}
Ok(Self(path))
}
/// Returns an object that implements [`std::fmt::Display`] for safely printing paths.
///
/// See [`PathBuf#method.display`] for more information.
pub fn display(&self) -> Display<'_> {
self.0.display()
}
}
impl AsRef<Path> for SafePathBuf {
fn as_ref(&self) -> &Path {
self.0.as_ref()
}
}
impl FromStr for SafePathBuf {
type Err = &'static str;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
Self::new(s.into())
}
}
impl From<SafePathBuf> for PathBuf {
fn from(path: SafePathBuf) -> Self {
path.0
}
}
impl<'de> Deserialize<'de> for SafePathBuf {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let path = PathBuf::deserialize(deserializer)?;
SafePathBuf::new(path).map_err(DeError::custom)
}
}
/// A base directory for a path.
///
/// The base directory is the optional root of a file system operation.
/// If informed by the API call, all paths will be relative to the path of the given directory.
///
/// For more information, check the [`dirs` documentation](https://docs.rs/dirs/).
#[derive(Serialize_repr, Deserialize_repr, Clone, Copy, Debug)]
#[repr(u16)]
#[non_exhaustive]
pub enum BaseDirectory {
/// The Audio directory.
/// Resolves to [`crate::path::PathResolver::audio_dir`].
Audio = 1,
/// The Cache directory.
/// Resolves to [`crate::path::PathResolver::cache_dir`].
Cache = 2,
/// The Config directory.
/// Resolves to [`crate::path::PathResolver::config_dir`].
Config = 3,
/// The Data directory.
/// Resolves to [`crate::path::PathResolver::data_dir`].
Data = 4,
/// The LocalData directory.
/// Resolves to [`crate::path::PathResolver::local_data_dir`].
LocalData = 5,
/// The Document directory.
/// Resolves to [`crate::path::PathResolver::document_dir`].
Document = 6,
/// The Download directory.
/// Resolves to [`crate::path::PathResolver::download_dir`].
Download = 7,
/// The Picture directory.
/// Resolves to [`crate::path::PathResolver::picture_dir`].
Picture = 8,
/// The Public directory.
/// Resolves to [`crate::path::PathResolver::public_dir`].
Public = 9,
/// The Video directory.
/// Resolves to [`crate::path::PathResolver::video_dir`].
Video = 10,
/// The Resource directory.
/// Resolves to [`crate::path::PathResolver::resource_dir`].
Resource = 11,
/// A temporary directory.
/// Resolves to [`std::env::temp_dir`].
Temp = 12,
/// The default app config directory.
/// Resolves to [`BaseDirectory::Config`]`/{bundle_identifier}`.
AppConfig = 13,
/// The default app data directory.
/// Resolves to [`BaseDirectory::Data`]`/{bundle_identifier}`.
AppData = 14,
/// The default app local data directory.
/// Resolves to [`BaseDirectory::LocalData`]`/{bundle_identifier}`.
AppLocalData = 15,
/// The default app cache directory.
/// Resolves to [`BaseDirectory::Cache`]`/{bundle_identifier}`.
AppCache = 16,
/// The default app log directory.
/// Resolves to [`BaseDirectory::Home`]`/Library/Logs/{bundle_identifier}` on macOS
/// and [`BaseDirectory::Config`]`/{bundle_identifier}/logs` on linux and Windows.
AppLog = 17,
/// The Desktop directory.
/// Resolves to [`crate::path::PathResolver::desktop_dir`].
#[cfg(not(target_os = "android"))]
Desktop = 18,
/// The Executable directory.
/// Resolves to [`crate::path::PathResolver::executable_dir`].
#[cfg(not(target_os = "android"))]
Executable = 19,
/// The Font directory.
/// Resolves to [`crate::path::PathResolver::font_dir`].
#[cfg(not(target_os = "android"))]
Font = 20,
/// The Home directory.
/// Resolves to [`crate::path::PathResolver::home_dir`].
Home = 21,
/// The Runtime directory.
/// Resolves to [`crate::path::PathResolver::runtime_dir`].
#[cfg(not(target_os = "android"))]
Runtime = 22,
/// The Template directory.
/// Resolves to [`crate::path::PathResolver::template_dir`].
#[cfg(not(target_os = "android"))]
Template = 23,
}
impl BaseDirectory {
/// Gets the variable that represents this [`BaseDirectory`] for string paths.
pub fn variable(self) -> &'static str {
match self {
Self::Audio => "$AUDIO",
Self::Cache => "$CACHE",
Self::Config => "$CONFIG",
Self::Data => "$DATA",
Self::LocalData => "$LOCALDATA",
Self::Document => "$DOCUMENT",
Self::Download => "$DOWNLOAD",
Self::Picture => "$PICTURE",
Self::Public => "$PUBLIC",
Self::Video => "$VIDEO",
Self::Resource => "$RESOURCE",
Self::Temp => "$TEMP",
Self::AppConfig => "$APPCONFIG",
Self::AppData => "$APPDATA",
Self::AppLocalData => "$APPLOCALDATA",
Self::AppCache => "$APPCACHE",
Self::AppLog => "$APPLOG",
Self::Home => "$HOME",
#[cfg(not(target_os = "android"))]
Self::Desktop => "$DESKTOP",
#[cfg(not(target_os = "android"))]
Self::Executable => "$EXE",
#[cfg(not(target_os = "android"))]
Self::Font => "$FONT",
#[cfg(not(target_os = "android"))]
Self::Runtime => "$RUNTIME",
#[cfg(not(target_os = "android"))]
Self::Template => "$TEMPLATE",
}
}
/// Gets the [`BaseDirectory`] associated with the given variable, or [`None`] if the variable doesn't match any.
pub fn from_variable(variable: &str) -> Option<Self> {
let res = match variable {
"$AUDIO" => Self::Audio,
"$CACHE" => Self::Cache,
"$CONFIG" => Self::Config,
"$DATA" => Self::Data,
"$LOCALDATA" => Self::LocalData,
"$DOCUMENT" => Self::Document,
"$DOWNLOAD" => Self::Download,
"$PICTURE" => Self::Picture,
"$PUBLIC" => Self::Public,
"$VIDEO" => Self::Video,
"$RESOURCE" => Self::Resource,
"$TEMP" => Self::Temp,
"$APPCONFIG" => Self::AppConfig,
"$APPDATA" => Self::AppData,
"$APPLOCALDATA" => Self::AppLocalData,
"$APPCACHE" => Self::AppCache,
"$APPLOG" => Self::AppLog,
"$HOME" => Self::Home,
#[cfg(not(target_os = "android"))]
"$DESKTOP" => Self::Desktop,
#[cfg(not(target_os = "android"))]
"$EXE" => Self::Executable,
#[cfg(not(target_os = "android"))]
"$FONT" => Self::Font,
#[cfg(not(target_os = "android"))]
"$RUNTIME" => Self::Runtime,
#[cfg(not(target_os = "android"))]
"$TEMPLATE" => Self::Template,
_ => return None,
};
Some(res)
}
}
impl<R: Runtime> PathResolver<R> {
/// Resolves the path with the base directory.
///
/// # Examples
///
/// ```rust,no_run
/// use tauri::{path::BaseDirectory, Manager};
/// tauri::Builder::default()
/// .setup(|app| {
/// let path = app.path().resolve("path/to/something", BaseDirectory::Config)?;
/// assert_eq!(path.to_str().unwrap(), "/home/${whoami}/.config/path/to/something");
/// Ok(())
/// });
/// ```
pub fn resolve<P: AsRef<Path>>(&self, path: P, base_directory: BaseDirectory) -> Result<PathBuf> {
resolve_path::<R>(self, base_directory, Some(path.as_ref().to_path_buf()))
}
/// Parse the given path, resolving a [`BaseDirectory`] variable if the path starts with one.
///
/// # Examples
///
/// ```rust,no_run
/// use tauri::Manager;
/// tauri::Builder::default()
/// .setup(|app| {
/// let path = app.path().parse("$HOME/.bashrc")?;
/// assert_eq!(path.to_str().unwrap(), "/home/${whoami}/.bashrc");
/// Ok(())
/// });
/// ```
pub fn parse<P: AsRef<Path>>(&self, path: P) -> Result<PathBuf> {
let mut p = PathBuf::new();
let mut components = path.as_ref().components();
match components.next() {
Some(Component::Normal(str)) => {
if let Some(base_directory) = BaseDirectory::from_variable(&str.to_string_lossy()) {
p.push(resolve_path::<R>(self, base_directory, None)?);
} else {
p.push(str);
}
}
Some(component) => p.push(component),
None => (),
}
for component in components {
if let Component::ParentDir = component {
continue;
}
p.push(component);
}
Ok(p)
}
}
fn resolve_path<R: Runtime>(
resolver: &PathResolver<R>,
directory: BaseDirectory,
path: Option<PathBuf>,
) -> Result<PathBuf> {
let resolve_resource = matches!(directory, BaseDirectory::Resource);
let mut base_dir_path = match directory {
BaseDirectory::Audio => resolver.audio_dir(),
BaseDirectory::Cache => resolver.cache_dir(),
BaseDirectory::Config => resolver.config_dir(),
BaseDirectory::Data => resolver.data_dir(),
BaseDirectory::LocalData => resolver.local_data_dir(),
BaseDirectory::Document => resolver.document_dir(),
BaseDirectory::Download => resolver.download_dir(),
BaseDirectory::Picture => resolver.picture_dir(),
BaseDirectory::Public => resolver.public_dir(),
BaseDirectory::Video => resolver.video_dir(),
BaseDirectory::Resource => resolver.resource_dir(),
BaseDirectory::Temp => resolver.temp_dir(),
BaseDirectory::AppConfig => resolver.app_config_dir(),
BaseDirectory::AppData => resolver.app_data_dir(),
BaseDirectory::AppLocalData => resolver.app_local_data_dir(),
BaseDirectory::AppCache => resolver.app_cache_dir(),
BaseDirectory::AppLog => resolver.app_log_dir(),
BaseDirectory::Home => resolver.home_dir(),
#[cfg(not(target_os = "android"))]
BaseDirectory::Desktop => resolver.desktop_dir(),
#[cfg(not(target_os = "android"))]
BaseDirectory::Executable => resolver.executable_dir(),
#[cfg(not(target_os = "android"))]
BaseDirectory::Font => resolver.font_dir(),
#[cfg(not(target_os = "android"))]
BaseDirectory::Runtime => resolver.runtime_dir(),
#[cfg(not(target_os = "android"))]
BaseDirectory::Template => resolver.template_dir(),
}?;
if let Some(path) = path {
// use the same path resolution mechanism as the bundler's resource injection algorithm
if resolve_resource {
let mut resource_path = PathBuf::new();
for component in path.components() {
match component {
Component::Prefix(_) => {}
Component::RootDir => resource_path.push("_root_"),
Component::CurDir => {}
Component::ParentDir => resource_path.push("_up_"),
Component::Normal(p) => resource_path.push(p),
}
}
base_dir_path.push(resource_path);
} else {
base_dir_path.push(path);
}
}
Ok(base_dir_path)
}
#[cfg(test)]
mod test {
use super::SafePathBuf;
use quickcheck::{Arbitrary, Gen};
use std::path::PathBuf;
impl Arbitrary for SafePathBuf {
fn arbitrary(g: &mut Gen) -> Self {
Self(PathBuf::arbitrary(g))
}
fn shrink(&self) -> Box<dyn Iterator<Item = Self>> {
Box::new(self.0.shrink().map(SafePathBuf))
}
}
}

View File

@@ -0,0 +1,315 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::path::{Component, MAIN_SEPARATOR, Path, PathBuf};
use serialize_to_javascript::{DefaultTemplate, Template, default_template};
use super::{BaseDirectory, Error, PathResolver, Result};
use crate::{
AppHandle, Manager, Runtime, State, command,
plugin::{Builder, TauriPlugin},
};
/// Normalize a path, removing things like `.` and `..`, this snippet is taken from cargo's paths util.
/// <https://github.com/rust-lang/cargo/blob/46fa867ff7043e3a0545bf3def7be904e1497afd/crates/cargo-util/src/paths.rs#L73-L106>
fn normalize_path(path: &Path) -> PathBuf {
let mut components = path.components().peekable();
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().copied() {
components.next();
PathBuf::from(c.as_os_str())
} else {
PathBuf::new()
};
for component in components {
match component {
Component::Prefix(..) => unreachable!(),
Component::RootDir => {
ret.push(component.as_os_str());
}
Component::CurDir => {}
Component::ParentDir => {
ret.pop();
}
Component::Normal(c) => {
ret.push(c);
}
}
}
ret
}
/// Normalize a path, removing things like `.` and `..`, this snippet is taken from cargo's paths util but
/// slightly modified to not resolve absolute paths.
/// <https://github.com/rust-lang/cargo/blob/46fa867ff7043e3a0545bf3def7be904e1497afd/crates/cargo-util/src/paths.rs#L73-L106>
fn normalize_path_no_absolute(path: &Path) -> PathBuf {
let mut components = path.components().peekable();
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().copied() {
components.next();
PathBuf::from(c.as_os_str())
} else {
PathBuf::new()
};
for component in components {
match component {
Component::Prefix(..) => unreachable!(),
Component::RootDir => {
ret.push(component.as_os_str());
}
Component::CurDir => {}
Component::ParentDir => {
ret.pop();
}
Component::Normal(c) => {
// Using PathBuf::push here will replace the whole path if an absolute path is encountered
// which is not the intended behavior, so instead of that, convert the current resolved path
// to a string and do simple string concatenation with the current component then convert it
// back to a PathBuf
let mut p = ret.to_string_lossy().to_string();
// Only add a separator if it doesn't have one already or if current normalized path is empty,
// this ensures it won't have an unwanted leading separator
if !p.is_empty() && !p.ends_with('/') && !p.ends_with('\\') {
p.push(MAIN_SEPARATOR);
}
if let Some(c) = c.to_str() {
p.push_str(c);
}
ret = PathBuf::from(p);
}
}
}
ret
}
#[command(root = "crate")]
pub fn resolve_directory<R: Runtime>(
_app: AppHandle<R>,
resolver: State<'_, PathResolver<R>>,
directory: BaseDirectory,
path: Option<PathBuf>,
) -> Result<PathBuf> {
super::resolve_path(&resolver, directory, path).map(|p| dunce::simplified(&p).to_path_buf())
}
#[command(root = "crate")]
pub fn resolve(paths: Vec<String>) -> Result<PathBuf> {
// Start with current directory then start adding paths from the vector one by one using `PathBuf.push()` which
// will ensure that if an absolute path is encountered in the iteration, it will be used as the current full path.
//
// examples:
// 1. `vec!["."]` or `vec![]` will be equal to `std::env::current_dir()`
// 2. `vec!["/foo/bar", "/tmp/file", "baz"]` will be equal to `PathBuf::from("/tmp/file/baz")`
let mut path = std::env::current_dir().map_err(Error::CurrentDir)?;
for p in paths {
path.push(p);
}
Ok(dunce::simplified(&normalize_path(&path)).to_path_buf())
}
#[command(root = "crate")]
pub fn normalize(path: String) -> String {
let mut p = dunce::simplified(&normalize_path_no_absolute(Path::new(&path)))
.to_string_lossy()
.to_string();
// Node.js behavior is to return `".."` for `normalize("..")`
// and `"."` for `normalize("")` or `normalize(".")`
if p.is_empty() && path == ".." {
"..".into()
} else if p.is_empty() && (path.is_empty() || path == ".") {
".".into()
} else {
// Add a trailing separator if the path passed to this functions had a trailing separator. That's how Node.js behaves.
if (path.ends_with('/') || path.ends_with('\\')) && (!p.ends_with('/') || !p.ends_with('\\')) {
p.push(MAIN_SEPARATOR);
}
p
}
}
#[command(root = "crate")]
pub fn join(paths: Vec<String>) -> String {
let path = PathBuf::from(
paths
.into_iter()
.map(|mut p| {
// Add a `MAIN_SEPARATOR` if it doesn't already have one and is not an empty string.
// Doing this to ensure that the vector elements are separated in
// the resulting string so path.components() can work correctly when called
// in `normalize_path_no_absolute()` later on.
if !p.is_empty() && !p.ends_with('/') && !p.ends_with('\\') {
p.push(MAIN_SEPARATOR);
}
p
})
.collect::<String>(),
);
let p = dunce::simplified(&normalize_path_no_absolute(&path))
.to_string_lossy()
.to_string();
if p.is_empty() { ".".into() } else { p }
}
#[command(root = "crate")]
pub fn dirname(path: String) -> Result<PathBuf> {
match Path::new(&path).parent() {
Some(p) => Ok(dunce::simplified(p).to_path_buf()),
None => Err(Error::NoParent),
}
}
#[command(root = "crate")]
pub fn extname<R: Runtime>(app: AppHandle<R>, path: String) -> Result<String> {
let file_name = app.path().file_name(&path).ok_or(Error::NoExtension)?;
match Path::new(&file_name)
.extension()
.and_then(std::ffi::OsStr::to_str)
{
Some(p) => Ok(p.to_string()),
None => Err(Error::NoExtension),
}
}
#[command(root = "crate")]
pub fn basename<R: Runtime>(app: AppHandle<R>, path: &str, ext: Option<&str>) -> Result<String> {
let file_name = app.path().file_name(path);
match file_name {
Some(p) => {
let maybe_stripped = if let Some(ext) = ext {
p.strip_suffix(ext).unwrap_or(&p).to_string()
} else {
p
};
Ok(maybe_stripped)
}
None => Err(Error::NoBasename),
}
}
#[command(root = "crate")]
pub fn is_absolute(path: String) -> bool {
Path::new(&path).is_absolute()
}
#[derive(Template)]
#[default_template("./init.js")]
struct InitJavascript {
sep: &'static str,
delimiter: &'static str,
}
/// Initializes the plugin.
pub(crate) fn init<R: Runtime>() -> TauriPlugin<R> {
#[cfg(windows)]
let (sep, delimiter) = ("\\", ";");
#[cfg(not(windows))]
let (sep, delimiter) = ("/", ":");
let init_js = InitJavascript { sep, delimiter }
.render_default(&Default::default())
// this will never fail with the above sep and delimiter values
.unwrap();
Builder::new("path")
.invoke_handler(crate::generate_handler![
#![plugin(path)]
resolve_directory,
resolve,
normalize,
join,
dirname,
extname,
basename,
is_absolute
])
.js_init_script(init_js.to_string())
.setup(|app, _api| {
#[cfg(target_os = "android")]
{
let handle = _api.register_android_plugin("app.tauri", "PathPlugin")?;
app.manage(PathResolver(handle));
}
#[cfg(not(target_os = "android"))]
{
app.manage(PathResolver(app.clone()));
}
Ok(())
})
.build()
}
#[cfg(test)]
mod tests {
use crate::test::mock_app;
#[test]
fn basename() {
let app = mock_app();
let path = "/path/to/some-json-file.json";
assert_eq!(
super::basename(app.handle().clone(), path, Some(".json")).unwrap(),
"some-json-file"
);
let path = "/path/to/some-json-file.json";
assert_eq!(
super::basename(app.handle().clone(), path, Some("json")).unwrap(),
"some-json-file."
);
let path = "/path/to/some-json-file.html.json";
assert_eq!(
super::basename(app.handle().clone(), path, Some(".json")).unwrap(),
"some-json-file.html"
);
let path = "/path/to/some-json-file.json.json";
assert_eq!(
super::basename(app.handle().clone(), path, Some(".json")).unwrap(),
"some-json-file.json"
);
let path = "/path/to/some-json-file.json.html";
assert_eq!(
super::basename(app.handle().clone(), path, Some(".json")).unwrap(),
"some-json-file.json.html"
);
}
#[test]
fn join() {
fn check(paths: Vec<&str>, expected_unix: &str, expected_windows: &str) {
let expected = if cfg!(windows) {
expected_windows
} else {
expected_unix
};
let paths = paths.into_iter().map(String::from).collect();
assert_eq!(super::join(paths), expected);
}
check(vec![""], ".", ".");
check(vec!["", ""], ".", ".");
check(vec!["a"], "a", "a");
check(vec!["", "a"], "a", "a");
check(vec!["a", "b"], "a/b", r"a\b");
check(vec!["a", "", "b"], "a/b", r"a\b");
check(vec!["a", "/b", "c"], "a/b/c", r"a\b\c");
check(vec!["a", "b/c", "d"], "a/b/c/d", r"a\b\c\d");
check(vec!["a/", "b"], "a/b", r"a\b");
}
#[test]
fn normalize() {
assert_eq!(super::normalize("".into()), ".");
assert_eq!(super::normalize(".".into()), ".");
assert_eq!(super::normalize("..".into()), "..");
}
}

View File

@@ -0,0 +1,87 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
#[cfg(feature = "isolation")]
use std::sync::Arc;
use serde::Serialize;
use serialize_to_javascript::{Template, default_template};
/// The domain of the isolation iframe source.
#[cfg(feature = "isolation")]
pub const ISOLATION_IFRAME_SRC_DOMAIN: &str = "localhost";
/// An application pattern.
#[derive(Debug)]
pub enum Pattern {
/// The brownfield pattern.
Brownfield,
/// Isolation pattern. Recommended for security purposes.
#[cfg(feature = "isolation")]
Isolation {
/// The HTML served on `isolation://index.html`.
assets: Arc<tauri_utils::assets::EmbeddedAssets>,
/// The schema used for the isolation frames.
schema: String,
/// A random string used to ensure that the message went through the isolation frame.
///
/// This should be regenerated at runtime.
key: String,
/// Cryptographically secure keys
crypto_keys: Box<tauri_utils::pattern::isolation::Keys>,
},
}
/// The shape of the JavaScript Pattern config
#[derive(Debug, Serialize)]
#[serde(rename_all = "lowercase", tag = "pattern")]
pub(crate) enum PatternObject {
/// Brownfield pattern.
Brownfield,
/// Isolation pattern. Recommended for security purposes.
#[cfg(feature = "isolation")]
Isolation {
/// Which `IsolationSide` this `PatternObject` is getting injected into
side: IsolationSide,
},
}
impl From<&Pattern> for PatternObject {
fn from(pattern: &Pattern) -> Self {
match pattern {
Pattern::Brownfield => Self::Brownfield,
#[cfg(feature = "isolation")]
Pattern::Isolation { .. } => Self::Isolation {
side: IsolationSide::default(),
},
}
}
}
/// Where the JavaScript is injected to
#[cfg(feature = "isolation")]
#[derive(Default, Debug, Serialize)]
#[serde(rename_all = "lowercase")]
pub(crate) enum IsolationSide {
/// Original frame, the Brownfield application
#[default]
Original,
/// Secure frame, the isolation security application
#[allow(dead_code)]
Secure,
}
#[derive(Template)]
#[default_template("../scripts/pattern.js")]
pub(crate) struct PatternJavascript {
pub(crate) pattern: PatternObject,
}
#[cfg(feature = "isolation")]
pub(crate) fn format_real_schema(schema: &str, https: bool) -> String {
format!("{}/", crate::protocol::origin(schema, https))
}

1151
crates/tauri/src/plugin.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,511 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use super::{PluginApi, PluginHandle};
use crate::{AppHandle, Runtime, ipc::Channel};
#[cfg(target_os = "android")]
use crate::{
runtime::RuntimeHandle,
sealed::{ManagerBase, RuntimeOrDispatch},
};
#[cfg(mobile)]
use std::sync::atomic::{AtomicI32, Ordering};
#[cfg(mobile)]
use tokio::sync::oneshot;
use serde::{Serialize, de::DeserializeOwned};
use std::{
collections::HashMap,
fmt,
sync::{Mutex, OnceLock, mpsc::channel},
};
type PluginResponse = Result<serde_json::Value, serde_json::Value>;
type PendingPluginCallHandler = Box<dyn FnOnce(PluginResponse) + Send + 'static>;
#[cfg(mobile)]
static PENDING_PLUGIN_CALLS_ID: AtomicI32 = AtomicI32::new(0);
static PENDING_PLUGIN_CALLS: OnceLock<Mutex<HashMap<i32, PendingPluginCallHandler>>> =
OnceLock::new();
static CHANNELS: OnceLock<Mutex<HashMap<u32, Channel<serde_json::Value>>>> = OnceLock::new();
/// Possible errors when invoking a plugin.
#[derive(Debug, thiserror::Error)]
pub enum PluginInvokeError {
/// Failed to reach platform webview handle.
#[error("the webview is unreachable")]
UnreachableWebview,
/// JNI error.
#[cfg(target_os = "android")]
#[error("jni error: {0}")]
Jni(#[from] jni::errors::Error),
/// Error returned from direct mobile plugin invoke.
#[error(transparent)]
InvokeRejected(#[from] ErrorResponse),
/// Failed to deserialize response.
#[error("failed to deserialize response: {0}")]
CannotDeserializeResponse(serde_json::Error),
/// Failed to serialize request payload.
#[error("failed to serialize payload: {0}")]
CannotSerializePayload(serde_json::Error),
}
pub(crate) fn register_channel(channel: Channel<serde_json::Value>) {
CHANNELS
.get_or_init(Default::default)
.lock()
.unwrap()
.insert(channel.id(), channel);
}
/// Glue between Rust and the Kotlin code that sends the plugin response back.
#[cfg(target_os = "android")]
pub fn handle_android_plugin_response(
env: &mut jni::JNIEnv<'_>,
id: i32,
success: jni::objects::JString<'_>,
error: jni::objects::JString<'_>,
) {
let (payload, is_ok): (serde_json::Value, bool) = match (
env
.is_same_object(&success, jni::objects::JObject::default())
.unwrap_or_default(),
env
.is_same_object(&error, jni::objects::JObject::default())
.unwrap_or_default(),
) {
// both null
(true, true) => (serde_json::Value::Null, true),
// error null
(false, true) => (
serde_json::from_str(env.get_string(&success).unwrap().to_str().unwrap()).unwrap(),
true,
),
// success null
(true, false) => (
serde_json::from_str(env.get_string(&error).unwrap().to_str().unwrap()).unwrap(),
false,
),
// both are set - impossible in the Kotlin code
(false, false) => unreachable!(),
};
// Drop the lock before invoking the handler: it delivers the command response
// to the webview (which can block on the UI thread), and holding
// PENDING_PLUGIN_CALLS across that call deadlocks a concurrent `run_command`.
let handler = PENDING_PLUGIN_CALLS
.get_or_init(Default::default)
.lock()
.unwrap()
.remove(&id);
if let Some(handler) = handler {
handler(if is_ok { Ok(payload) } else { Err(payload) });
}
}
/// Glue between Rust and the Kotlin code that sends the channel data.
#[cfg(target_os = "android")]
pub fn send_channel_data(
env: &mut jni::JNIEnv<'_>,
channel_id: i64,
data_str: jni::objects::JString<'_>,
) {
let data: serde_json::Value =
serde_json::from_str(env.get_string(&data_str).unwrap().to_str().unwrap()).unwrap();
// Clone the channel out and drop the lock before send(): send() can block
// delivering to the webview, and holding CHANNELS across it deadlocks a
// concurrent channel registration/send.
let channel = CHANNELS
.get_or_init(Default::default)
.lock()
.unwrap()
.get(&(channel_id as u32))
.cloned();
if let Some(channel) = channel {
let _ = channel.send(data);
}
}
/// Error response from the Kotlin and Swift backends.
#[derive(Debug, thiserror::Error, Clone, serde::Deserialize)]
pub struct ErrorResponse<T = ()> {
/// Error code.
pub code: Option<String>,
/// Error message.
pub message: Option<String>,
/// Optional error data.
#[serde(flatten)]
pub data: T,
}
impl<T> fmt::Display for ErrorResponse<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(code) = &self.code {
write!(f, "[{code}]")?;
if self.message.is_some() {
write!(f, " - ")?;
}
}
if let Some(message) = &self.message {
write!(f, "{message}")?;
}
Ok(())
}
}
impl<R: Runtime, C: DeserializeOwned> PluginApi<R, C> {
/// Registers an iOS plugin.
#[cfg(all(target_os = "ios", feature = "wry"))]
pub fn register_ios_plugin(
&self,
init_fn: unsafe fn() -> *const std::ffi::c_void,
) -> Result<PluginHandle<R>, PluginInvokeError> {
if let Some(webview) = self.handle.manager.webviews().values().next() {
let (tx, rx) = channel();
let name = self.name;
let config = self.raw_config.clone();
webview
.with_webview(move |w| {
unsafe {
crate::ios::register_plugin(
&name.into(),
init_fn(),
&serde_json::to_string(&config).unwrap().as_str().into(),
w.inner() as _,
)
};
tx.send(()).unwrap();
})
.map_err(|_| PluginInvokeError::UnreachableWebview)?;
rx.recv().unwrap();
} else {
unsafe {
crate::ios::register_plugin(
&self.name.into(),
init_fn(),
&serde_json::to_string(&self.raw_config)
.unwrap()
.as_str()
.into(),
std::ptr::null(),
)
};
}
Ok(PluginHandle {
name: self.name,
handle: self.handle.clone(),
})
}
/// Registers an Android plugin.
#[cfg(target_os = "android")]
pub fn register_android_plugin(
&self,
plugin_identifier: &str,
class_name: &str,
) -> Result<PluginHandle<R>, PluginInvokeError> {
use jni::{JNIEnv, errors::Error as JniError, objects::JObject};
fn initialize_plugin<R: Runtime>(
env: &mut JNIEnv<'_>,
activity: &JObject<'_>,
webview: &JObject<'_>,
runtime_handle: &R::Handle,
plugin_name: &'static str,
plugin_class: String,
plugin_config: &serde_json::Value,
) -> Result<(), JniError> {
// instantiate plugin
let plugin_class = runtime_handle.find_class(env, activity, plugin_class)?;
let plugin = env.new_object(
plugin_class,
"(Landroid/app/Activity;)V",
&[activity.into()],
)?;
// load plugin
let plugin_manager = env
.call_method(
activity,
"getPluginManager",
"()Lapp/tauri/plugin/PluginManager;",
&[],
)?
.l()?;
let plugin_name = env.new_string(plugin_name)?;
let config = env.new_string(serde_json::to_string(plugin_config).unwrap())?;
env.call_method(
plugin_manager,
"load",
"(Landroid/webkit/WebView;Ljava/lang/String;Lapp/tauri/plugin/Plugin;Ljava/lang/String;)V",
&[
webview.into(),
(&plugin_name).into(),
(&plugin).into(),
(&config).into(),
],
)?;
Ok(())
}
let plugin_class = format!("{}/{}", plugin_identifier.replace('.', "/"), class_name);
let plugin_name = self.name;
let plugin_config = self.raw_config.clone();
let runtime_handle = self.handle.runtime_handle.clone();
let (tx, rx) = channel();
self
.handle
.runtime_handle
.run_on_android_context(move |env, activity, webview| {
let result = initialize_plugin::<R>(
env,
activity,
webview,
&runtime_handle,
plugin_name,
plugin_class,
&plugin_config,
);
tx.send(result).unwrap();
});
rx.recv().unwrap()?;
Ok(PluginHandle {
name: self.name,
handle: self.handle.clone(),
})
}
}
impl<R: Runtime> PluginHandle<R> {
/// Executes the given mobile command.
/// This is an async optimized variant of run_mobile_plugin
pub async fn run_mobile_plugin_async<T: DeserializeOwned>(
&self,
command: impl AsRef<str>,
payload: impl Serialize,
) -> Result<T, PluginInvokeError> {
let (tx, rx) = oneshot::channel();
// the closure is an FnOnce but on Android we need to clone it (error handling)
let tx = std::sync::Arc::new(std::sync::Mutex::new(Some(tx)));
run_command(
self.name,
&self.handle,
command,
serde_json::to_value(payload).map_err(PluginInvokeError::CannotSerializePayload)?,
move |response| {
tx.lock().unwrap().take().unwrap().send(response).unwrap();
},
)?;
let response = rx.await.unwrap();
match response {
Ok(r) => serde_json::from_value(r).map_err(PluginInvokeError::CannotDeserializeResponse),
Err(r) => Err(
serde_json::from_value::<ErrorResponse>(r)
.map(Into::into)
.map_err(PluginInvokeError::CannotDeserializeResponse)?,
),
}
}
/// Executes the given mobile command.
pub fn run_mobile_plugin<T: DeserializeOwned>(
&self,
command: impl AsRef<str>,
payload: impl Serialize,
) -> Result<T, PluginInvokeError> {
let (tx, rx) = channel();
run_command(
self.name,
&self.handle,
command,
serde_json::to_value(payload).map_err(PluginInvokeError::CannotSerializePayload)?,
move |response| {
tx.send(response).unwrap();
},
)?;
let response = rx.recv().unwrap();
match response {
Ok(r) => serde_json::from_value(r).map_err(PluginInvokeError::CannotDeserializeResponse),
Err(r) => Err(
serde_json::from_value::<ErrorResponse>(r)
.map(Into::into)
.map_err(PluginInvokeError::CannotDeserializeResponse)?,
),
}
}
}
#[cfg(target_os = "ios")]
pub(crate) fn run_command<R: Runtime, C: AsRef<str>, F: FnOnce(PluginResponse) + Send + 'static>(
name: &str,
_handle: &AppHandle<R>,
command: C,
payload: serde_json::Value,
handler: F,
) -> Result<(), PluginInvokeError> {
use std::{
ffi::CStr,
os::raw::{c_char, c_int, c_ulonglong},
};
let id: i32 = PENDING_PLUGIN_CALLS_ID.fetch_add(1, Ordering::Relaxed);
PENDING_PLUGIN_CALLS
.get_or_init(Default::default)
.lock()
.unwrap()
.insert(id, Box::new(handler));
unsafe {
extern "C" fn plugin_command_response_handler(
id: c_int,
success: c_int,
payload: *const c_char,
) {
let payload = unsafe {
assert!(!payload.is_null());
CStr::from_ptr(payload)
};
let handler = PENDING_PLUGIN_CALLS
.get_or_init(Default::default)
.lock()
.unwrap()
.remove(&id);
if let Some(handler) = handler {
let json = payload.to_str().unwrap();
match serde_json::from_str(json) {
Ok(payload) => {
handler(if success == 1 {
Ok(payload)
} else {
Err(payload)
});
}
Err(err) => {
handler(Err(format!("{err}, data: {}", json).into()));
}
}
}
}
extern "C" fn send_channel_data_handler(id: c_ulonglong, payload: *const c_char) {
let payload = unsafe {
assert!(!payload.is_null());
CStr::from_ptr(payload)
};
// Clone the channel out and drop the lock before send(): send() can block
// delivering to the webview, and holding CHANNELS across it deadlocks a
// concurrent channel registration/send.
let channel = CHANNELS
.get_or_init(Default::default)
.lock()
.unwrap()
.get(&(id as u32))
.cloned();
if let Some(channel) = channel {
let payload: serde_json::Value = serde_json::from_str(payload.to_str().unwrap()).unwrap();
let _ = channel.send(payload);
}
}
crate::ios::run_plugin_command(
id,
&name.into(),
&command.as_ref().into(),
&serde_json::to_string(&payload).unwrap().as_str().into(),
crate::ios::PluginMessageCallback(plugin_command_response_handler),
crate::ios::ChannelSendDataCallback(send_channel_data_handler),
);
}
Ok(())
}
#[cfg(target_os = "android")]
pub(crate) fn run_command<
R: Runtime,
C: AsRef<str>,
F: FnOnce(PluginResponse) + Send + Clone + 'static,
>(
name: &str,
handle: &AppHandle<R>,
command: C,
payload: serde_json::Value,
handler: F,
) -> Result<(), PluginInvokeError> {
use jni::{JNIEnv, errors::Error as JniError, objects::JObject};
fn run(
id: i32,
plugin: &str,
command: String,
payload: &serde_json::Value,
env: &mut JNIEnv<'_>,
activity: &JObject<'_>,
) -> Result<(), JniError> {
let plugin = env.new_string(plugin)?;
let command = env.new_string(&command)?;
let data = env.new_string(serde_json::to_string(payload).unwrap())?;
let plugin_manager = env
.call_method(
activity,
"getPluginManager",
"()Lapp/tauri/plugin/PluginManager;",
&[],
)?
.l()?;
env.call_method(
plugin_manager,
"runCommand",
"(ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;)V",
&[
id.into(),
(&plugin).into(),
(&command).into(),
(&data).into(),
],
)?;
Ok(())
}
let handle = match handle.runtime() {
RuntimeOrDispatch::Runtime(r) => r.handle(),
RuntimeOrDispatch::RuntimeHandle(h) => h,
_ => unreachable!(),
};
let id: i32 = PENDING_PLUGIN_CALLS_ID.fetch_add(1, Ordering::Relaxed);
let plugin_name = name.to_string();
let command = command.as_ref().to_string();
PENDING_PLUGIN_CALLS
.get_or_init(Default::default)
.lock()
.unwrap()
.insert(id, Box::new(handler.clone()));
handle.run_on_android_context(move |env, activity, _webview| {
if let Err(e) = run(id, &plugin_name, command, &payload, env, activity) {
handler(Err(e.to_string().into()));
}
});
Ok(())
}

130
crates/tauri/src/process.rs Normal file
View File

@@ -0,0 +1,130 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
//! Types and functions related to child processes management.
use crate::Env;
use std::path::PathBuf;
/// Finds the current running binary's path.
///
/// With exception to any following platform-specific behavior, the path is cached as soon as
/// possible, and then used repeatedly instead of querying for a new path every time this function
/// is called.
///
/// # Platform-specific behavior
///
/// ## Linux
///
/// On Linux, this function will **attempt** to detect if it's currently running from a
/// valid [AppImage] and use that path instead.
///
/// ## macOS
///
/// On `macOS`, this function will return an error if the original path contained any symlinks
/// due to less protection on macOS regarding symlinks. This behavior can be disabled by setting the
/// `process-relaunch-dangerous-allow-symlink-macos` feature, although it is *highly discouraged*.
///
/// # Security
///
/// See [`tauri_utils::platform::current_exe`] for possible security implications.
///
/// # Examples
///
/// ```rust,no_run
/// use tauri::{process::current_binary, Env, Manager};
/// let current_binary_path = current_binary(&Env::default()).unwrap();
///
/// tauri::Builder::default()
/// .setup(|app| {
/// let current_binary_path = current_binary(&app.env())?;
/// Ok(())
/// });
/// ```
///
/// [AppImage]: https://appimage.org/
pub fn current_binary(_env: &Env) -> std::io::Result<PathBuf> {
// if we are running from an AppImage, we ONLY want the set AppImage path
#[cfg(target_os = "linux")]
if let Some(app_image_path) = &_env.appimage {
return Ok(PathBuf::from(app_image_path));
}
tauri_utils::platform::current_exe()
}
/// Restarts the currently running binary.
///
/// See [`current_binary`] for platform specific behavior, and
/// [`tauri_utils::platform::current_exe`] for possible security implications.
///
/// # Examples
///
/// ```rust,no_run
/// use tauri::{process::restart, Env, Manager};
///
/// tauri::Builder::default()
/// .setup(|app| {
/// restart(&app.env());
/// Ok(())
/// });
/// ```
pub fn restart(env: &Env) -> ! {
use std::process::{Command, exit};
if let Ok(path) = current_binary(env) {
// on macOS on updates the binary name might have changed
// so we'll read the Contents/Info.plist file to determine the binary path
#[cfg(target_os = "macos")]
restart_macos_app(&path, env);
if let Err(e) = Command::new(path).args(env.args_os.iter().skip(1)).spawn() {
log::error!("failed to restart app: {e}");
}
}
exit(0);
}
#[cfg(target_os = "macos")]
fn restart_macos_app(current_binary: &std::path::Path, env: &Env) {
use std::process::{Command, exit};
if let Some(macos_directory) = current_binary.parent() {
if macos_directory.components().next_back()
!= Some(std::path::Component::Normal(std::ffi::OsStr::new("MacOS")))
{
return;
}
if let Some(contents_directory) = macos_directory.parent() {
if contents_directory.components().next_back()
!= Some(std::path::Component::Normal(std::ffi::OsStr::new(
"Contents",
)))
{
return;
}
if let Ok(info_plist) =
plist::from_file::<_, plist::Dictionary>(contents_directory.join("Info.plist"))
{
if let Some(binary_name) = info_plist
.get("CFBundleExecutable")
.and_then(|v| v.as_string())
{
if let Err(e) = Command::new(macos_directory.join(binary_name))
.args(env.args_os.iter().skip(1))
.spawn()
{
log::error!("failed to restart app: {e}");
}
exit(0);
}
}
}
}
}

View File

@@ -0,0 +1,309 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use crate::{path::SafePathBuf, scope, webview::UriSchemeProtocolHandler};
use http::{Request, Response, header::*, status::StatusCode};
use http_range::HttpRange;
use std::{borrow::Cow, io::SeekFrom};
use tauri_utils::mime_type::MimeType;
use tokio::fs::File;
use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt};
pub fn get(scope: scope::fs::Scope, window_origin: String) -> UriSchemeProtocolHandler {
Box::new(move |_, request, responder| {
let scope = scope.clone();
let window_origin = window_origin.clone();
crate::async_runtime::spawn(async move {
match get_response(request, &scope, &window_origin).await {
Ok(response) => responder.respond(response),
Err(e) => responder.respond(
http::Response::builder()
.status(http::StatusCode::INTERNAL_SERVER_ERROR)
.header(CONTENT_TYPE, mime::TEXT_PLAIN.essence_str())
.header("Access-Control-Allow-Origin", &window_origin)
.body(e.to_string().into_bytes())
.unwrap(),
),
}
});
})
}
async fn get_response(
request: Request<Vec<u8>>,
scope: &scope::fs::Scope,
window_origin: &str,
) -> Result<Response<Cow<'static, [u8]>>, Box<dyn std::error::Error>> {
// skip leading `/`
let path =
percent_encoding::percent_decode(&request.uri().path().as_bytes()[1..]).decode_utf8_lossy();
let mut resp = Response::builder().header("Access-Control-Allow-Origin", window_origin);
let safe_path = match SafePathBuf::new(path.as_ref().into()) {
Ok(path) => path,
Err(e) => {
log::error!("asset protocol path \"{path}\" is not valid: {e}");
return resp.status(403).body(Vec::new().into()).map_err(Into::into);
}
};
if !scope.is_allowed(&safe_path) {
log::error!("asset protocol not configured to allow the path: {path}");
return resp.status(403).body(Vec::new().into()).map_err(Into::into);
}
// Separate block for easier error handling
let mut file = match File::open(&safe_path).await {
Ok(file) => file,
Err(e) => {
#[cfg(target_os = "android")]
{
if path.starts_with("/storage/emulated/0/Android/data/") {
log::error!(
"Failed to open Android external storage file '{path}': {e}. This may be due to missing storage permissions."
);
}
}
return if e.kind() == std::io::ErrorKind::NotFound {
log::error!("File does not exist at path: {path}");
return resp.status(404).body(Vec::new().into()).map_err(Into::into);
} else if e.kind() == std::io::ErrorKind::PermissionDenied {
log::error!("Missing OS permission to access path \"{path}\": {e}");
return resp.status(403).body(Vec::new().into()).map_err(Into::into);
} else {
Err(e.into())
};
}
};
let len = file.metadata().await?.len();
let (mime_type, read_bytes) = {
// get file mime type
let nbytes = len.min(8192);
let mut magic_buf = Vec::with_capacity(nbytes as usize);
(&mut file).take(nbytes).read_to_end(&mut magic_buf).await?;
file.rewind().await?;
(
MimeType::parse(&magic_buf, &path),
// return the `magic_bytes` if we read the whole file
// to avoid reading it again later if this is not a range request
if len < 8192 { Some(magic_buf) } else { None },
)
};
resp = resp.header(CONTENT_TYPE, &mime_type);
// handle 206 (partial range) http requests
let response =
if let Some(range_header) = request.headers().get("range").and_then(|r| r.to_str().ok()) {
resp = resp.header(ACCEPT_RANGES, "bytes");
resp = resp.header(ACCESS_CONTROL_EXPOSE_HEADERS, "content-range");
let not_satisfiable = || {
Response::builder()
.status(StatusCode::RANGE_NOT_SATISFIABLE)
.header(CONTENT_RANGE, format!("bytes */{len}"))
.body(vec![].into())
.map_err(Into::into)
};
// parse range header
let ranges = if let Ok(ranges) = HttpRange::parse(range_header, len) {
ranges
.iter()
// map the output to spec range <start-end>, example: 0-499
.map(|r| (r.start, r.start + r.length - 1))
.collect::<Vec<_>>()
} else {
return not_satisfiable();
};
/// The Maximum bytes we send in one range
const MAX_LEN: u64 = 1000 * 1024;
// single-part range header
if ranges.len() == 1 {
let &(start, mut end) = ranges.first().unwrap();
// check if a range is not satisfiable
//
// this should be already taken care of by the range parsing library
// but checking here again for extra assurance
if start >= len || end >= len || end < start {
return not_satisfiable();
}
// adjust end byte for MAX_LEN
end = start + (end - start).min(len - start).min(MAX_LEN - 1);
// calculate number of bytes needed to be read
let nbytes = end + 1 - start;
let buf = {
let mut buf = Vec::with_capacity(nbytes as usize);
file.seek(SeekFrom::Start(start)).await?;
file.take(nbytes).read_to_end(&mut buf).await?;
buf
};
resp = resp.header(CONTENT_RANGE, format!("bytes {start}-{end}/{len}"));
resp = resp.header(CONTENT_LENGTH, end + 1 - start);
resp = resp.status(StatusCode::PARTIAL_CONTENT);
resp.body(buf.into())
} else {
let ranges = ranges
.iter()
.filter_map(|&(start, mut end)| {
// filter out unsatisfiable ranges
//
// this should be already taken care of by the range parsing library
// but checking here again for extra assurance
if start >= len || end >= len || end < start {
None
} else {
// adjust end byte for MAX_LEN
end = start + (end - start).min(len - start).min(MAX_LEN - 1);
Some((start, end))
}
})
.collect::<Vec<_>>();
let boundary = random_boundary();
let boundary_sep = format!("\r\n--{boundary}\r\n");
let boundary_closer = format!("\r\n--{boundary}--\r\n");
// `Builder::header` appends, we want to replace the file mime type set earlier
if let Some(headers) = resp.headers_mut() {
headers.insert(
CONTENT_TYPE,
HeaderValue::from_str(&format!("multipart/byteranges; boundary={boundary}"))?,
);
}
let buf = {
// multi-part range header
let mut buf = Vec::new();
for (start, end) in ranges {
// a new range is being written, write the range boundary
buf.write_all(boundary_sep.as_bytes()).await?;
// write the needed headers `Content-Type` and `Content-Range`
buf
.write_all(format!("{CONTENT_TYPE}: {mime_type}\r\n").as_bytes())
.await?;
buf
.write_all(format!("{CONTENT_RANGE}: bytes {start}-{end}/{len}\r\n").as_bytes())
.await?;
// write the separator to indicate the start of the range body
buf.write_all("\r\n".as_bytes()).await?;
// calculate number of bytes needed to be read
let nbytes = end + 1 - start;
buf.reserve(nbytes as usize);
file.seek(SeekFrom::Start(start)).await?;
(&mut file).take(nbytes).read_to_end(&mut buf).await?;
}
// all ranges have been written, write the closing boundary
buf.write_all(boundary_closer.as_bytes()).await?;
buf
};
resp = resp.status(StatusCode::PARTIAL_CONTENT);
resp.body(buf.into())
}
} else if request.method() == http::Method::HEAD {
// if the HEAD method is used, we should not return a body
resp = resp.header(CONTENT_LENGTH, len);
resp.body(Vec::new().into())
} else {
// avoid reading the file if we already read it
// as part of mime type detection
let buf = if let Some(b) = read_bytes {
b
} else {
let mut local_buf = Vec::with_capacity(len as usize);
file.read_to_end(&mut local_buf).await?;
local_buf
};
resp = resp.header(CONTENT_LENGTH, len);
resp.body(buf.into())
};
response.map_err(Into::into)
}
fn random_boundary() -> String {
let mut x = [0_u8; 30];
getrandom::fill(&mut x).expect("failed to get random bytes");
(x[..])
.iter()
.map(|&x| format!("{x:x}"))
.fold(String::new(), |mut a, x| {
a.push_str(x.as_str());
a
})
}
#[cfg(test)]
mod tests {
use super::get_response;
use crate::scope::fs::Scope;
use http::{Request, header::CONTENT_TYPE, status::StatusCode};
use tauri_utils::config::FsScope;
#[test]
fn multi_range_request() {
let app = crate::test::mock_app();
let path = std::env::temp_dir().join(format!(
"tauri-asset-protocol-multi-range-{}.bin",
std::process::id()
));
std::fs::write(&path, vec![b'a'; 1000]).unwrap();
let scope = Scope::new(&app, &FsScope::default()).unwrap();
scope.allow_file(&path).unwrap();
let encoded_path = percent_encoding::percent_encode(
path.to_string_lossy().as_bytes(),
percent_encoding::NON_ALPHANUMERIC,
)
.to_string();
let request = Request::builder()
.uri(format!("asset://localhost/{encoded_path}"))
.header("range", "bytes=0-9, 20-29")
.body(Vec::new())
.unwrap();
let response =
crate::async_runtime::block_on(get_response(request, &scope, "http://tauri.localhost"))
.unwrap();
std::fs::remove_file(&path).unwrap();
assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT);
let content_types = response
.headers()
.get_all(CONTENT_TYPE)
.iter()
.map(|v| v.to_str().unwrap().to_string())
.collect::<Vec<_>>();
assert_eq!(content_types.len(), 1);
let boundary = content_types[0]
.strip_prefix("multipart/byteranges; boundary=")
.unwrap();
assert!(
response
.body()
.ends_with(format!("\r\n--{boundary}--\r\n").as_bytes())
);
}
}

View File

@@ -0,0 +1,112 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use crate::Assets;
use http::header::CONTENT_TYPE;
use serialize_to_javascript::Template;
use tauri_utils::{
assets::EmbeddedAssets,
config::{Csp, HeaderAddition},
};
use std::sync::Arc;
use crate::{
Runtime,
manager::{AppManager, set_csp, webview::PROCESS_IPC_MESSAGE_FN},
webview::UriSchemeProtocolHandler,
};
pub fn get<R: Runtime>(
manager: Arc<AppManager<R>>,
schema: &str,
assets: Arc<EmbeddedAssets>,
aes_gcm_key: [u8; 32],
window_origin: String,
use_https_scheme: bool,
) -> UriSchemeProtocolHandler {
let frame_src = if cfg!(any(windows, target_os = "android")) {
let https = if use_https_scheme { "https" } else { "http" };
format!("{https}://{schema}.localhost")
} else {
format!("{schema}:")
};
let assets = assets as Arc<dyn Assets<R>>;
Box::new(move |_, request, responder| {
let response = match request_to_path(&request).as_str() {
"index.html" => match assets.get(&"index.html".into()) {
Some(asset) => {
let mut asset = String::from_utf8_lossy(asset.as_ref()).into_owned();
let csp_map = set_csp(
&mut asset,
&assets,
&"index.html".into(),
&manager,
Csp::Policy(format!("default-src 'none'; frame-src {frame_src}")),
);
let csp = Csp::DirectiveMap(csp_map).to_string();
let template = tauri_utils::pattern::isolation::IsolationJavascriptRuntime {
runtime_aes_gcm_key: &aes_gcm_key,
origin: window_origin.clone(),
process_ipc_message_fn: PROCESS_IPC_MESSAGE_FN,
};
match template.render(asset.as_ref(), &Default::default()) {
Ok(asset) => http::Response::builder()
.add_configured_headers(manager.config.app.security.headers.as_ref())
.header(CONTENT_TYPE, mime::TEXT_HTML.as_ref())
.header("Content-Security-Policy", csp)
.body(asset.into_string().into_bytes()),
Err(_) => http::Response::builder()
.status(http::StatusCode::INTERNAL_SERVER_ERROR)
.header(CONTENT_TYPE, mime::TEXT_PLAIN.as_ref())
.body(Vec::new()),
}
}
None => http::Response::builder()
.status(http::StatusCode::NOT_FOUND)
.header(CONTENT_TYPE, mime::TEXT_PLAIN.as_ref())
.body(Vec::new()),
},
_ => http::Response::builder()
.status(http::StatusCode::NOT_FOUND)
.header(CONTENT_TYPE, mime::TEXT_PLAIN.as_ref())
.body(Vec::new()),
};
if let Ok(r) = response {
responder.respond(r);
} else {
responder.respond(
http::Response::builder()
.status(http::StatusCode::INTERNAL_SERVER_ERROR)
.body("failed to get response".as_bytes())
.unwrap(),
);
}
})
}
fn request_to_path(request: &http::Request<Vec<u8>>) -> String {
let path = request
.uri()
.path()
.trim_start_matches('/')
.trim_end_matches('/');
let path = percent_encoding::percent_decode(path.as_bytes())
.decode_utf8_lossy()
.to_string();
if path.is_empty() {
// if the url has no path, we should load `index.html`
"index.html".to_string()
} else {
// skip leading `/`
path.chars().skip(1).collect()
}
}

View File

@@ -0,0 +1,53 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
//! Custom protocol handlers
use percent_encoding::{AsciiSet, NON_ALPHANUMERIC};
#[cfg(feature = "protocol-asset")]
pub mod asset;
#[cfg(feature = "isolation")]
pub mod isolation;
pub mod tauri;
/// Percent-encoding set matching JavaScript's `encodeURIComponent`,
/// which leaves `A-Z a-z 0-9 - _ . ! ~ * ' ( )` unescaped.
pub(crate) const ENCODE_URI_COMPONENT: &AsciiSet = &NON_ALPHANUMERIC
.remove(b'-')
.remove(b'_')
.remove(b'.')
.remove(b'!')
.remove(b'~')
.remove(b'*')
.remove(b'\'')
.remove(b'(')
.remove(b')');
/// The origin (`scheme://host`, without a trailing slash) a custom protocol is served from.
///
/// On Windows and Android custom protocols are served over `http(s)://{scheme}.localhost`;
/// everywhere else they are served over `{scheme}://localhost`.
pub(crate) fn origin(scheme: &str, use_https: bool) -> String {
if cfg!(windows) || cfg!(target_os = "android") {
let http = if use_https { "https" } else { "http" };
format!("{http}://{scheme}.localhost")
} else {
format!("{scheme}://localhost")
}
}
#[cfg(test)]
mod tests {
#[test]
fn encode_uri_component_matches_js() {
// encodeURIComponent("aZ09-_.!~*'() /\\?#&%+é")
let input = "aZ09-_.!~*'() /\\?#&%+é";
let expected = "aZ09-_.!~*'()%20%2F%5C%3F%23%26%25%2B%C3%A9";
assert_eq!(
percent_encoding::utf8_percent_encode(input, super::ENCODE_URI_COMPONENT).to_string(),
expected
);
}
}

View File

@@ -0,0 +1,267 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::{borrow::Cow, sync::Arc};
use http::{Request, Response as HttpResponse, StatusCode, header::CONTENT_TYPE};
use tauri_utils::config::HeaderAddition;
use crate::{
Runtime,
manager::{AppManager, webview::PROXY_DEV_SERVER},
webview::{UriSchemeProtocolHandler, WebResourceRequestHandler},
};
#[cfg(all(dev, mobile))]
use std::{collections::HashMap, sync::Mutex};
#[cfg(all(dev, mobile))]
#[derive(Clone)]
struct CachedResponse {
status: http::StatusCode,
headers: http::HeaderMap,
body: Vec<u8>,
}
pub fn get<R: Runtime>(
manager: Arc<AppManager<R>>,
window_origin: String,
web_resource_request_handler: Option<Box<WebResourceRequestHandler>>,
) -> UriSchemeProtocolHandler {
#[cfg(all(dev, mobile))]
let (url, client, response_cache) = {
let use_https = window_origin.starts_with("https");
let mut url = manager.get_app_url(use_https).as_str().to_string();
if url.ends_with('/') {
url.pop();
}
#[allow(unused_mut)]
let mut client_builder = reqwest::ClientBuilder::new();
if use_https {
#[cfg(feature = "rustls-tls")]
if rustls::crypto::CryptoProvider::get_default().is_none() {
let _ = rustls::crypto::ring::default_provider().install_default();
}
// we can't load env vars at runtime, gotta embed them in the lib
#[allow(unused_variables)]
if let Some(cert_pem) = option_env!("TAURI_DEV_ROOT_CERTIFICATE") {
#[cfg(any(
feature = "native-tls",
feature = "native-tls-vendored",
feature = "rustls-tls"
))]
{
log::info!("adding dev server root certificate");
let certificate = reqwest::Certificate::from_pem(cert_pem.as_bytes())
.expect("failed to parse TAURI_DEV_ROOT_CERTIFICATE");
client_builder = client_builder.tls_certs_merge([certificate]);
}
#[cfg(not(any(
feature = "native-tls",
feature = "native-tls-vendored",
feature = "rustls-tls"
)))]
{
log::warn!(
"the dev root-certificate-path option was provided, but you must enable one of the following Tauri features in Cargo.toml: native-tls, native-tls-vendored, rustls-tls"
);
}
} else {
log::warn!(
"loading HTTPS URL; you might need to provide a certificate via the `dev --root-certificate-path` option. You must enable one of the following Tauri features in Cargo.toml: native-tls, native-tls-vendored, rustls-tls"
);
}
}
let client = client_builder.build().unwrap();
let response_cache = Mutex::new(HashMap::new());
(url, client, response_cache)
};
let context = Arc::new(Context {
manager,
web_resource_request_handler,
window_origin,
#[cfg(all(dev, mobile))]
client,
#[cfg(all(dev, mobile))]
url,
#[cfg(all(dev, mobile))]
response_cache,
});
Box::new(move |_, request, responder| {
let context = context.clone();
crate::async_runtime::spawn(async move {
match get_response(&context, request).await {
Ok(response) => responder.respond(response),
Err(e) => responder.respond(
HttpResponse::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.header(CONTENT_TYPE, mime::TEXT_PLAIN.essence_str())
.header("Access-Control-Allow-Origin", &context.window_origin)
.body(e.to_string().into_bytes())
.unwrap(),
),
}
});
})
}
struct Context<R: Runtime> {
manager: Arc<AppManager<R>>,
window_origin: String,
web_resource_request_handler: Option<Box<WebResourceRequestHandler>>,
#[cfg(all(dev, mobile))]
url: String,
#[cfg(all(dev, mobile))]
client: reqwest::Client,
#[cfg(all(dev, mobile))]
response_cache: Mutex<HashMap<String, CachedResponse>>,
}
async fn get_response<R: Runtime>(
context: &Context<R>,
request: Request<Vec<u8>>,
) -> Result<HttpResponse<Cow<'static, [u8]>>, Box<dyn std::error::Error>> {
let Context {
manager,
web_resource_request_handler,
window_origin,
#[cfg(all(dev, mobile))]
client,
#[cfg(all(dev, mobile))]
url,
#[cfg(all(dev, mobile))]
response_cache,
} = context;
// use the entire URI as we are going to proxy the request
let path = if PROXY_DEV_SERVER {
request.uri().to_string()
} else {
// ignore query string and fragment
request
.uri()
.to_string()
.split(&['?', '#'])
.next()
.unwrap()
.into()
};
let path = path
.strip_prefix("tauri://localhost")
.map(|p| p.to_string())
// the `strip_prefix` only returns None when a request is made to `https://tauri.$P` on Windows and Android
// where `$P` is not `localhost/*`
.unwrap_or_default();
#[allow(unused_mut)]
let mut builder = HttpResponse::builder()
.add_configured_headers(manager.config.app.security.headers.as_ref())
.header("Access-Control-Allow-Origin", window_origin);
#[cfg(all(dev, mobile))]
let mut response =
proxy_dev_request(client, url, response_cache, path, builder, &request).await?;
#[cfg(not(all(dev, mobile)))]
let mut response = {
let asset = manager.get_asset(
path,
request.uri().scheme() == Some(&http::uri::Scheme::HTTPS),
)?;
builder = builder.header(CONTENT_TYPE, &asset.mime_type);
if let Some(csp) = &asset.csp_header {
builder = builder.header("Content-Security-Policy", csp);
}
builder.body(asset.bytes.into())?
};
if let Some(handler) = web_resource_request_handler {
handler(request, &mut response);
}
Ok(response)
}
#[cfg(all(dev, mobile))]
async fn proxy_dev_request(
client: &reqwest::Client,
url: &String,
response_cache: &Mutex<HashMap<String, CachedResponse>>,
path: String,
mut builder: http::response::Builder,
request: &Request<Vec<u8>>,
) -> Result<HttpResponse<Cow<'static, [u8]>>, Box<dyn std::error::Error>> {
let decoded_path = percent_encoding::percent_decode(path.as_bytes())
.decode_utf8_lossy()
.to_string();
let url = format!(
"{}/{}",
url.trim_end_matches('/'),
decoded_path.trim_start_matches('/')
);
let mut proxy_builder = client.request(request.method().clone(), &url);
for (name, value) in request.headers() {
proxy_builder = proxy_builder.header(name, value);
}
proxy_builder = proxy_builder.body(request.body().clone());
let response = proxy_builder.send().await.map_err(|e|{
let error_message = format!(
"Failed to request {url}: {e}{}",
if let Some(s) = e.status() {
format!("status code: {}", s.as_u16())
} else if cfg!(target_os = "ios") {
", did you grant local network permissions? That is required to reach the development server. Please grant the permission via the prompt or in `Settings > Privacy & Security > Local Network` and restart the app. See https://support.apple.com/en-us/102229 for more information.".to_string()
} else {
"".to_string()
}
);
log::error!("{error_message}");
error_message
})?;
let status = response.status();
if status == http::StatusCode::NOT_MODIFIED {
if let Some(response) = response_cache.lock().unwrap().get(&url).cloned() {
for (name, value) in &response.headers {
builder = builder.header(name, value);
}
return Ok(builder.status(response.status).body(response.body.into())?);
}
}
let headers = response.headers().clone();
let body = response.bytes().await?.to_vec();
let response = CachedResponse {
status,
headers,
body,
};
response_cache
.lock()
.unwrap()
.insert(url.clone(), response.clone());
for (name, value) in &response.headers {
builder = builder.header(name, value);
}
builder
.status(response.status)
.body(response.body.into())
.map_err(Into::into)
}

View File

@@ -0,0 +1,223 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
// a modified version of https://github.com/denoland/deno/blob/0ae83847f498a2886ae32172e50fd5bdbab2f524/core/resources.rs#L220
pub(crate) mod plugin;
use std::{
any::{Any, TypeId, type_name},
borrow::Cow,
collections::BTreeMap,
sync::Arc,
};
/// Resources are Rust objects that are stored in [ResourceTable] and managed by tauri.
///
/// They are identified in JS by a numeric ID (the resource ID, or rid).
/// Resources can be created in commands. Resources can also be retrieved in commands by
/// their rid. Resources are thread-safe.
///
/// Resources are reference counted in Rust. This means that they can be
/// cloned and passed around. When the last reference is dropped, the resource
/// is automatically closed. As long as the resource exists in the resource
/// table, the reference count is at least 1.
pub trait Resource: Any + 'static + Send + Sync {
/// Returns a string representation of the resource. The default implementation
/// returns the Rust type name, but specific resource types may override this
/// trait method.
fn name(&self) -> Cow<'_, str> {
type_name::<Self>().into()
}
/// Resources may implement the `close()` trait method if they need to do
/// resource specific clean-ups, such as cancelling pending futures, after a
/// resource has been removed from the resource table.
fn close(self: Arc<Self>) {}
}
impl dyn Resource {
#[inline(always)]
fn is<T: Resource>(&self) -> bool {
self.type_id() == TypeId::of::<T>()
}
#[inline(always)]
pub(crate) fn downcast_arc<T: Resource>(self: &Arc<Self>) -> Option<&Arc<T>> {
if self.is::<T>() {
// A resource is stored as `Arc<T>` in a BTreeMap
// and is safe to cast to `Arc<T>` because of the runtime
// check done in `self.is::<T>()`
let ptr = self as *const Arc<_> as *const Arc<T>;
Some(unsafe { &*ptr })
} else {
None
}
}
}
/// A `ResourceId` is an integer value referencing a resource. It could be
/// considered to be the tauri equivalent of a `file descriptor` in POSIX like
/// operating systems.
pub type ResourceId = u32;
/// Map-like data structure storing Tauri's resources (equivalent to file
/// descriptors).
///
/// Provides basic methods for element access. A resource can be of any type.
/// Different types of resources can be stored in the same map, and provided
/// with a name for description.
///
/// Each resource is identified through a _resource ID (rid)_, which acts as
/// the key in the map.
#[derive(Default)]
pub struct ResourceTable {
index: BTreeMap<ResourceId, Arc<dyn Resource>>,
}
impl ResourceTable {
fn new_random_rid() -> u32 {
let mut bytes = [0_u8; 4];
getrandom::fill(&mut bytes).expect("failed to get random bytes");
u32::from_ne_bytes(bytes)
}
/// Inserts resource into the resource table, which takes ownership of it.
///
/// The resource type is erased at runtime and must be statically known
/// when retrieving it through `get()`.
///
/// Returns a unique resource ID, which acts as a key for this resource.
pub fn add<T: Resource>(&mut self, resource: T) -> ResourceId {
self.add_arc(Arc::new(resource))
}
/// Inserts a `Arc`-wrapped resource into the resource table.
///
/// The resource type is erased at runtime and must be statically known
/// when retrieving it through `get()`.
///
/// Returns a unique resource ID, which acts as a key for this resource.
pub fn add_arc<T: Resource>(&mut self, resource: Arc<T>) -> ResourceId {
let resource = resource as Arc<dyn Resource>;
self.add_arc_dyn(resource)
}
/// Inserts a `Arc`-wrapped resource into the resource table.
///
/// The resource type is erased at runtime and must be statically known
/// when retrieving it through `get()`.
///
/// Returns a unique resource ID, which acts as a key for this resource.
pub fn add_arc_dyn(&mut self, resource: Arc<dyn Resource>) -> ResourceId {
let mut rid = Self::new_random_rid();
while self.index.contains_key(&rid) {
rid = Self::new_random_rid();
}
let removed_resource = self.index.insert(rid, resource);
assert!(removed_resource.is_none());
rid
}
/// Returns true if any resource with the given `rid` exists.
pub fn has(&self, rid: ResourceId) -> bool {
self.index.contains_key(&rid)
}
/// Returns a reference counted pointer to the resource of type `T` with the
/// given `rid`. If `rid` is not present or has a type different than `T`,
/// this function returns [`Error::BadResourceId`](crate::Error::BadResourceId).
pub fn get<T: Resource>(&self, rid: ResourceId) -> crate::Result<Arc<T>> {
self
.index
.get(&rid)
.and_then(|rc| rc.downcast_arc::<T>())
.cloned()
.ok_or_else(|| crate::Error::BadResourceId(rid))
}
/// Returns a reference counted pointer to the resource of the given `rid`.
/// If `rid` is not present, this function returns [`crate::Error::BadResourceId`].
pub fn get_any(&self, rid: ResourceId) -> crate::Result<Arc<dyn Resource>> {
self
.index
.get(&rid)
.ok_or_else(|| crate::Error::BadResourceId(rid))
.cloned()
}
/// Replaces a resource with a new resource.
///
/// Panics if the resource does not exist.
pub fn replace<T: Resource>(&mut self, rid: ResourceId, resource: T) {
let result = self
.index
.insert(rid, Arc::new(resource) as Arc<dyn Resource>);
assert!(result.is_some());
}
/// Removes a resource of type `T` from the resource table and returns it.
/// If a resource with the given `rid` exists but its type does not match `T`,
/// it is not removed from the resource table. Note that the resource's
/// `close()` method is *not* called.
///
/// Also note that there might be a case where
/// the returned `Arc<T>` is referenced by other variables. That is, we cannot
/// assume that `Arc::strong_count(&returned_arc)` is always equal to 1 on success.
/// In particular, be really careful when you want to extract the inner value of
/// type `T` from `Arc<T>`.
pub fn take<T: Resource>(&mut self, rid: ResourceId) -> crate::Result<Arc<T>> {
let resource = self.get::<T>(rid)?;
self.index.remove(&rid);
Ok(resource)
}
/// Removes a resource from the resource table and returns it. Note that the
/// resource's `close()` method is *not* called.
///
/// Also note that there might be a
/// case where the returned `Arc<T>` is referenced by other variables. That is,
/// we cannot assume that `Arc::strong_count(&returned_arc)` is always equal to 1
/// on success. In particular, be really careful when you want to extract the
/// inner value of type `T` from `Arc<T>`.
pub fn take_any(&mut self, rid: ResourceId) -> crate::Result<Arc<dyn Resource>> {
self
.index
.remove(&rid)
.ok_or_else(|| crate::Error::BadResourceId(rid))
}
/// Returns an iterator that yields a `(id, name)` pair for every resource
/// that's currently in the resource table. This can be used for debugging
/// purposes. Note that the order in
/// which items appear is not specified.
pub fn names(&self) -> impl Iterator<Item = (ResourceId, Cow<'_, str>)> {
self
.index
.iter()
.map(|(&id, resource)| (id, resource.name()))
}
/// Removes the resource with the given `rid` from the resource table. If the
/// only reference to this resource existed in the resource table, this will
/// cause the resource to be dropped. However, since resources are reference
/// counted, therefore pending ops are not automatically cancelled. A resource
/// may implement the `close()` method to perform clean-ups such as canceling
/// ops.
pub fn close(&mut self, rid: ResourceId) -> crate::Result<()> {
self
.index
.remove(&rid)
.ok_or_else(|| crate::Error::BadResourceId(rid))
.map(|resource| resource.close())
}
/// Removes and frees all resources stored. Note that the
/// resource's `close()` method is *not* called.
pub(crate) fn clear(&mut self) {
self.index.clear()
}
}

View File

@@ -0,0 +1,31 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use crate::{
Manager, Runtime, Webview, command,
plugin::{Builder, TauriPlugin},
};
use super::ResourceId;
#[command(root = "crate")]
fn close<R: Runtime>(webview: Webview<R>, rid: ResourceId) -> crate::Result<()> {
let mut result = webview.resources_table().close(rid);
if result.is_err() {
result = webview.window().resources_table().close(rid);
if result.is_err() {
result = webview.app_handle().resources_table().close(rid);
}
}
result
}
pub(crate) fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("resources")
.invoke_handler(crate::generate_handler![
#![plugin(resources)]
close
])
.build()
}

View File

@@ -0,0 +1,739 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::{
collections::{HashMap, HashSet},
fmt,
path::{MAIN_SEPARATOR, Path, PathBuf},
sync::{
Arc, Mutex,
atomic::{AtomicBool, AtomicU32, Ordering},
},
};
use tauri_utils::config::FsScope;
use crate::ScopeEventId;
pub use glob::Pattern;
/// Scope change event.
#[derive(Debug, Clone)]
pub enum Event {
/// A path has been allowed.
PathAllowed(PathBuf),
/// A path has been forbidden.
PathForbidden(PathBuf),
}
type EventListener = Box<dyn Fn(&Event) + Send>;
enum Pending {
Unlisten(ScopeEventId),
Listen {
id: ScopeEventId,
handler: EventListener,
},
Emit(Event),
}
struct ScopeInner {
allowed_patterns: Mutex<HashSet<Pattern>>,
forbidden_patterns: Mutex<HashSet<Pattern>>,
match_options: glob::MatchOptions,
event_listeners: Mutex<HashMap<ScopeEventId, EventListener>>,
pending: Mutex<Vec<Pending>>,
emitting: AtomicBool,
next_event_id: AtomicU32,
}
/// Scope for filesystem access.
#[derive(Clone)]
pub struct Scope {
inner: Arc<ScopeInner>,
}
impl Scope {
fn next_event_id(&self) -> u32 {
self.inner.next_event_id.fetch_add(1, Ordering::Relaxed)
}
}
impl fmt::Debug for Scope {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Scope")
.field(
"allowed_patterns",
&self
.inner
.allowed_patterns
.lock()
.unwrap()
.iter()
.map(|p| p.as_str())
.collect::<Vec<&str>>(),
)
.field(
"forbidden_patterns",
&self
.inner
.forbidden_patterns
.lock()
.unwrap()
.iter()
.map(|p| p.as_str())
.collect::<Vec<&str>>(),
)
.finish()
}
}
fn push_pattern<P: AsRef<Path>, F: Fn(&str) -> Result<Pattern, glob::PatternError>>(
list: &mut HashSet<Pattern>,
pattern: P,
f: F,
) -> crate::Result<()> {
// Reconstruct pattern path components with appropriate separator
// so `some\path/to/dir/**\*` would be `some/path/to/dir/**/*` on Unix
// and `some\path\to\dir\**\*` on Windows.
let path: PathBuf = pattern.as_ref().components().collect();
// Add pattern as is to be matched with paths as is
let path_str = path.to_string_lossy();
list.insert(f(&path_str)?);
// On Windows, if path starts with a Prefix, try to strip it if possible
// so `\\?\C:\\SomeDir` would result in a scope of:
// - `\\?\C:\\SomeDir`
// - `C:\\SomeDir`
#[cfg(windows)]
{
use std::path::{Component, Prefix};
let mut components = path.components();
let is_unc = match components.next() {
Some(Component::Prefix(p)) => match p.kind() {
Prefix::VerbatimDisk(..) => true,
_ => false, // Other kinds of UNC paths
},
_ => false, // relative or empty
};
if is_unc {
// we remove UNC manually, instead of `dunce::simplified` because
// `path` could have `*` in it and that's not allowed on Windows and
// `dunce::simplified` will check that and return `path` as is without simplification
let simplified = path
.to_str()
.and_then(|s| s.get(4..))
.map_or(path.as_path(), Path::new);
let simplified_str = simplified.to_string_lossy();
if simplified_str != path_str {
list.insert(f(&simplified_str)?);
}
}
}
// Add canonicalized version of the pattern or canonicalized version of its parents
// so `/data/user/0/appid/assets/*` would be canonicalized to `/data/data/appid/assets/*`
// and can then be matched against any of them.
if let Some(p) = canonicalize_parent(path) {
list.insert(f(&p.to_string_lossy())?);
}
Ok(())
}
/// Attempt to canonicalize path or its parents in case we have a path like `/data/user/0/appid/**`
/// where `**` obviously does not exist but we need to canonicalize the parent.
///
/// example: given the `/data/user/0/appid/assets/*` path,
/// it's a glob pattern so it won't exist (std::fs::canonicalize() fails);
///
/// the second iteration needs to check `/data/user/0/appid/assets` and save the `*` component to append later.
///
/// if it also does not exist, a third iteration is required to check `/data/user/0/appid`
/// with `assets/*` as the cached value (`checked_path` variable)
/// on Android that gets canonicalized to `/data/data/appid` so the final value will be `/data/data/appid/assets/*`
/// which is the value we want to check when we execute the `Scope::is_allowed` function
fn canonicalize_parent(mut path: PathBuf) -> Option<PathBuf> {
let mut failed_components = None;
loop {
if let Ok(path) = path.canonicalize() {
break Some(if let Some(p) = failed_components {
path.join(p)
} else {
path
});
}
// grap the last component of the path
if let Some(mut last) = path.iter().next_back().map(PathBuf::from) {
// remove the last component of the path so the next iteration checks its parent
// if there is no more parent components, we failed to canonicalize
if !path.pop() {
break None;
}
// append the already checked path to the last component
// to construct `<last>/<checked_path>` and saved it for next iteration
if let Some(failed_components) = &failed_components {
last.push(failed_components);
}
failed_components.replace(last);
} else {
break None;
}
}
}
impl Scope {
/// Creates a new scope from a [`FsScope`] configuration.
pub fn new<R: crate::Runtime, M: crate::Manager<R>>(
manager: &M,
scope: &FsScope,
) -> crate::Result<Self> {
let mut allowed_patterns = HashSet::new();
for path in scope.allowed_paths() {
if let Ok(path) = manager.path().parse(path) {
push_pattern(&mut allowed_patterns, path, Pattern::new)?;
}
}
let mut forbidden_patterns = HashSet::new();
if let Some(forbidden_paths) = scope.forbidden_paths() {
for path in forbidden_paths {
if let Ok(path) = manager.path().parse(path) {
push_pattern(&mut forbidden_patterns, path, Pattern::new)?;
}
}
}
let require_literal_leading_dot = match scope {
FsScope::Scope {
require_literal_leading_dot: Some(require),
..
} => *require,
// dotfiles are not supposed to be exposed by default on unix
#[cfg(unix)]
_ => true,
#[cfg(windows)]
_ => false,
};
Ok(Self {
inner: Arc::new(ScopeInner {
allowed_patterns: Mutex::new(allowed_patterns),
forbidden_patterns: Mutex::new(forbidden_patterns),
event_listeners: Default::default(),
pending: Default::default(),
emitting: AtomicBool::new(false),
next_event_id: Default::default(),
match_options: glob::MatchOptions {
// this is needed so `/dir/*` doesn't match files within subdirectories such as `/dir/subdir/file.txt`
// see: <https://github.com/tauri-apps/tauri/security/advisories/GHSA-6mv3-wm7j-h4w5>
require_literal_separator: true,
require_literal_leading_dot,
..Default::default()
},
}),
})
}
/// The list of allowed patterns.
pub fn allowed_patterns(&self) -> HashSet<Pattern> {
self.inner.allowed_patterns.lock().unwrap().clone()
}
/// The list of forbidden patterns.
pub fn forbidden_patterns(&self) -> HashSet<Pattern> {
self.inner.forbidden_patterns.lock().unwrap().clone()
}
/// Listen to an event on this scope.
pub fn listen<F: Fn(&Event) + Send + 'static>(&self, f: F) -> ScopeEventId {
let id = self.next_event_id();
self.listen_with_id(id, Box::new(f));
id
}
fn listen_with_id(&self, id: ScopeEventId, handler: EventListener) {
if self.inner.emitting.load(Ordering::Relaxed) {
self
.inner
.pending
.lock()
.unwrap()
.push(Pending::Listen { id, handler });
} else {
self
.inner
.event_listeners
.lock()
.unwrap()
.insert(id, handler);
}
}
/// Listen to an event on this scope and immediately unlisten.
pub fn once<F: FnOnce(&Event) + Send + 'static>(&self, f: F) -> ScopeEventId {
let self_ = self.clone();
let handler = std::cell::Cell::new(Some(f));
let id = self.next_event_id();
self.listen_with_id(
id,
Box::new(move |event| {
self_.unlisten(id);
let handler = handler
.take()
.expect("attempted to call handler more than once");
handler(event);
}),
);
id
}
/// Removes an event listener on this scope.
pub fn unlisten(&self, id: ScopeEventId) {
if self.inner.emitting.load(Ordering::Relaxed) {
self
.inner
.pending
.lock()
.unwrap()
.push(Pending::Unlisten(id));
} else {
self.inner.event_listeners.lock().unwrap().remove(&id);
}
}
fn emit(&self, event: Event) {
let was_emitting = self.inner.emitting.swap(true, Ordering::Relaxed);
if was_emitting {
self
.inner
.pending
.lock()
.unwrap()
.push(Pending::Emit(event));
return;
}
{
let listeners = self.inner.event_listeners.lock().unwrap();
let handlers = listeners.values();
for listener in handlers {
listener(&event);
}
self.inner.emitting.store(false, Ordering::Relaxed);
}
let pending = {
let mut lock = self.inner.pending.lock().unwrap();
std::mem::take(&mut *lock)
};
for action in pending {
match action {
Pending::Unlisten(id) => self.unlisten(id),
Pending::Listen { id, handler } => self.listen_with_id(id, handler),
Pending::Emit(event) => self.emit(event),
}
}
}
/// Extend the allowed patterns with the given directory.
///
/// After this function has been called, the frontend will be able to use the Tauri API to read
/// the directory and all of its files. If `recursive` is `true`, subdirectories will be accessible too.
pub fn allow_directory<P: AsRef<Path>>(&self, path: P, recursive: bool) -> crate::Result<()> {
let path = path.as_ref();
{
let mut list = self.inner.allowed_patterns.lock().unwrap();
// allow the directory to be read
push_pattern(&mut list, path, escaped_pattern)?;
// allow its files and subdirectories to be read
push_pattern(&mut list, path, |p| {
escaped_pattern_with(p, if recursive { "**" } else { "*" })
})?;
}
self.emit(Event::PathAllowed(path.to_path_buf()));
Ok(())
}
/// Extend the allowed patterns with the given file path.
///
/// After this function has been called, the frontend will be able to use the Tauri API to read the contents of this file.
pub fn allow_file<P: AsRef<Path>>(&self, path: P) -> crate::Result<()> {
let path = path.as_ref();
push_pattern(
&mut self.inner.allowed_patterns.lock().unwrap(),
path,
escaped_pattern,
)?;
self.emit(Event::PathAllowed(path.to_path_buf()));
Ok(())
}
/// Set the given directory path to be forbidden by this scope.
///
/// **Note:** this takes precedence over allowed paths, so its access gets denied **always**.
pub fn forbid_directory<P: AsRef<Path>>(&self, path: P, recursive: bool) -> crate::Result<()> {
let path = path.as_ref();
{
let mut list = self.inner.forbidden_patterns.lock().unwrap();
// allow the directory to be read
push_pattern(&mut list, path, escaped_pattern)?;
// allow its files and subdirectories to be read
push_pattern(&mut list, path, |p| {
escaped_pattern_with(p, if recursive { "**" } else { "*" })
})?;
}
self.emit(Event::PathForbidden(path.to_path_buf()));
Ok(())
}
/// Set the given file path to be forbidden by this scope.
///
/// **Note:** this takes precedence over allowed paths, so its access gets denied **always**.
pub fn forbid_file<P: AsRef<Path>>(&self, path: P) -> crate::Result<()> {
let path = path.as_ref();
push_pattern(
&mut self.inner.forbidden_patterns.lock().unwrap(),
path,
escaped_pattern,
)?;
self.emit(Event::PathForbidden(path.to_path_buf()));
Ok(())
}
/// Determines if the given path is allowed on this scope.
///
/// Returns `false` if the path was explicitly forbidden or neither allowed nor forbidden.
///
/// May return `false` if the path points to a broken symlink.
pub fn is_allowed<P: AsRef<Path>>(&self, path: P) -> bool {
let path = try_resolve_symlink_and_canonicalize(path);
if let Ok(path) = path {
let path: PathBuf = path.components().collect();
let forbidden = self
.inner
.forbidden_patterns
.lock()
.unwrap()
.iter()
.any(|p| p.matches_path_with(&path, self.inner.match_options));
if forbidden {
false
} else {
self
.inner
.allowed_patterns
.lock()
.unwrap()
.iter()
.any(|p| p.matches_path_with(&path, self.inner.match_options))
}
} else {
false
}
}
/// Determines if the given path is explicitly forbidden on this scope.
///
/// May return `true` if the path points to a broken symlink.
pub fn is_forbidden<P: AsRef<Path>>(&self, path: P) -> bool {
let path = try_resolve_symlink_and_canonicalize(path);
if let Ok(path) = path {
let path: PathBuf = path.components().collect();
self
.inner
.forbidden_patterns
.lock()
.unwrap()
.iter()
.any(|p| p.matches_path_with(&path, self.inner.match_options))
} else {
true
}
}
}
fn try_resolve_symlink_and_canonicalize<P: AsRef<Path>>(path: P) -> crate::Result<PathBuf> {
let path = path.as_ref();
let path = if path.is_symlink() {
std::fs::read_link(path)?
} else {
path.to_path_buf()
};
if !path.exists() {
crate::Result::Ok(path)
} else {
std::fs::canonicalize(path).map_err(Into::into)
}
}
fn escaped_pattern(p: &str) -> Result<Pattern, glob::PatternError> {
Pattern::new(&glob::Pattern::escape(p))
}
fn escaped_pattern_with(p: &str, append: &str) -> Result<Pattern, glob::PatternError> {
if p.ends_with(MAIN_SEPARATOR) {
Pattern::new(&format!("{}{append}", glob::Pattern::escape(p)))
} else {
Pattern::new(&format!(
"{}{}{append}",
glob::Pattern::escape(p),
MAIN_SEPARATOR
))
}
}
#[cfg(test)]
mod tests {
use std::{collections::HashSet, sync::Arc};
use glob::Pattern;
use crate::fs::ScopeInner;
use super::{Scope, push_pattern};
fn new_scope() -> Scope {
Scope {
inner: Arc::new(ScopeInner {
allowed_patterns: Default::default(),
forbidden_patterns: Default::default(),
event_listeners: Default::default(),
pending: Default::default(),
emitting: Default::default(),
next_event_id: Default::default(),
match_options: glob::MatchOptions {
// this is needed so `/dir/*` doesn't match files within subdirectories such as `/dir/subdir/file.txt`
// see: <https://github.com/tauri-apps/tauri/security/advisories/GHSA-6mv3-wm7j-h4w5>
require_literal_separator: true,
// dotfiles are not supposed to be exposed by default on unix
#[cfg(unix)]
require_literal_leading_dot: true,
#[cfg(windows)]
require_literal_leading_dot: false,
..Default::default()
},
}),
}
}
#[test]
fn path_is_escaped() {
let scope = new_scope();
#[cfg(unix)]
{
scope.allow_directory("/home/tauri/**", false).unwrap();
assert!(scope.is_allowed("/home/tauri/**"));
assert!(scope.is_allowed("/home/tauri/**/file"));
assert!(!scope.is_allowed("/home/tauri/anyfile"));
}
#[cfg(windows)]
{
scope.allow_directory("C:\\home\\tauri\\**", false).unwrap();
assert!(scope.is_allowed("C:\\home\\tauri\\**"));
assert!(scope.is_allowed("C:\\home\\tauri\\**\\file"));
assert!(!scope.is_allowed("C:\\home\\tauri\\anyfile"));
}
let scope = new_scope();
#[cfg(unix)]
{
scope.allow_file("/home/tauri/**").unwrap();
assert!(scope.is_allowed("/home/tauri/**"));
assert!(!scope.is_allowed("/home/tauri/**/file"));
assert!(!scope.is_allowed("/home/tauri/anyfile"));
}
#[cfg(windows)]
{
scope.allow_file("C:\\home\\tauri\\**").unwrap();
assert!(scope.is_allowed("C:\\home\\tauri\\**"));
assert!(!scope.is_allowed("C:\\home\\tauri\\**\\file"));
assert!(!scope.is_allowed("C:\\home\\tauri\\anyfile"));
}
let scope = new_scope();
#[cfg(unix)]
{
scope.allow_directory("/home/tauri", true).unwrap();
scope.forbid_directory("/home/tauri/**", false).unwrap();
assert!(!scope.is_allowed("/home/tauri/**"));
assert!(!scope.is_allowed("/home/tauri/**/file"));
assert!(scope.is_allowed("/home/tauri/**/inner/file"));
assert!(scope.is_allowed("/home/tauri/inner/folder/anyfile"));
assert!(scope.is_allowed("/home/tauri/anyfile"));
}
#[cfg(windows)]
{
scope.allow_directory("C:\\home\\tauri", true).unwrap();
scope
.forbid_directory("C:\\home\\tauri\\**", false)
.unwrap();
assert!(!scope.is_allowed("C:\\home\\tauri\\**"));
assert!(!scope.is_allowed("C:\\home\\tauri\\**\\file"));
assert!(scope.is_allowed("C:\\home\\tauri\\**\\inner\\file"));
assert!(scope.is_allowed("C:\\home\\tauri\\inner\\folder\\anyfile"));
assert!(scope.is_allowed("C:\\home\\tauri\\anyfile"));
}
let scope = new_scope();
#[cfg(unix)]
{
scope.allow_directory("/home/tauri", true).unwrap();
scope.forbid_file("/home/tauri/**").unwrap();
assert!(!scope.is_allowed("/home/tauri/**"));
assert!(scope.is_allowed("/home/tauri/**/file"));
assert!(scope.is_allowed("/home/tauri/**/inner/file"));
assert!(scope.is_allowed("/home/tauri/anyfile"));
}
#[cfg(windows)]
{
scope.allow_directory("C:\\home\\tauri", true).unwrap();
scope.forbid_file("C:\\home\\tauri\\**").unwrap();
assert!(!scope.is_allowed("C:\\home\\tauri\\**"));
assert!(scope.is_allowed("C:\\home\\tauri\\**\\file"));
assert!(scope.is_allowed("C:\\home\\tauri\\**\\inner\\file"));
assert!(scope.is_allowed("C:\\home\\tauri\\anyfile"));
}
let scope = new_scope();
#[cfg(unix)]
{
scope.allow_directory("/home/tauri", false).unwrap();
assert!(scope.is_allowed("/home/tauri/**"));
assert!(!scope.is_allowed("/home/tauri/**/file"));
assert!(!scope.is_allowed("/home/tauri/**/inner/file"));
assert!(scope.is_allowed("/home/tauri/anyfile"));
}
#[cfg(windows)]
{
scope.allow_directory("C:\\home\\tauri", false).unwrap();
assert!(scope.is_allowed("C:\\home\\tauri\\**"));
assert!(!scope.is_allowed("C:\\home\\tauri\\**\\file"));
assert!(!scope.is_allowed("C:\\home\\tauri\\**\\inner\\file"));
assert!(scope.is_allowed("C:\\home\\tauri\\anyfile"));
}
}
#[cfg(windows)]
#[test]
fn windows_root_paths() {
let scope = new_scope();
{
// UNC network path
scope.allow_directory("\\\\localhost\\c$", true).unwrap();
assert!(scope.is_allowed("\\\\localhost\\c$"));
assert!(scope.is_allowed("\\\\localhost\\c$\\Windows"));
assert!(scope.is_allowed("\\\\localhost\\c$\\NonExistentFile"));
assert!(!scope.is_allowed("\\\\localhost\\d$"));
assert!(!scope.is_allowed("\\\\OtherServer\\Share"));
}
let scope = new_scope();
{
// Verbatim UNC network path
scope
.allow_directory("\\\\?\\UNC\\localhost\\c$", true)
.unwrap();
assert!(scope.is_allowed("\\\\localhost\\c$"));
assert!(scope.is_allowed("\\\\localhost\\c$\\Windows"));
assert!(scope.is_allowed("\\\\?\\UNC\\localhost\\c$\\Windows\\NonExistentFile"));
// A non-existent file cannot be canonicalized to a verbatim UNC path, so this will fail to match
assert!(!scope.is_allowed("\\\\localhost\\c$\\Windows\\NonExistentFile"));
assert!(!scope.is_allowed("\\\\localhost\\d$"));
assert!(!scope.is_allowed("\\\\OtherServer\\Share"));
}
let scope = new_scope();
{
// Device namespace
scope.allow_file("\\\\.\\COM1").unwrap();
assert!(scope.is_allowed("\\\\.\\COM1"));
assert!(!scope.is_allowed("\\\\.\\COM2"));
}
let scope = new_scope();
{
// Disk root
scope.allow_directory("C:\\", true).unwrap();
assert!(scope.is_allowed("C:\\Windows"));
assert!(scope.is_allowed("C:\\Windows\\system.ini"));
assert!(scope.is_allowed("C:\\NonExistentFile"));
assert!(!scope.is_allowed("D:\\home"));
}
let scope = new_scope();
{
// Verbatim disk root
scope.allow_directory("\\\\?\\C:\\", true).unwrap();
assert!(scope.is_allowed("C:\\Windows"));
assert!(scope.is_allowed("C:\\Windows\\system.ini"));
assert!(scope.is_allowed("C:\\NonExistentFile"));
assert!(!scope.is_allowed("D:\\home"));
}
let scope = new_scope();
{
// Verbatim path
scope.allow_file("\\\\?\\anyfile").unwrap();
assert!(scope.is_allowed("\\\\?\\anyfile"));
assert!(!scope.is_allowed("\\\\?\\otherfile"));
}
}
#[test]
fn push_pattern_generated_paths() {
macro_rules! assert_pattern {
($patterns:ident, $pattern:literal) => {
assert!($patterns.contains(&Pattern::new($pattern).unwrap()))
};
}
let mut patterns = HashSet::new();
#[cfg(not(windows))]
{
push_pattern(&mut patterns, "/path/to/dir/", Pattern::new).expect("failed to push pattern");
push_pattern(&mut patterns, "/path/to/dir/**", Pattern::new).expect("failed to push pattern");
assert_pattern!(patterns, "/path/to/dir");
assert_pattern!(patterns, "/path/to/dir/**");
}
#[cfg(windows)]
{
push_pattern(&mut patterns, "C:\\path\\to\\dir", Pattern::new)
.expect("failed to push pattern");
push_pattern(&mut patterns, "C:\\path\\to\\dir\\**", Pattern::new)
.expect("failed to push pattern");
assert_pattern!(patterns, "C:\\path\\to\\dir");
assert_pattern!(patterns, "C:\\path\\to\\dir\\**");
assert_pattern!(patterns, "\\\\?\\C:\\path\\to\\dir");
assert_pattern!(patterns, "\\\\?\\C:\\path\\to\\dir\\**");
}
}
#[test]
fn event_no_deadlocks() {
let scope = new_scope();
let scope_clone = scope.clone();
scope.once(move |event| {
assert!(matches!(event, super::Event::PathAllowed(_)));
scope_clone.allow_file("/another-test-path").unwrap();
});
scope.allow_file("/test-path").unwrap();
}
}

View File

@@ -0,0 +1,80 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
//! Runtime access scopes.
//!
//! A scope narrows what an allowed feature may actually touch at runtime.
//! Tauri has two complementary scoping mechanisms:
//!
//! - **The IPC scope**, which is part of the [Access Control List](crate::ipc::RuntimeAuthority):
//! capabilities define which commands each window/webview and origin may call, and the scope
//! attached to a permission is read from a command with [`CommandScope`](crate::ipc::CommandScope)
//! and [`GlobalScope`](crate::ipc::GlobalScope). That scope is static - it is resolved from the
//! capability files at compile time (or added at runtime with
//! [`Manager::add_capability`](crate::Manager::add_capability)) - and it is the command
//! implementation that decides what to do with it.
//! - **The file system scope** in [`mod@fs`], a mutable list of allowed and forbidden glob patterns
//! that the [asset protocol](https://v2.tauri.app/security/asset-protocol/) checks before serving
//! a file. It is seeded from `app > security > assetProtocol > scope` in the configuration and
//! can be changed while the application is running.
//!
//! Use `Manager::asset_protocol_scope` (`protocol-asset` Cargo feature) to get the asset
//! protocol scope and [`fs::Scope::allow_directory`], [`fs::Scope::allow_file`],
//! [`fs::Scope::forbid_directory`] and [`fs::Scope::forbid_file`] to change it, for instance to
//! grant access to a file the user just picked in a dialog:
//!
//! ```rust
//! # #[cfg(feature = "protocol-asset")]
//! # fn run() {
//! use tauri::Manager;
//!
//! tauri::Builder::default()
//! .setup(|app| {
//! app.asset_protocol_scope().allow_directory("/home/user/pictures", true)?;
//! Ok(())
//! });
//! # }
//! ```
//!
//! Scope changes are not persisted: they are lost when the application restarts.
//! Use the [persisted scope plugin](https://v2.tauri.app/plugin/persisted-scope/)
//! if you need them to survive a restart.
/// FS scope.
pub mod fs;
use std::path::Path;
/// Unique id of a scope event.
pub type ScopeEventId = u32;
/// Managed state for all the core scopes in a tauri application.
pub struct Scopes {
#[cfg(feature = "protocol-asset")]
pub(crate) asset_protocol: fs::Scope,
}
#[allow(unused)]
impl Scopes {
/// Allows a directory on the scopes.
pub fn allow_directory<P: AsRef<Path>>(&self, path: P, recursive: bool) -> crate::Result<()> {
#[cfg(feature = "protocol-asset")]
self.asset_protocol.allow_directory(path, recursive)?;
Ok(())
}
/// Allows a file on the scopes.
pub fn allow_file<P: AsRef<Path>>(&self, path: P) -> crate::Result<()> {
#[cfg(feature = "protocol-asset")]
self.asset_protocol.allow_file(path)?;
Ok(())
}
/// Forbids a file on the scopes.
pub fn forbid_file<P: AsRef<Path>>(&self, path: P) -> crate::Result<()> {
#[cfg(feature = "protocol-asset")]
self.asset_protocol.forbid_file(path)?;
Ok(())
}
}

291
crates/tauri/src/state.rs Normal file
View File

@@ -0,0 +1,291 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::{
any::{Any, TypeId},
collections::HashMap,
hash::BuildHasherDefault,
sync::Mutex,
};
use crate::{
Manager, Runtime,
ipc::{CommandArg, CommandItem, InvokeError},
};
/// A guard for a state value.
///
/// See [`Manager::manage`](`crate::Manager::manage`) for usage examples.
pub struct State<'r, T>(&'r T);
impl<'r, T> State<'r, T> {
/// Retrieve a borrow to the underlying value with a lifetime of `'r`.
/// Using this method is typically unnecessary as `State` implements
/// [`std::ops::Deref`] with a [`std::ops::Deref::Target`] of `T`.
#[inline(always)]
pub fn inner(&self) -> &'r T {
self.0
}
}
impl<T> std::ops::Deref for State<'_, T> {
type Target = T;
#[inline(always)]
fn deref(&self) -> &T {
self.0
}
}
impl<T> Clone for State<'_, T> {
fn clone(&self) -> Self {
State(self.0)
}
}
impl<T: PartialEq> PartialEq for State<'_, T> {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
impl<T: std::fmt::Debug> std::fmt::Debug for State<'_, T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("State").field(&self.0).finish()
}
}
impl<'r, 'de: 'r, T: Send + Sync + 'static, R: Runtime> CommandArg<'de, R> for State<'r, T> {
/// Grabs the [`State`] from the [`CommandItem`]. This will never fail.
fn from_command(command: CommandItem<'de, R>) -> Result<Self, InvokeError> {
command.message.webview_ref().try_state().ok_or_else(|| {
InvokeError::from_anyhow(anyhow::anyhow!(
"state not managed for field `{}` on command `{}`. You must call `.manage()` before using this command",
command.key, command.name
))
})
}
}
// Taken from: https://github.com/SergioBenitez/state/blob/556c1b94db8ce8427a0e72de7983ab5a9af4cc41/src/ident_hash.rs
// This is a _super_ stupid hash. It just uses its input as the hash value. This
// hash is meant to be used _only_ for "prehashed" values. In particular, we use
// this so that hashing a TypeId is essentially a noop. This is because TypeIds
// are already unique integers.
#[derive(Default)]
struct IdentHash(u64);
impl std::hash::Hasher for IdentHash {
fn finish(&self) -> u64 {
self.0
}
fn write(&mut self, bytes: &[u8]) {
for byte in bytes {
self.write_u8(*byte);
}
}
fn write_u8(&mut self, i: u8) {
self.0 = (self.0 << 8) | (i as u64);
}
fn write_u64(&mut self, i: u64) {
self.0 = i;
}
}
/// Safety:
/// - The `key` must equal to `(*value).type_id()`, see the safety doc in methods of [StateManager] for details.
/// - Once you insert a value, you can't remove/mutated/move it anymore, see [StateManager::try_get] for details.
type TypeIdMap = HashMap<TypeId, Box<dyn Any + Sync + Send>, BuildHasherDefault<IdentHash>>;
/// The Tauri state manager.
// TODO: make private for v3
#[derive(Debug)]
pub struct StateManager {
map: Mutex<TypeIdMap>,
}
impl StateManager {
pub(crate) fn new() -> Self {
Self {
map: Default::default(),
}
}
pub(crate) fn set<T: Send + Sync + 'static>(&self, state: T) -> bool {
let mut map = self.map.lock().unwrap();
let type_id = TypeId::of::<T>();
let already_set = map.contains_key(&type_id);
if !already_set {
let state = Box::new(state) as Box<dyn Any + Sync + Send>;
map.insert(type_id, state);
}
!already_set
}
/// SAFETY: Calling this method will move the `value`,
/// which will cause references obtained through [Self::try_get] to dangle.
pub(crate) unsafe fn unmanage<T: Send + Sync + 'static>(&self) -> Option<T> {
let mut map = self.map.lock().unwrap();
let type_id = TypeId::of::<T>();
let state = map.remove(&type_id)?;
let value = state
.downcast::<T>()
.expect("the type of the key should be same as the type of the value");
Some(*value)
}
/// Gets the state associated with the specified type.
pub fn get<T: 'static>(&self) -> State<'_, T> {
self
.try_get()
.unwrap_or_else(|| panic!("state not found for type {}", std::any::type_name::<T>()))
}
/// Gets the state associated with the specified type.
pub fn try_get<T: 'static>(&self) -> Option<State<'_, T>> {
let map = self.map.lock().unwrap();
let type_id = TypeId::of::<T>();
let state = map.get(&type_id)?;
let value = state
.downcast_ref::<T>()
.expect("the type of the key should be same as the type of the value");
// SAFETY: We ensure the lifetime of `value` is the same as [StateManager] and `value` will not be mutated/moved.
let v_ref = unsafe { &*(value as *const T) };
Some(State(v_ref))
}
}
// Ported from https://github.com/SergioBenitez/state/blob/556c1b94db8ce8427a0e72de7983ab5a9af4cc41/tests/main.rs
#[cfg(test)]
mod tests {
use super::StateManager;
use std::sync::{Arc, RwLock};
use std::thread;
// Tiny structures to test that dropping works as expected.
struct DroppingStruct(Arc<RwLock<bool>>);
struct DroppingStructWrap(#[allow(dead_code)] DroppingStruct);
impl Drop for DroppingStruct {
fn drop(&mut self) {
*self.0.write().unwrap() = true;
}
}
#[test]
#[should_panic(expected = "state not found for type core::option::Option<alloc::string::String>")]
fn get_panics() {
let state = StateManager::new();
state.get::<Option<String>>();
}
#[test]
fn simple_set_get() {
let state = StateManager::new();
assert!(state.set(1u32));
assert_eq!(*state.get::<u32>(), 1);
}
#[test]
fn simple_set_get_unmanage() {
let state = StateManager::new();
assert!(state.set(1u32));
assert_eq!(*state.get::<u32>(), 1);
// safety: the reference returned by `try_get` is already dropped.
assert!(unsafe { state.unmanage::<u32>() }.is_some());
assert!(unsafe { state.unmanage::<u32>() }.is_none());
assert_eq!(state.try_get::<u32>(), None);
assert!(state.set(2u32));
assert_eq!(*state.get::<u32>(), 2);
}
#[test]
fn dst_set_get() {
let state = StateManager::new();
assert!(state.set::<[u32; 4]>([1, 2, 3, 4u32]));
assert_eq!(*state.get::<[u32; 4]>(), [1, 2, 3, 4]);
}
#[test]
fn set_get_remote() {
let state = Arc::new(StateManager::new());
let sate_ = Arc::clone(&state);
thread::spawn(move || {
sate_.set(10isize);
})
.join()
.unwrap();
assert_eq!(*state.get::<isize>(), 10);
}
#[test]
fn two_put_get() {
let state = StateManager::new();
assert!(state.set("Hello, world!".to_string()));
let s_old = state.get::<String>();
assert_eq!(*s_old, "Hello, world!");
assert!(!state.set::<String>("Bye bye!".into()));
assert_eq!(*state.get::<String>(), "Hello, world!");
assert_eq!(state.get::<String>(), s_old);
}
#[test]
fn many_puts_only_one_succeeds() {
let state = Arc::new(StateManager::new());
let mut threads = vec![];
for _ in 0..1000 {
let state_ = Arc::clone(&state);
threads.push(thread::spawn(move || state_.set(10i64)))
}
let results: Vec<bool> = threads.into_iter().map(|t| t.join().unwrap()).collect();
assert_eq!(results.into_iter().filter(|&b| b).count(), 1);
assert_eq!(*state.get::<i64>(), 10);
}
// Ensure setting when already set doesn't cause a drop.
#[test]
fn test_no_drop_on_set() {
let state = StateManager::new();
let drop_flag = Arc::new(RwLock::new(false));
let dropping_struct = DroppingStruct(drop_flag.clone());
let _drop_flag_ignore = Arc::new(RwLock::new(false));
let _dropping_struct_ignore = DroppingStruct(_drop_flag_ignore);
state.set::<DroppingStruct>(dropping_struct);
assert!(!state.set::<DroppingStruct>(_dropping_struct_ignore));
assert!(!*drop_flag.read().unwrap());
}
// Ensure dropping a type_map drops its contents.
#[test]
fn drop_inners_on_drop() {
let drop_flag_a = Arc::new(RwLock::new(false));
let dropping_struct_a = DroppingStruct(drop_flag_a.clone());
let drop_flag_b = Arc::new(RwLock::new(false));
let dropping_struct_b = DroppingStructWrap(DroppingStruct(drop_flag_b.clone()));
{
let state = StateManager::new();
state.set(dropping_struct_a);
assert!(!*drop_flag_a.read().unwrap());
state.set(dropping_struct_b);
assert!(!*drop_flag_a.read().unwrap());
assert!(!*drop_flag_b.read().unwrap());
}
assert!(*drop_flag_a.read().unwrap());
assert!(*drop_flag_b.read().unwrap());
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,338 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
//! Utilities for unit testing on Tauri applications.
//!
//! # Stability
//!
//! This module is unstable.
//!
//! # Examples
//!
//! ```rust
//! use tauri::test::{mock_builder, mock_context, noop_assets};
//!
//! #[tauri::command]
//! fn ping() -> &'static str {
//! "pong"
//! }
//!
//! fn create_app<R: tauri::Runtime>(builder: tauri::Builder<R>) -> tauri::App<R> {
//! builder
//! .invoke_handler(tauri::generate_handler![ping])
//! // remove the string argument to use your app's config file
//! .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json"))
//! .expect("failed to build app")
//! }
//!
//! fn main() {
//! // Use `tauri::Builder::default()` to use the default runtime rather than the `MockRuntime`;
//! // let app = create_app(tauri::Builder::default());
//! let app = create_app(mock_builder());
//! let webview = tauri::WebviewWindowBuilder::new(&app, "main", Default::default()).build().unwrap();
//!
//! // run the `ping` command and assert it returns `pong`
//! let res = tauri::test::get_ipc_response(
//! &webview,
//! tauri::webview::InvokeRequest {
//! cmd: "ping".into(),
//! callback: tauri::ipc::CallbackFn(0),
//! error: tauri::ipc::CallbackFn(1),
//! // alternatively use "tauri://localhost"
//! url: "http://tauri.localhost".parse().unwrap(),
//! body: tauri::ipc::InvokeBody::default(),
//! headers: Default::default(),
//! invoke_key: tauri::test::INVOKE_KEY.to_string(),
//! },
//! ).map(|b| b.deserialize::<String>().unwrap());
//! }
//! ```
#![allow(unused_variables)]
mod mock_runtime;
pub use mock_runtime::*;
use serde::Serialize;
use serialize_to_javascript::DefaultTemplate;
use std::{borrow::Cow, collections::HashMap, fmt::Debug};
use crate::{
App, Assets, Builder, Context, Pattern, Runtime, Webview,
ipc::{InvokeError, InvokeResponse, InvokeResponseBody},
webview::InvokeRequest,
};
use tauri_utils::{
acl::resolved::Resolved,
assets::{AssetKey, AssetsIter, CspHash},
config::{AppConfig, Config},
};
/// The invoke key used for tests.
pub const INVOKE_KEY: &str = "__invoke-key__";
/// An empty [`Assets`] implementation.
pub struct NoopAsset {
assets: HashMap<String, Vec<u8>>,
csp_hashes: Vec<CspHash<'static>>,
}
impl<R: Runtime> Assets<R> for NoopAsset {
fn get(&self, key: &AssetKey) -> Option<Cow<'_, [u8]>> {
None
}
fn iter(&self) -> Box<AssetsIter<'_>> {
Box::new(
self
.assets
.iter()
.map(|(k, b)| (Cow::Borrowed(k.as_str()), Cow::Borrowed(b.as_slice()))),
)
}
fn csp_hashes(&self, html_path: &AssetKey) -> Box<dyn Iterator<Item = CspHash<'_>> + '_> {
Box::new(self.csp_hashes.iter().copied())
}
}
/// Creates a new empty [`Assets`] implementation.
pub fn noop_assets() -> NoopAsset {
NoopAsset {
assets: Default::default(),
csp_hashes: Default::default(),
}
}
/// Creates a new [`crate::Context`] for testing.
pub fn mock_context<R: Runtime, A: Assets<R>>(assets: A) -> crate::Context<R> {
Context {
config: Config {
schema: None,
product_name: Default::default(),
main_binary_name: Default::default(),
version: Default::default(),
identifier: Default::default(),
app: AppConfig {
with_global_tauri: Default::default(),
windows: Vec::new(),
security: Default::default(),
tray_icon: None,
macos_private_api: false,
enable_gtk_app_id: false,
},
bundle: Default::default(),
build: Default::default(),
plugins: Default::default(),
},
assets: Box::new(assets),
default_window_icon: None,
app_icon: None,
#[cfg(all(desktop, feature = "tray-icon"))]
tray_icon: None,
package_info: crate::PackageInfo {
name: "test".into(),
version: "0.1.0".parse().unwrap(),
authors: "Tauri",
description: "Tauri test",
crate_name: "test",
},
pattern: Pattern::Brownfield,
runtime_authority: crate::runtime_authority!(Default::default(), Resolved::default()),
plugin_global_api_scripts: None,
#[cfg(dev)]
config_parent: None,
}
}
/// Creates a new [`Builder`] using the [`MockRuntime`].
///
/// To use a dummy [`Context`], see [`mock_app`].
///
/// # Examples
///
/// ```rust
/// #[cfg(test)]
/// fn do_something() {
/// let app = tauri::test::mock_builder()
/// // remove the string argument to use your app's config file
/// .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json"))
/// .unwrap();
/// }
/// ```
pub fn mock_builder() -> Builder<MockRuntime> {
let mut builder = Builder::<MockRuntime>::new().enable_macos_default_menu(false);
builder.invoke_initialization_script = crate::app::InvokeInitializationScript {
process_ipc_message_fn: crate::manager::webview::PROCESS_IPC_MESSAGE_FN,
os_name: std::env::consts::OS,
fetch_channel_data_command: crate::ipc::channel::FETCH_CHANNEL_DATA_COMMAND,
invoke_key: INVOKE_KEY,
}
.render_default(&Default::default())
.unwrap()
.into_string();
builder.invoke_key = INVOKE_KEY.to_string();
builder
}
/// Creates a new [`App`] for testing using the [`mock_context`] with a [`noop_assets`].
pub fn mock_app() -> App<MockRuntime> {
mock_builder().build(mock_context(noop_assets())).unwrap()
}
/// Executes the given IPC message and assert the response matches the expected value.
///
/// # Examples
///
/// ```rust
/// use tauri::test::{mock_builder, mock_context, noop_assets};
///
/// #[tauri::command]
/// fn ping() -> &'static str {
/// "pong"
/// }
///
/// fn create_app<R: tauri::Runtime>(builder: tauri::Builder<R>) -> tauri::App<R> {
/// builder
/// .invoke_handler(tauri::generate_handler![ping])
/// // remove the string argument to use your app's config file
/// .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json"))
/// .expect("failed to build app")
/// }
///
/// fn main() {
/// let app = create_app(mock_builder());
/// let webview = tauri::WebviewWindowBuilder::new(&app, "main", Default::default()).build().unwrap();
///
/// // run the `ping` command and assert it returns `pong`
/// tauri::test::assert_ipc_response(
/// &webview,
/// tauri::webview::InvokeRequest {
/// cmd: "ping".into(),
/// callback: tauri::ipc::CallbackFn(0),
/// error: tauri::ipc::CallbackFn(1),
/// url: if cfg!(any(windows, target_os = "android")) {
/// "http://tauri.localhost"
/// } else {
/// "tauri://localhost"
/// }.parse().unwrap(),
/// body: tauri::ipc::InvokeBody::default(),
/// headers: Default::default(),
/// invoke_key: tauri::test::INVOKE_KEY.to_string(),
/// },
/// Ok("pong")
/// );
/// }
/// ```
pub fn assert_ipc_response<
T: Serialize + Debug + Send + Sync + 'static,
W: AsRef<Webview<MockRuntime>>,
>(
webview: &W,
request: InvokeRequest,
expected: Result<T, T>,
) {
let response =
get_ipc_response(webview, request).map(|b| b.deserialize::<serde_json::Value>().unwrap());
assert_eq!(
response,
expected
.map(|e| serde_json::to_value(e).unwrap())
.map_err(|e| serde_json::to_value(e).unwrap())
);
}
#[allow(clippy::needless_doctest_main)]
/// Executes the given IPC message and get the return value.
///
/// # Examples
///
/// ```rust
/// use tauri::test::{mock_builder, mock_context, noop_assets};
///
/// #[tauri::command]
/// fn ping() -> &'static str {
/// "pong"
/// }
///
/// fn create_app<R: tauri::Runtime>(builder: tauri::Builder<R>) -> tauri::App<R> {
/// builder
/// .invoke_handler(tauri::generate_handler![ping])
/// // remove the string argument to use your app's config file
/// .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json"))
/// .expect("failed to build app")
/// }
///
/// fn main() {
/// let app = create_app(mock_builder());
/// let webview = tauri::WebviewWindowBuilder::new(&app, "main", Default::default()).build().unwrap();
///
/// // run the `ping` command and assert it returns `pong`
/// let res = tauri::test::get_ipc_response(
/// &webview,
/// tauri::webview::InvokeRequest {
/// cmd: "ping".into(),
/// callback: tauri::ipc::CallbackFn(0),
/// error: tauri::ipc::CallbackFn(1),
/// url: if cfg!(any(windows, target_os = "android")) {
/// "http://tauri.localhost"
/// } else {
/// "tauri://localhost"
/// }.parse().unwrap(),
/// body: tauri::ipc::InvokeBody::default(),
/// headers: Default::default(),
/// invoke_key: tauri::test::INVOKE_KEY.to_string(),
/// },
/// );
/// assert!(res.is_ok());
/// assert_eq!(res.unwrap().deserialize::<String>().unwrap(), String::from("pong"));
/// }
///```
pub fn get_ipc_response<W: AsRef<Webview<MockRuntime>>>(
webview: &W,
request: InvokeRequest,
) -> Result<InvokeResponseBody, serde_json::Value> {
let (tx, rx) = std::sync::mpsc::sync_channel(1);
webview.as_ref().clone().on_message(
request,
Box::new(move |_window, _cmd, response, _callback, _error| {
tx.send(response).unwrap();
}),
);
let res = rx.recv().expect("Failed to receive result from command");
match res {
InvokeResponse::Ok(b) => Ok(b),
InvokeResponse::Err(InvokeError(v)) => Err(v),
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::mock_app;
#[test]
fn run_app() {
let app = mock_app();
let w = crate::WebviewWindowBuilder::new(&app, "main", Default::default())
.build()
.unwrap();
std::thread::spawn(move || {
std::thread::sleep(Duration::from_secs(1));
w.close().unwrap();
});
app.run(|_app, event| {
println!("{event:?}");
});
}
}

View File

@@ -0,0 +1,701 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
//! Tray icon types and utilities.
pub(crate) mod plugin;
use crate::app::{GlobalMenuEventListener, GlobalTrayIconEventListener};
use crate::menu::ContextMenu;
use crate::menu::MenuEvent;
use crate::resources::Resource;
use crate::{
AppHandle, Manager, PhysicalPosition, Rect, Runtime, image::Image, menu::run_item_main_thread,
};
use crate::{ResourceId, UnsafeSend};
use serde::Serialize;
use std::path::Path;
pub use tray_icon::TrayIconId;
/// Describes the mouse button state.
#[derive(Default, Clone, Copy, PartialEq, Eq, Debug, Serialize)]
pub enum MouseButtonState {
/// Mouse button pressed.
#[default]
Up,
/// Mouse button released.
Down,
}
impl From<tray_icon::MouseButtonState> for MouseButtonState {
fn from(value: tray_icon::MouseButtonState) -> Self {
match value {
tray_icon::MouseButtonState::Up => MouseButtonState::Up,
tray_icon::MouseButtonState::Down => MouseButtonState::Down,
}
}
}
/// Describes which mouse button triggered the event..
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Default)]
pub enum MouseButton {
/// Left mouse button.
#[default]
Left,
/// Right mouse button.
Right,
/// Middle mouse button.
Middle,
}
impl From<tray_icon::MouseButton> for MouseButton {
fn from(value: tray_icon::MouseButton) -> Self {
match value {
tray_icon::MouseButton::Left => MouseButton::Left,
tray_icon::MouseButton::Right => MouseButton::Right,
tray_icon::MouseButton::Middle => MouseButton::Middle,
}
}
}
/// Describes a tray icon event.
///
/// ## Platform-specific:
///
/// - **Linux**: Unsupported. The event is not emitted even though the icon is shown
/// and will still show a context menu on right click.
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "type")]
#[non_exhaustive]
pub enum TrayIconEvent {
/// A click happened on the tray icon.
#[serde(rename_all = "camelCase")]
Click {
/// Id of the tray icon which triggered this event.
id: TrayIconId,
/// Physical Position of this event.
position: PhysicalPosition<f64>,
/// Position and size of the tray icon.
rect: Rect,
/// Mouse button that triggered this event.
button: MouseButton,
/// Mouse button state when this event was triggered.
button_state: MouseButtonState,
},
/// A double click happened on the tray icon. **Windows Only**
DoubleClick {
/// Id of the tray icon which triggered this event.
id: TrayIconId,
/// Physical Position of this event.
position: PhysicalPosition<f64>,
/// Position and size of the tray icon.
rect: Rect,
/// Mouse button that triggered this event.
button: MouseButton,
},
/// The mouse entered the tray icon region.
Enter {
/// Id of the tray icon which triggered this event.
id: TrayIconId,
/// Physical Position of this event.
position: PhysicalPosition<f64>,
/// Position and size of the tray icon.
rect: Rect,
},
/// The mouse moved over the tray icon region.
Move {
/// Id of the tray icon which triggered this event.
id: TrayIconId,
/// Physical Position of this event.
position: PhysicalPosition<f64>,
/// Position and size of the tray icon.
rect: Rect,
},
/// The mouse left the tray icon region.
Leave {
/// Id of the tray icon which triggered this event.
id: TrayIconId,
/// Physical Position of this event.
position: PhysicalPosition<f64>,
/// Position and size of the tray icon.
rect: Rect,
},
}
impl TrayIconEvent {
/// Get the id of the tray icon that triggered this event.
pub fn id(&self) -> &TrayIconId {
match self {
TrayIconEvent::Click { id, .. } => id,
TrayIconEvent::DoubleClick { id, .. } => id,
TrayIconEvent::Enter { id, .. } => id,
TrayIconEvent::Move { id, .. } => id,
TrayIconEvent::Leave { id, .. } => id,
}
}
}
impl From<tray_icon::TrayIconEvent> for TrayIconEvent {
fn from(value: tray_icon::TrayIconEvent) -> Self {
match value {
tray_icon::TrayIconEvent::Click {
id,
position,
rect,
button,
button_state,
} => TrayIconEvent::Click {
id,
position,
rect: Rect {
position: rect.position.into(),
size: rect.size.into(),
},
button: button.into(),
button_state: button_state.into(),
},
tray_icon::TrayIconEvent::DoubleClick {
id,
position,
rect,
button,
} => TrayIconEvent::DoubleClick {
id,
position,
rect: Rect {
position: rect.position.into(),
size: rect.size.into(),
},
button: button.into(),
},
tray_icon::TrayIconEvent::Enter { id, position, rect } => TrayIconEvent::Enter {
id,
position,
rect: Rect {
position: rect.position.into(),
size: rect.size.into(),
},
},
tray_icon::TrayIconEvent::Move { id, position, rect } => TrayIconEvent::Move {
id,
position,
rect: Rect {
position: rect.position.into(),
size: rect.size.into(),
},
},
tray_icon::TrayIconEvent::Leave { id, position, rect } => TrayIconEvent::Leave {
id,
position,
rect: Rect {
position: rect.position.into(),
size: rect.size.into(),
},
},
_ => todo!(),
}
}
}
/// [`TrayIcon`] builder struct and associated methods.
#[derive(Default)]
pub struct TrayIconBuilder<R: Runtime> {
on_menu_event: Option<GlobalMenuEventListener<AppHandle<R>>>,
on_tray_icon_event: Option<GlobalTrayIconEventListener<TrayIcon<R>>>,
inner: tray_icon::TrayIconBuilder,
}
impl<R: Runtime> TrayIconBuilder<R> {
/// Creates a new tray icon builder.
///
/// ## Platform-specific:
///
/// - **Linux:** Sometimes the icon won't be visible unless a menu is set.
/// Setting an empty [`Menu`](crate::menu::Menu) is enough.
pub fn new() -> Self {
Self {
inner: tray_icon::TrayIconBuilder::new(),
on_menu_event: None,
on_tray_icon_event: None,
}
}
/// Creates a new tray icon builder with the specified id.
///
/// ## Platform-specific:
///
/// - **Linux:** Sometimes the icon won't be visible unless a menu is set.
/// Setting an empty [`Menu`](crate::menu::Menu) is enough.
pub fn with_id<I: Into<TrayIconId>>(id: I) -> Self {
let mut builder = Self::new();
builder.inner = builder.inner.with_id(id);
builder
}
/// Set the a menu for this tray icon.
///
/// ## Platform-specific:
///
/// - **Linux**: once a menu is set, it cannot be removed or replaced but you can change its content.
pub fn menu<M: ContextMenu>(mut self, menu: &M) -> Self {
self.inner = self.inner.with_menu(menu.inner_context_owned());
self
}
/// Set an icon for this tray icon.
///
/// ## Platform-specific:
///
/// - **Linux:** Sometimes the icon won't be visible unless a menu is set.
/// Setting an empty [`Menu`](crate::menu::Menu) is enough.
pub fn icon(mut self, icon: Image<'_>) -> Self {
let icon = icon.try_into().ok();
if let Some(icon) = icon {
self.inner = self.inner.with_icon(icon);
}
self
}
/// Set a tooltip for this tray icon.
///
/// ## Platform-specific:
///
/// - **Linux:** Unsupported.
pub fn tooltip<S: AsRef<str>>(mut self, s: S) -> Self {
self.inner = self.inner.with_tooltip(s);
self
}
/// Set the tray icon title.
///
/// ## Platform-specific
///
/// - **Linux:** The title will not be shown unless there is an icon
/// as well. The title is useful for numerical and other frequently
/// updated information. In general, it shouldn't be shown unless a
/// user requests it as it can take up a significant amount of space
/// on the user's panel. This may not be shown in all visualizations.
/// - **Windows:** Unsupported.
pub fn title<S: AsRef<str>>(mut self, title: S) -> Self {
self.inner = self.inner.with_title(title);
self
}
/// Set tray icon temp dir path. **Linux only**.
///
/// On Linux, we need to write the icon to the disk and usually it will
/// be `$XDG_RUNTIME_DIR/tray-icon` or `$TEMP/tray-icon`.
pub fn temp_dir_path<P: AsRef<Path>>(mut self, s: P) -> Self {
self.inner = self.inner.with_temp_dir_path(s);
self
}
/// Use the icon as a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc). **macOS only**.
pub fn icon_as_template(mut self, is_template: bool) -> Self {
self.inner = self.inner.with_icon_as_template(is_template);
self
}
/// Whether to show the tray menu on left click or not, default is `true`.
///
/// ## Platform-specific:
///
/// - **Linux:** Unsupported.
#[deprecated(
since = "2.2.0",
note = "Use `TrayIconBuilder::show_menu_on_left_click` instead."
)]
pub fn menu_on_left_click(mut self, enable: bool) -> Self {
self.inner = self.inner.with_menu_on_left_click(enable);
self
}
/// Whether to show the tray menu on left click or not, default is `true`.
///
/// ## Platform-specific:
///
/// - **Linux:** Unsupported.
pub fn show_menu_on_left_click(mut self, enable: bool) -> Self {
self.inner = self.inner.with_menu_on_left_click(enable);
self
}
/// Set a handler for menu events.
///
/// Note that this handler is called for any menu event,
/// whether it is coming from this window, another window or from the tray icon menu.
pub fn on_menu_event<F: Fn(&AppHandle<R>, MenuEvent) + Sync + Send + 'static>(
mut self,
f: F,
) -> Self {
self.on_menu_event.replace(Box::new(f));
self
}
/// Set a handler for this tray icon events.
pub fn on_tray_icon_event<F: Fn(&TrayIcon<R>, TrayIconEvent) + Sync + Send + 'static>(
mut self,
f: F,
) -> Self {
self.on_tray_icon_event.replace(Box::new(f));
self
}
/// Access the unique id that will be assigned to the tray icon
/// this builder will create.
pub fn id(&self) -> &TrayIconId {
self.inner.id()
}
pub(crate) fn build_inner(
self,
app_handle: &AppHandle<R>,
) -> crate::Result<(TrayIcon<R>, ResourceId)> {
let id = self.id().clone();
// SAFETY:
// the menu within this builder was created on main thread
// and will be accessed on the main thread
let unsafe_builder = UnsafeSend(self.inner);
let (tx, rx) = std::sync::mpsc::channel();
let unsafe_tray = app_handle
.run_on_main_thread(move || {
// SAFETY: will only be accessed on main thread
let _ = tx.send(unsafe_builder.take().build().map(UnsafeSend));
})
.and_then(|_| rx.recv().map_err(|_| crate::Error::FailedToReceiveMessage))??;
let icon = TrayIcon {
id,
inner: unsafe_tray.take(),
app_handle: app_handle.clone(),
};
let rid = icon.register(
&icon.app_handle,
self.on_menu_event,
self.on_tray_icon_event,
);
Ok((icon, rid))
}
/// Builds and adds a new [`TrayIcon`] to the system tray.
pub fn build<M: Manager<R>>(self, manager: &M) -> crate::Result<TrayIcon<R>> {
let (icon, _rid) = self.build_inner(manager.app_handle())?;
Ok(icon)
}
}
/// Tray icon struct and associated methods.
///
/// This type is reference-counted and the icon is removed when the last instance is dropped.
///
/// See [`TrayIconBuilder`] to construct this type.
#[tauri_macros::default_runtime(crate::Wry, wry)]
pub struct TrayIcon<R: Runtime> {
id: TrayIconId,
inner: tray_icon::TrayIcon,
app_handle: AppHandle<R>,
}
impl<R: Runtime> Clone for TrayIcon<R> {
fn clone(&self) -> Self {
Self {
id: self.id.clone(),
inner: self.inner.clone(),
app_handle: self.app_handle.clone(),
}
}
}
/// # Safety
///
/// We make sure it always runs on the main thread.
unsafe impl<R: Runtime> Sync for TrayIcon<R> {}
unsafe impl<R: Runtime> Send for TrayIcon<R> {}
impl<R: Runtime> TrayIcon<R> {
fn register(
&self,
app_handle: &AppHandle<R>,
on_menu_event: Option<GlobalMenuEventListener<AppHandle<R>>>,
on_tray_icon_event: Option<GlobalTrayIconEventListener<TrayIcon<R>>>,
) -> ResourceId {
if let Some(handler) = on_menu_event {
app_handle
.manager
.menu
.global_event_listeners
.lock()
.unwrap()
.push(handler);
}
if let Some(handler) = on_tray_icon_event {
app_handle
.manager
.tray
.event_listeners
.lock()
.unwrap()
.insert(self.id.clone(), handler);
}
let rid = app_handle.resources_table().add(self.clone());
app_handle
.manager
.tray
.icons
.lock()
.unwrap()
.push((self.id().clone(), rid));
rid
}
/// The application handle associated with this type.
pub fn app_handle(&self) -> &AppHandle<R> {
&self.app_handle
}
/// Register a handler for menu events.
///
/// Note that this handler is called for any menu event,
/// whether it is coming from this window, another window or from the tray icon menu.
pub fn on_menu_event<F: Fn(&AppHandle<R>, MenuEvent) + Sync + Send + 'static>(&self, f: F) {
self
.app_handle
.manager
.menu
.global_event_listeners
.lock()
.unwrap()
.push(Box::new(f));
}
/// Register a handler for this tray icon events.
pub fn on_tray_icon_event<F: Fn(&TrayIcon<R>, TrayIconEvent) + Sync + Send + 'static>(
&self,
f: F,
) {
self
.app_handle
.manager
.tray
.event_listeners
.lock()
.unwrap()
.insert(self.id.clone(), Box::new(f));
}
/// Returns the id associated with this tray icon.
pub fn id(&self) -> &TrayIconId {
&self.id
}
/// Sets a new tray icon. If `None` is provided, it will remove the icon.
pub fn set_icon(&self, icon: Option<Image<'_>>) -> crate::Result<()> {
let icon = match icon {
Some(i) => Some(i.try_into()?),
None => None,
};
run_item_main_thread!(self, |self_: Self| self_.inner.set_icon(icon))?.map_err(Into::into)
}
/// Sets a new tray menu.
///
/// ## Platform-specific:
///
/// - **Linux**: once a menu is set it cannot be removed so `None` has no effect
pub fn set_menu<M: ContextMenu + 'static>(&self, menu: Option<M>) -> crate::Result<()> {
run_item_main_thread!(self, |self_: Self| {
self_.inner.set_menu(menu.map(|m| m.inner_context_owned()))
})
}
/// Sets the tooltip for this tray icon.
///
/// ## Platform-specific:
///
/// - **Linux:** Unsupported
pub fn set_tooltip<S: AsRef<str>>(&self, tooltip: Option<S>) -> crate::Result<()> {
let s = tooltip.map(|s| s.as_ref().to_string());
run_item_main_thread!(self, |self_: Self| self_.inner.set_tooltip(s))?.map_err(Into::into)
}
/// Sets the title for this tray icon.
///
/// ## Platform-specific:
///
/// - **Linux:** The title will not be shown unless there is an icon
/// as well. The title is useful for numerical and other frequently
/// updated information. In general, it shouldn't be shown unless a
/// user requests it as it can take up a significant amount of space
/// on the user's panel. This may not be shown in all visualizations.
/// - **Windows:** Unsupported
pub fn set_title<S: AsRef<str>>(&self, title: Option<S>) -> crate::Result<()> {
let s = title.map(|s| s.as_ref().to_string());
run_item_main_thread!(self, |self_: Self| self_.inner.set_title(s))
}
/// Show or hide this tray icon.
pub fn set_visible(&self, visible: bool) -> crate::Result<()> {
run_item_main_thread!(self, |self_: Self| self_.inner.set_visible(visible))?.map_err(Into::into)
}
/// Sets the tray icon temp dir path. **Linux only**.
///
/// On Linux, we need to write the icon to the disk and usually it will
/// be `$XDG_RUNTIME_DIR/tray-icon` or `$TEMP/tray-icon`.
pub fn set_temp_dir_path<P: AsRef<Path>>(&self, path: Option<P>) -> crate::Result<()> {
#[allow(unused)]
let p = path.map(|p| p.as_ref().to_path_buf());
#[cfg(target_os = "linux")]
run_item_main_thread!(self, |self_: Self| self_.inner.set_temp_dir_path(p))?;
Ok(())
}
/// Sets the current icon as a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc). **macOS only**.
pub fn set_icon_as_template(&self, #[allow(unused)] is_template: bool) -> crate::Result<()> {
#[cfg(target_os = "macos")]
run_item_main_thread!(self, |self_: Self| {
self_.inner.set_icon_as_template(is_template)
})?;
Ok(())
}
/// Sets the tray icon and template status atomically. **macOS only**.
///
/// On macOS, calling `set_icon` followed by `set_icon_as_template` causes a visible
/// flicker as the icon is rendered twice. This method sets both atomically to prevent that.
///
/// ## Platform-specific:
///
/// - **Linux / Windows:** Falls back to calling `set_icon`.
pub fn set_icon_with_as_template(
&self,
icon: Option<Image<'_>>,
#[allow(unused)] is_template: bool,
) -> crate::Result<()> {
#[cfg(target_os = "macos")]
{
let tray_icon = match icon {
Some(i) => Some(i.try_into()?),
None => None,
};
run_item_main_thread!(self, |self_: Self| {
self_
.inner
.set_icon_with_as_template(tray_icon, is_template)
})??;
}
#[cfg(not(target_os = "macos"))]
{
self.set_icon(icon)?;
}
Ok(())
}
/// Disable or enable showing the tray menu on left click.
///
///
/// ## Platform-specific:
///
/// - **Linux**: Unsupported.
pub fn set_show_menu_on_left_click(&self, #[allow(unused)] enable: bool) -> crate::Result<()> {
#[cfg(any(target_os = "macos", windows))]
run_item_main_thread!(self, |self_: Self| {
self_.inner.set_show_menu_on_left_click(enable)
})?;
Ok(())
}
/// Get tray icon rect.
///
/// ## Platform-specific:
///
/// - **Linux**: Unsupported, always returns `None`.
pub fn rect(&self) -> crate::Result<Option<crate::Rect>> {
run_item_main_thread!(self, |self_: Self| {
self_.inner.rect().map(|rect| Rect {
position: rect.position.into(),
size: rect.size.into(),
})
})
}
/// Do something with the inner [`tray_icon::TrayIcon`] on main thread
///
/// Note that `tray-icon` crate may be updated in minor releases of Tauri.
/// Therefore, it’s recommended to pin Tauri to at least a minor version when you’re using `with_inner_tray_icon`.
pub fn with_inner_tray_icon<F, T>(&self, f: F) -> crate::Result<T>
where
F: FnOnce(&tray_icon::TrayIcon) -> T + Send + 'static,
T: Send + 'static,
{
run_item_main_thread!(self, |self_: Self| { f(&self_.inner) })
}
}
impl<R: Runtime> Resource for TrayIcon<R> {
fn close(self: std::sync::Arc<Self>) {
let mut icons = self.app_handle.manager.tray.icons.lock().unwrap();
for (i, (tray_icon_id, _rid)) in icons.iter_mut().enumerate() {
if tray_icon_id == &self.id {
icons.swap_remove(i);
return;
}
}
}
}
#[cfg(test)]
mod tests {
#[test]
fn tray_event_json_serialization() {
// NOTE: if this test is ever changed, you probably need to change `TrayIconEvent` in JS as well
use super::*;
let event = TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Down,
id: TrayIconId::new("id"),
position: crate::PhysicalPosition::default(),
rect: crate::Rect {
position: tray_icon::Rect::default().position.into(),
size: tray_icon::Rect::default().size.into(),
},
};
let value = serde_json::to_value(&event).unwrap();
assert_eq!(
value,
serde_json::json!({
"type": "Click",
"button": "Left",
"buttonState": "Down",
"id": "id",
"position": {
"x": 0.0,
"y": 0.0,
},
"rect": {
"size": {
"Physical": {
"width": 0,
"height": 0,
}
},
"position": {
"Physical": {
"x": 0,
"y": 0,
}
},
}
})
);
}
}

View File

@@ -0,0 +1,253 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::Context;
use serde::Deserialize;
use crate::{
AppHandle, Manager, Runtime, Webview, command,
image::JsImage,
ipc::Channel,
menu::{Menu, Submenu, plugin::ItemKind},
plugin::{Builder, TauriPlugin},
resources::ResourceId,
tray::TrayIconBuilder,
};
use super::{TrayIcon, TrayIconEvent};
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct TrayIconOptions {
id: Option<String>,
menu: Option<(ResourceId, ItemKind)>,
icon: Option<JsImage>,
tooltip: Option<String>,
title: Option<String>,
temp_dir_path: Option<PathBuf>,
icon_as_template: Option<bool>,
menu_on_left_click: Option<bool>,
show_menu_on_left_click: Option<bool>,
}
#[command(root = "crate")]
fn new<R: Runtime>(
webview: Webview<R>,
options: TrayIconOptions,
handler: Channel<TrayIconEvent>,
) -> crate::Result<(ResourceId, String)> {
let mut builder = if let Some(id) = options.id {
TrayIconBuilder::<R>::with_id(id)
} else {
TrayIconBuilder::<R>::new()
};
builder = builder.on_tray_icon_event(move |_tray, e| {
let _ = handler.send(e);
});
let resources_table = webview.resources_table();
if let Some((rid, kind)) = options.menu {
match kind {
ItemKind::Menu => {
let menu = resources_table.get::<Menu<R>>(rid)?;
builder = builder.menu(&*menu);
}
ItemKind::Submenu => {
let submenu = resources_table.get::<Submenu<R>>(rid)?;
builder = builder.menu(&*submenu);
}
_ => return Err(crate::Error::UnexpectedMenuKind),
};
}
if let Some(icon) = options.icon {
builder = builder.icon(Arc::unwrap_or_clone(icon.into_img(&resources_table)?));
}
if let Some(tooltip) = options.tooltip {
builder = builder.tooltip(tooltip);
}
if let Some(title) = options.title {
builder = builder.title(title);
}
if let Some(temp_dir_path) = options.temp_dir_path {
builder = builder.temp_dir_path(temp_dir_path);
}
if let Some(icon_as_template) = options.icon_as_template {
builder = builder.icon_as_template(icon_as_template);
}
#[allow(deprecated)]
if let Some(menu_on_left_click) = options.menu_on_left_click {
builder = builder.menu_on_left_click(menu_on_left_click);
}
if let Some(show_menu_on_left_click) = options.show_menu_on_left_click {
builder = builder.show_menu_on_left_click(show_menu_on_left_click);
}
let (tray, rid) = builder.build_inner(webview.app_handle())?;
let id = tray.id().as_ref().to_string();
Ok((rid, id))
}
#[command(root = "crate")]
fn get_by_id<R: Runtime>(app: AppHandle<R>, id: &str) -> Option<ResourceId> {
app.manager.tray.tray_resource_by_id(id)
}
#[command(root = "crate")]
fn remove_by_id<R: Runtime>(app: AppHandle<R>, id: &str) -> crate::Result<()> {
app
.remove_tray_by_id(id)
.with_context(|| format!("Can't find a tray associated with this id: {id}"))?;
Ok(())
}
#[command(root = "crate")]
fn set_icon<R: Runtime>(
app: AppHandle<R>,
webview: Webview<R>,
rid: ResourceId,
icon: Option<JsImage>,
) -> crate::Result<()> {
let resources_table = app.resources_table();
let tray = resources_table.get::<TrayIcon<R>>(rid)?;
let webview_resources_table = webview.resources_table();
let icon = match icon {
Some(i) => Some(Arc::unwrap_or_clone(i.into_img(&webview_resources_table)?)),
None => None,
};
tray.set_icon(icon)
}
#[command(root = "crate")]
fn set_menu<R: Runtime>(
app: AppHandle<R>,
webview: Webview<R>,
rid: ResourceId,
menu: Option<(ResourceId, ItemKind)>,
) -> crate::Result<()> {
let resources_table = app.resources_table();
let tray = resources_table.get::<TrayIcon<R>>(rid)?;
if let Some((rid, kind)) = menu {
let webview_resources_table = webview.resources_table();
match kind {
ItemKind::Menu => {
let menu = webview_resources_table.get::<Menu<R>>(rid)?;
tray.set_menu(Some((*menu).clone()))?;
}
ItemKind::Submenu => {
let submenu = webview_resources_table.get::<Submenu<R>>(rid)?;
tray.set_menu(Some((*submenu).clone()))?;
}
_ => return Err(crate::Error::UnexpectedMenuKind),
};
} else {
tray.set_menu(None::<Menu<R>>)?;
}
Ok(())
}
#[command(root = "crate")]
fn set_tooltip<R: Runtime>(
app: AppHandle<R>,
rid: ResourceId,
tooltip: Option<String>,
) -> crate::Result<()> {
let resources_table = app.resources_table();
let tray = resources_table.get::<TrayIcon<R>>(rid)?;
tray.set_tooltip(tooltip)
}
#[command(root = "crate")]
fn set_title<R: Runtime>(
app: AppHandle<R>,
rid: ResourceId,
title: Option<String>,
) -> crate::Result<()> {
let resources_table = app.resources_table();
let tray = resources_table.get::<TrayIcon<R>>(rid)?;
tray.set_title(title)
}
#[command(root = "crate")]
fn set_visible<R: Runtime>(app: AppHandle<R>, rid: ResourceId, visible: bool) -> crate::Result<()> {
let resources_table = app.resources_table();
let tray = resources_table.get::<TrayIcon<R>>(rid)?;
tray.set_visible(visible)
}
#[command(root = "crate")]
fn set_temp_dir_path<R: Runtime>(
app: AppHandle<R>,
rid: ResourceId,
path: Option<PathBuf>,
) -> crate::Result<()> {
let resources_table = app.resources_table();
let tray = resources_table.get::<TrayIcon<R>>(rid)?;
tray.set_temp_dir_path(path)
}
#[command(root = "crate")]
fn set_icon_as_template<R: Runtime>(
app: AppHandle<R>,
rid: ResourceId,
as_template: bool,
) -> crate::Result<()> {
let resources_table = app.resources_table();
let tray = resources_table.get::<TrayIcon<R>>(rid)?;
tray.set_icon_as_template(as_template)
}
#[command(root = "crate")]
fn set_icon_with_as_template<R: Runtime>(
app: AppHandle<R>,
webview: Webview<R>,
rid: ResourceId,
icon: Option<JsImage>,
as_template: bool,
) -> crate::Result<()> {
let resources_table = app.resources_table();
let tray = resources_table.get::<TrayIcon<R>>(rid)?;
let webview_resources_table = webview.resources_table();
let icon = match icon {
Some(i) => Some(Arc::unwrap_or_clone(i.into_img(&webview_resources_table)?)),
None => None,
};
tray.set_icon_with_as_template(icon, as_template)
}
#[command(root = "crate")]
fn set_show_menu_on_left_click<R: Runtime>(
app: AppHandle<R>,
rid: ResourceId,
on_left: bool,
) -> crate::Result<()> {
let resources_table = app.resources_table();
let tray = resources_table.get::<TrayIcon<R>>(rid)?;
tray.set_show_menu_on_left_click(on_left)
}
pub(crate) fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("tray")
.invoke_handler(crate::generate_handler![
#![plugin(tray)]
new,
get_by_id,
remove_by_id,
set_icon,
set_menu,
set_tooltip,
set_title,
set_visible,
set_temp_dir_path,
set_icon_as_template,
set_icon_with_as_template,
set_show_menu_on_left_click,
])
.build()
}

View File

@@ -0,0 +1,120 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
#![allow(deprecated)]
use crate::utils::config::WindowEffectsConfig;
use crate::window::{Effect, EffectState};
use raw_window_handle::HasWindowHandle;
use window_vibrancy::{
LiquidGlassOptions, NSGlassEffectViewStyle, NSVisualEffectMaterial, NSVisualEffectState,
};
pub fn apply_effects(window: impl HasWindowHandle, effects: WindowEffectsConfig) {
let WindowEffectsConfig {
effects,
radius,
state,
color,
interactive,
} = effects;
// window-vibrancy inserts a new subview on every call, so drop the previous effect first
clear_effects(&window);
if let Some(effect) = effects
.iter()
.find(|e| matches!(e, Effect::LiquidGlassRegular | Effect::LiquidGlassClear))
{
let mut options = LiquidGlassOptions::new(match effect {
Effect::LiquidGlassRegular => NSGlassEffectViewStyle::Regular,
Effect::LiquidGlassClear => NSGlassEffectViewStyle::Clear,
_ => unreachable!(),
})
.interactive(interactive);
if let Some(color) = color {
options = options.tint_color(color.into());
}
if let Some(radius) = radius {
options = options.radius(radius);
}
match window_vibrancy::apply_liquid_glass(&window, options) {
Ok(()) => return,
// macOS 15 and below: fall back to the Visual Effect material, if any
Err(window_vibrancy::Error::UnsupportedPlatformVersion(_)) => {}
Err(e) => {
log::error!("failed to apply liquid glass effect: {e}");
return;
}
}
}
let effect = if let Some(effect) = effects.into_iter().find(|e| {
matches!(
e,
Effect::AppearanceBased
| Effect::Light
| Effect::Dark
| Effect::MediumLight
| Effect::UltraDark
| Effect::Titlebar
| Effect::Selection
| Effect::Menu
| Effect::Popover
| Effect::Sidebar
| Effect::HeaderView
| Effect::Sheet
| Effect::WindowBackground
| Effect::HudWindow
| Effect::FullScreenUI
| Effect::Tooltip
| Effect::ContentBackground
| Effect::UnderWindowBackground
| Effect::UnderPageBackground
)
}) {
effect
} else {
return;
};
window_vibrancy::apply_vibrancy(
window,
match effect {
Effect::AppearanceBased => NSVisualEffectMaterial::AppearanceBased,
Effect::Light => NSVisualEffectMaterial::Light,
Effect::Dark => NSVisualEffectMaterial::Dark,
Effect::MediumLight => NSVisualEffectMaterial::MediumLight,
Effect::UltraDark => NSVisualEffectMaterial::UltraDark,
Effect::Titlebar => NSVisualEffectMaterial::Titlebar,
Effect::Selection => NSVisualEffectMaterial::Selection,
Effect::Menu => NSVisualEffectMaterial::Menu,
Effect::Popover => NSVisualEffectMaterial::Popover,
Effect::Sidebar => NSVisualEffectMaterial::Sidebar,
Effect::HeaderView => NSVisualEffectMaterial::HeaderView,
Effect::Sheet => NSVisualEffectMaterial::Sheet,
Effect::WindowBackground => NSVisualEffectMaterial::WindowBackground,
Effect::HudWindow => NSVisualEffectMaterial::HudWindow,
Effect::FullScreenUI => NSVisualEffectMaterial::FullScreenUI,
Effect::Tooltip => NSVisualEffectMaterial::Tooltip,
Effect::ContentBackground => NSVisualEffectMaterial::ContentBackground,
Effect::UnderWindowBackground => NSVisualEffectMaterial::UnderWindowBackground,
Effect::UnderPageBackground => NSVisualEffectMaterial::UnderPageBackground,
_ => unreachable!(),
},
state.map(|s| match s {
EffectState::FollowsWindowActiveState => NSVisualEffectState::FollowsWindowActiveState,
EffectState::Active => NSVisualEffectState::Active,
EffectState::Inactive => NSVisualEffectState::Inactive,
}),
radius,
);
}
pub fn clear_effects(window: impl HasWindowHandle) {
let _ = window_vibrancy::clear_vibrancy(&window);
let _ = window_vibrancy::clear_liquid_glass(&window);
}

View File

@@ -0,0 +1,32 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
#![allow(unused)]
use tauri_utils::config::WindowEffectsConfig;
use crate::{Runtime, Window};
#[cfg(target_os = "macos")]
mod macos;
#[cfg(windows)]
mod windows;
pub fn set_window_effects<R: Runtime>(
window: &Window<R>,
effects: Option<WindowEffectsConfig>,
) -> crate::Result<()> {
if let Some(_effects) = effects {
#[cfg(windows)]
windows::apply_effects(window, _effects);
#[cfg(target_os = "macos")]
macos::apply_effects(window, _effects);
} else {
#[cfg(windows)]
windows::clear_effects(window);
#[cfg(target_os = "macos")]
macos::clear_effects(window);
}
Ok(())
}

View File

@@ -0,0 +1,53 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
#![allow(non_snake_case)]
#![allow(non_camel_case_types)]
#![allow(clippy::upper_case_acronyms)]
use std::ffi::c_void;
use crate::utils::config::WindowEffectsConfig;
use crate::window::{Color, Effect};
use raw_window_handle::HasWindowHandle;
use windows::Win32::Foundation::HWND;
pub fn apply_effects(window: impl HasWindowHandle, effects: WindowEffectsConfig) {
let WindowEffectsConfig { effects, color, .. } = effects;
let effect = if let Some(effect) = effects.iter().find(|e| {
matches!(
e,
Effect::Mica
| Effect::MicaDark
| Effect::MicaLight
| Effect::Acrylic
| Effect::Blur
| Effect::Tabbed
| Effect::TabbedDark
| Effect::TabbedLight
)
}) {
effect
} else {
return;
};
match effect {
Effect::Blur => window_vibrancy::apply_blur(window, color.map(Into::into)),
Effect::Acrylic => window_vibrancy::apply_acrylic(window, color.map(Into::into)),
Effect::Mica => window_vibrancy::apply_mica(window, None),
Effect::MicaDark => window_vibrancy::apply_mica(window, Some(true)),
Effect::MicaLight => window_vibrancy::apply_mica(window, Some(false)),
Effect::Tabbed => window_vibrancy::apply_tabbed(window, None),
Effect::TabbedDark => window_vibrancy::apply_tabbed(window, Some(true)),
Effect::TabbedLight => window_vibrancy::apply_tabbed(window, Some(false)),
_ => unreachable!(),
};
}
pub fn clear_effects(window: impl HasWindowHandle) {
window_vibrancy::clear_blur(&window);
window_vibrancy::clear_acrylic(&window);
window_vibrancy::clear_mica(&window);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,255 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
//! The tauri plugin to create and manipulate windows from JS.
use crate::{
AppHandle, Runtime, WebviewWindowBuilder, command,
plugin::{Builder, TauriPlugin},
sealed::ManagerBase,
utils::config::WindowConfig,
};
#[derive(serde::Serialize)]
struct WebviewRef {
window_label: String,
label: String,
}
#[command(root = "crate")]
async fn get_all_webviews<R: Runtime>(app: AppHandle<R>) -> Vec<WebviewRef> {
app
.manager()
.webviews()
.values()
.map(|webview| WebviewRef {
window_label: webview.window_ref().label().into(),
label: webview.label().into(),
})
.collect()
}
#[command(root = "crate")]
async fn create_webview_window<R: Runtime>(
app: AppHandle<R>,
options: WindowConfig,
) -> crate::Result<()> {
WebviewWindowBuilder::from_config(&app, &options)?.build()?;
Ok(())
}
#[cfg(desktop)]
mod desktop_commands {
use super::*;
use crate::{
Webview, command,
runtime::dpi::{Position, Size},
utils::config::Color,
};
fn get_webview<R: Runtime>(
webview: Webview<R>,
label: Option<String>,
) -> crate::Result<Webview<R>> {
match label {
Some(l) if !l.is_empty() => webview
.manager()
.get_webview(&l)
.ok_or(crate::Error::WebviewNotFound),
_ => Ok(webview),
}
}
macro_rules! getter {
($cmd: ident, $ret: ty) => {
getter!($cmd, $cmd, $ret)
};
($fn: ident, $cmd: ident, $ret: ty) => {
#[command(root = "crate")]
pub async fn $fn<R: Runtime>(
webview: Webview<R>,
label: Option<String>,
) -> crate::Result<$ret> {
get_webview(webview, label)?.$cmd().map_err(Into::into)
}
};
}
macro_rules! setter {
($cmd: ident) => {
setter!($cmd, $cmd);
};
($fn: ident, $cmd: ident) => {
#[command(root = "crate")]
pub async fn $fn<R: Runtime>(
webview: Webview<R>,
label: Option<String>,
) -> crate::Result<()> {
get_webview(webview, label)?.$cmd().map_err(Into::into)
}
};
($fn: ident, $cmd: ident, $input: ty) => {
#[command(root = "crate")]
pub async fn $fn<R: Runtime>(
webview: Webview<R>,
label: Option<String>,
value: $input,
) -> crate::Result<()> {
get_webview(webview, label)?.$cmd(value).map_err(Into::into)
}
};
}
// TODO
getter!(
webview_position,
position,
tauri_runtime::dpi::PhysicalPosition<i32>
);
getter!(webview_size, size, tauri_runtime::dpi::PhysicalSize<u32>);
//getter!(is_focused, bool);
setter!(print);
setter!(webview_close, close);
setter!(set_webview_size, set_size, Size);
setter!(set_webview_position, set_position, Position);
setter!(set_webview_focus, set_focus);
setter!(set_webview_auto_resize, set_auto_resize, bool);
setter!(webview_hide, hide);
setter!(webview_show, show);
setter!(set_webview_zoom, set_zoom, f64);
setter!(
set_webview_background_color,
set_background_color,
Option<Color>
);
setter!(clear_all_browsing_data, clear_all_browsing_data);
#[cfg(not(feature = "unstable"))]
#[command(root = "crate")]
pub async fn create_webview() -> crate::Result<()> {
Err(crate::Error::UnstableFeatureNotSupported)
}
#[cfg(feature = "unstable")]
#[command(root = "crate")]
pub async fn create_webview<R: Runtime>(
app: crate::AppHandle<R>,
window_label: String,
options: WindowConfig,
) -> crate::Result<()> {
use anyhow::Context;
let window = app
.manager()
.get_window(&window_label)
.ok_or(crate::Error::WindowNotFound)?;
let x = options.x.context("missing parameter `options.x`")?;
let y = options.y.context("missing parameter `options.y`")?;
let width = options.width;
let height = options.height;
let builder = crate::webview::WebviewBuilder::from_config(&options);
window.add_child(
builder,
tauri_runtime::dpi::LogicalPosition::new(x, y),
tauri_runtime::dpi::LogicalSize::new(width, height),
)?;
Ok(())
}
#[command(root = "crate")]
pub async fn reparent<R: Runtime>(
webview: crate::Webview<R>,
label: Option<String>,
window: String,
) -> crate::Result<()> {
let webview = get_webview(webview, label)?;
if let Some(window) = webview.manager.get_window(&window) {
webview.reparent(&window)?;
}
Ok(())
}
#[cfg(any(debug_assertions, feature = "devtools"))]
#[command(root = "crate")]
pub async fn internal_toggle_devtools<R: Runtime>(
webview: crate::Webview<R>,
label: Option<String>,
) -> crate::Result<()> {
let webview = get_webview(webview, label)?;
if webview.is_devtools_open() {
webview.close_devtools();
} else {
webview.open_devtools();
}
Ok(())
}
}
/// Initializes the plugin.
pub fn init<R: Runtime>() -> TauriPlugin<R> {
#[allow(unused_mut)]
let mut init_script = String::new();
// window.print works on Linux/Windows; need to use the API on macOS
#[cfg(any(target_os = "macos", target_os = "ios"))]
{
init_script.push_str(include_str!("./scripts/print.js"));
}
#[cfg(any(debug_assertions, feature = "devtools"))]
{
use serialize_to_javascript::{DefaultTemplate, Template, default_template};
#[derive(Template)]
#[default_template("./scripts/toggle-devtools.js")]
struct Devtools<'a> {
os_name: &'a str,
}
init_script.push_str(
&Devtools {
os_name: std::env::consts::OS,
}
.render_default(&Default::default())
.unwrap()
.into_string(),
);
}
let mut builder = Builder::new("webview");
if !init_script.is_empty() {
builder = builder.js_init_script(init_script);
}
builder
.invoke_handler(crate::generate_handler![
#![plugin(webview)]
create_webview_window,
get_all_webviews,
#[cfg(desktop)] desktop_commands::create_webview,
// getters
#[cfg(desktop)] desktop_commands::webview_position,
#[cfg(desktop)] desktop_commands::webview_size,
// setters
#[cfg(desktop)] desktop_commands::webview_close,
#[cfg(desktop)] desktop_commands::set_webview_size,
#[cfg(desktop)] desktop_commands::set_webview_position,
#[cfg(desktop)] desktop_commands::set_webview_focus,
#[cfg(desktop)] desktop_commands::set_webview_auto_resize,
#[cfg(desktop)] desktop_commands::set_webview_background_color,
#[cfg(desktop)] desktop_commands::set_webview_zoom,
#[cfg(desktop)] desktop_commands::webview_hide,
#[cfg(desktop)] desktop_commands::webview_show,
#[cfg(desktop)] desktop_commands::print,
#[cfg(desktop)] desktop_commands::clear_all_browsing_data,
#[cfg(desktop)] desktop_commands::reparent,
#[cfg(all(desktop, any(debug_assertions, feature = "devtools")))]
desktop_commands::internal_toggle_devtools,
])
.build()
}

View File

@@ -0,0 +1,7 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
window.print = function () {
return window.__TAURI_INTERNALS__.invoke('plugin:webview|print')
}

View File

@@ -0,0 +1,31 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
;(function () {
function toggleDevtoolsHotkey() {
const osName = __TEMPLATE_os_name__
const isHotkey =
osName === 'macos'
? (event) => event.metaKey && event.altKey && event.code === 'KeyI'
: (event) => event.ctrlKey && event.shiftKey && event.code === 'KeyI'
document.addEventListener('keydown', (event) => {
if (isHotkey(event)) {
window.__TAURI_INTERNALS__.invoke(
'plugin:webview|internal_toggle_devtools'
)
}
})
}
if (
document.readyState === 'complete'
|| document.readyState === 'interactive'
) {
toggleDevtoolsHotkey()
} else {
window.addEventListener('DOMContentLoaded', toggleDevtoolsHotkey, true)
}
})()

View File

@@ -0,0 +1,43 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
const OS_NAME = __TEMPLATE_os_name__
let zoomLevel = 1
const MAX_ZOOM_LEVEL = 10
const MIN_ZOOM_LEVEL = 0.2
window.addEventListener('keydown', (event) => {
if (OS_NAME === 'macos' ? event.metaKey : event.ctrlKey) {
if (event.key === '-') {
zoomLevel -= 0.2
} else if (event.key === '=' || event.key === '+') {
zoomLevel += 0.2
} else if (event.key === '0') {
zoomLevel = 1
} else {
return
}
zoomLevel = Math.min(Math.max(zoomLevel, MIN_ZOOM_LEVEL), MAX_ZOOM_LEVEL)
window.__TAURI_INTERNALS__.invoke('plugin:webview|set_webview_zoom', {
value: zoomLevel
})
}
})
window.addEventListener('mousewheel', (event) => {
if (event.ctrlKey) {
event.preventDefault()
if (event.deltaY < 0) {
zoomLevel += 0.2
} else {
zoomLevel -= 0.2
}
zoomLevel = Math.min(Math.max(zoomLevel, MIN_ZOOM_LEVEL), MAX_ZOOM_LEVEL)
window.__TAURI_INTERNALS__.invoke('plugin:webview|set_webview_zoom', {
value: zoomLevel
})
}
})

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,351 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
//! The tauri plugin to create and manipulate windows from JS.
use crate::{
Runtime, Window,
plugin::{Builder, TauriPlugin},
sealed::ManagerBase,
};
fn get_window<R: Runtime>(window: Window<R>, label: Option<String>) -> crate::Result<Window<R>> {
match label {
Some(l) if !l.is_empty() => window
.manager()
.get_window(&l)
.ok_or(crate::Error::WindowNotFound),
_ => Ok(window),
}
}
macro_rules! getter {
($cmd: ident, $ret: ty) => {
#[command(root = "crate")]
pub async fn $cmd<R: Runtime>(window: Window<R>, label: Option<String>) -> crate::Result<$ret> {
get_window(window, label)?.$cmd().map_err(Into::into)
}
};
}
macro_rules! setter {
($cmd: ident) => {
#[command(root = "crate")]
pub async fn $cmd<R: Runtime>(window: Window<R>, label: Option<String>) -> crate::Result<()> {
get_window(window, label)?.$cmd().map_err(Into::into)
}
};
($cmd: ident, $input: ty) => {
#[command(root = "crate")]
pub async fn $cmd<R: Runtime>(
window: Window<R>,
label: Option<String>,
value: $input,
) -> crate::Result<()> {
get_window(window, label)?.$cmd(value).map_err(Into::into)
}
};
}
mod commands {
use tauri_runtime::window::WindowSizeConstraints;
use super::*;
use crate::{
AppHandle, Monitor, PhysicalPosition, PhysicalSize, Position, Size, Theme, Window, command,
sealed::ManagerBase, utils::config::WindowConfig, window::Color, window::WindowBuilder,
};
#[command(root = "crate")]
pub async fn get_all_windows<R: Runtime>(app: AppHandle<R>) -> Vec<String> {
app.manager().windows().keys().cloned().collect()
}
#[command(root = "crate")]
pub async fn create<R: Runtime>(window: Window<R>, options: WindowConfig) -> crate::Result<()> {
WindowBuilder::from_config(&window, &options)?.build()?;
Ok(())
}
getter!(scale_factor, f64);
getter!(inner_position, PhysicalPosition<i32>);
getter!(outer_position, PhysicalPosition<i32>);
getter!(inner_size, PhysicalSize<u32>);
getter!(outer_size, PhysicalSize<u32>);
getter!(is_focused, bool);
getter!(is_resizable, bool);
getter!(is_visible, bool);
getter!(is_enabled, bool);
getter!(title, String);
getter!(theme, Theme);
#[cfg(target_os = "android")]
getter!(activity_name, String);
#[cfg(target_os = "ios")]
getter!(scene_identifier, String);
setter!(set_resizable, bool);
setter!(set_title, &str);
setter!(show);
setter!(hide);
setter!(close);
setter!(destroy);
setter!(set_content_protected, bool);
setter!(set_size, Size);
setter!(set_min_size, Option<Size>);
setter!(set_max_size, Option<Size>);
setter!(set_position, Position);
setter!(set_focus);
setter!(set_focusable, bool);
setter!(set_background_color, Option<Color>);
setter!(set_size_constraints, WindowSizeConstraints);
setter!(set_theme, Option<Theme>);
setter!(set_enabled, bool);
getter!(current_monitor, Option<Monitor>);
getter!(primary_monitor, Option<Monitor>);
getter!(available_monitors, Vec<Monitor>);
#[command(root = "crate")]
pub async fn monitor_from_point<R: Runtime>(
window: Window<R>,
label: Option<String>,
x: f64,
y: f64,
) -> crate::Result<Option<Monitor>> {
let window = get_window(window, label)?;
window.monitor_from_point(x, y)
}
}
#[cfg(desktop)]
mod desktop_commands {
use tauri_runtime::ResizeDirection;
use tauri_utils::TitleBarStyle;
use super::*;
use crate::{
CursorIcon, Manager, PhysicalPosition, Position, UserAttentionType, Webview, command,
utils::config::WindowEffectsConfig, window::ProgressBarState,
};
getter!(is_fullscreen, bool);
getter!(is_minimized, bool);
getter!(is_maximized, bool);
getter!(is_decorated, bool);
getter!(is_maximizable, bool);
getter!(is_minimizable, bool);
getter!(is_closable, bool);
getter!(cursor_position, PhysicalPosition<f64>);
getter!(is_always_on_top, bool);
setter!(center);
setter!(request_user_attention, Option<UserAttentionType>);
setter!(set_maximizable, bool);
setter!(set_minimizable, bool);
setter!(set_closable, bool);
setter!(maximize);
setter!(unmaximize);
setter!(minimize);
setter!(unminimize);
setter!(set_decorations, bool);
setter!(set_shadow, bool);
setter!(set_effects, Option<WindowEffectsConfig>);
setter!(set_always_on_top, bool);
setter!(set_always_on_bottom, bool);
setter!(set_fullscreen, bool);
setter!(set_fullscreen_on_monitor, PhysicalPosition<f64>);
setter!(set_simple_fullscreen, bool);
setter!(set_skip_taskbar, bool);
setter!(set_cursor_grab, bool);
setter!(set_cursor_visible, bool);
setter!(set_cursor_icon, CursorIcon);
setter!(set_cursor_position, Position);
setter!(set_ignore_cursor_events, bool);
setter!(start_dragging);
setter!(start_resize_dragging, ResizeDirection);
setter!(set_progress_bar, ProgressBarState);
setter!(set_badge_count, Option<i64>);
#[cfg(target_os = "macos")]
setter!(set_badge_label, Option<String>);
setter!(set_visible_on_all_workspaces, bool);
setter!(set_title_bar_style, TitleBarStyle);
#[command(root = "crate")]
#[cfg(target_os = "windows")]
pub async fn set_overlay_icon<R: Runtime>(
webview: Webview<R>,
window: Window<R>,
label: Option<String>,
value: Option<crate::image::JsImage>,
) -> crate::Result<()> {
let window = get_window(window, label)?;
let resources_table = webview.resources_table();
let value = match value {
Some(value) => Some(value.into_img(&resources_table)?.as_ref().clone()),
None => None,
};
window.set_overlay_icon(value)
}
#[command(root = "crate")]
pub async fn set_icon<R: Runtime>(
webview: Webview<R>,
window: Window<R>,
label: Option<String>,
value: crate::image::JsImage,
) -> crate::Result<()> {
let window = get_window(window, label)?;
let resources_table = webview.resources_table();
window.set_icon(value.into_img(&resources_table)?.as_ref().clone())
}
#[command(root = "crate")]
pub async fn toggle_maximize<R: Runtime>(
window: Window<R>,
label: Option<String>,
) -> crate::Result<()> {
let window = get_window(window, label)?;
if window.is_maximized()? {
window.unmaximize()?
} else {
window.maximize()?
};
Ok(())
}
#[command(root = "crate")]
pub async fn internal_toggle_maximize<R: Runtime>(
window: Window<R>,
label: Option<String>,
) -> crate::Result<()> {
let window = get_window(window, label)?;
if window.is_resizable()? {
if window.is_maximized()? {
window.unmaximize()?
} else if window.is_maximizable()? {
window.maximize()?
};
}
Ok(())
}
}
/// Initializes the plugin.
pub fn init<R: Runtime>() -> TauriPlugin<R> {
use serialize_to_javascript::{DefaultTemplate, Template, default_template};
let mut init_script = String::new();
#[derive(Template)]
#[default_template("./scripts/drag.js")]
struct Drag<'a> {
os_name: &'a str,
}
init_script.push_str(
&Drag {
os_name: std::env::consts::OS,
}
.render_default(&Default::default())
.unwrap()
.into_string(),
);
Builder::new("window")
.js_init_script(init_script)
.invoke_handler(crate::generate_handler![
#![plugin(window)]
commands::create,
// getters
commands::get_all_windows,
commands::scale_factor,
commands::inner_position,
commands::outer_position,
commands::inner_size,
commands::outer_size,
commands::is_focused,
commands::is_resizable,
commands::is_visible,
commands::is_enabled,
commands::title,
commands::theme,
#[cfg(target_os = "android")]
commands::activity_name,
#[cfg(target_os = "ios")]
commands::scene_identifier,
commands::set_resizable,
commands::set_title,
commands::show,
commands::hide,
commands::close,
commands::destroy,
commands::set_content_protected,
commands::set_size,
commands::set_min_size,
commands::set_max_size,
commands::set_position,
commands::set_size_constraints,
commands::set_focus,
commands::set_focusable,
commands::set_enabled,
commands::set_background_color,
commands::set_theme,
commands::current_monitor,
commands::primary_monitor,
commands::monitor_from_point,
commands::available_monitors,
#[cfg(desktop)] desktop_commands::is_fullscreen,
#[cfg(desktop)] desktop_commands::is_minimized,
#[cfg(desktop)] desktop_commands::is_maximized,
#[cfg(desktop)] desktop_commands::is_decorated,
#[cfg(desktop)] desktop_commands::is_maximizable,
#[cfg(desktop)] desktop_commands::is_minimizable,
#[cfg(desktop)] desktop_commands::is_closable,
#[cfg(desktop)] desktop_commands::cursor_position,
#[cfg(desktop)] desktop_commands::is_always_on_top,
// setters
#[cfg(desktop)] desktop_commands::center,
#[cfg(desktop)] desktop_commands::request_user_attention,
#[cfg(desktop)] desktop_commands::set_maximizable,
#[cfg(desktop)] desktop_commands::set_minimizable,
#[cfg(desktop)] desktop_commands::set_closable,
#[cfg(desktop)] desktop_commands::maximize,
#[cfg(desktop)] desktop_commands::unmaximize,
#[cfg(desktop)] desktop_commands::minimize,
#[cfg(desktop)] desktop_commands::unminimize,
#[cfg(desktop)] desktop_commands::set_decorations,
#[cfg(desktop)] desktop_commands::set_shadow,
#[cfg(desktop)] desktop_commands::set_effects,
#[cfg(desktop)] desktop_commands::set_always_on_top,
#[cfg(desktop)] desktop_commands::set_always_on_bottom,
#[cfg(desktop)] desktop_commands::set_fullscreen,
#[cfg(desktop)] desktop_commands::set_fullscreen_on_monitor,
#[cfg(desktop)] desktop_commands::set_simple_fullscreen,
#[cfg(desktop)] desktop_commands::set_skip_taskbar,
#[cfg(desktop)] desktop_commands::set_cursor_grab,
#[cfg(desktop)] desktop_commands::set_cursor_visible,
#[cfg(desktop)] desktop_commands::set_cursor_icon,
#[cfg(desktop)] desktop_commands::set_cursor_position,
#[cfg(desktop)] desktop_commands::set_ignore_cursor_events,
#[cfg(desktop)] desktop_commands::start_dragging,
#[cfg(desktop)] desktop_commands::start_resize_dragging,
#[cfg(desktop)] desktop_commands::set_badge_count,
#[cfg(target_os = "macos")]
#[cfg(desktop)] desktop_commands::set_badge_label,
#[cfg(desktop)] desktop_commands::set_progress_bar,
#[cfg(target_os = "windows")]
#[cfg(desktop)] desktop_commands::set_overlay_icon,
#[cfg(desktop)] desktop_commands::set_icon,
#[cfg(desktop)] desktop_commands::set_visible_on_all_workspaces,
#[cfg(desktop)] desktop_commands::set_title_bar_style,
#[cfg(desktop)] desktop_commands::toggle_maximize,
#[cfg(desktop)] desktop_commands::internal_toggle_maximize,
])
.build()
}

View File

@@ -0,0 +1,129 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
;(function () {
//-----------------------//
// drag on mousedown and maximize on double click on Windows and Linux
// while macOS maximization should be on mouseup and if the mouse
// moves after the double click, it should be cancelled (see https://github.com/tauri-apps/tauri/issues/8306)
//-----------------------//
const TAURI_DRAG_REGION_ATTR = 'data-tauri-drag-region'
const CLICKABLE_TAGS = new Set([
'A',
'BUTTON',
'INPUT',
'SELECT',
'TEXTAREA',
'LABEL',
'SUMMARY'
])
const INTERACTIVE_ROLES = new Set([
'button',
'link',
'menuitem',
'tab',
'checkbox',
'radio',
'switch',
'option'
])
function isClickableElement(el) {
return (
CLICKABLE_TAGS.has(el.tagName)
|| (el.hasAttribute('contenteditable')
&& el.getAttribute('contenteditable') !== 'false')
|| (el.hasAttribute('tabindex') && el.getAttribute('tabindex') !== '-1')
|| INTERACTIVE_ROLES.has(el.getAttribute('role'))
)
}
// Walk the composed path from target upward.
//
// Supported values for data-tauri-drag-region:
// (bare / no value / "true") -> self: only direct clicks on this element trigger drag
// "deep" -> deep: clicks anywhere in the subtree trigger drag
// "false" -> disabled: drag is blocked here (and for ancestors)
//
// Clickable elements (buttons, links, etc.) normally block dragging,
// but if they themselves carry data-tauri-drag-region they act as drag regions.
function isDragRegion(composedPath) {
for (const el of composedPath) {
if (!(el instanceof HTMLElement)) continue
const attr = el.getAttribute(TAURI_DRAG_REGION_ATTR)
// clickable without explicit drag region → blocks drag
if (isClickableElement(el) && attr === null) return false
// no attr → keep walking up
if (attr === null) continue
// explicitly disabled
if (attr === 'false') return false
// subtree drag — any descendant triggers
if (attr === 'deep') return true
// bare or "true" attr — only direct clicks on this element
if (attr === '' || attr === 'true') return el === composedPath[0]
}
return false
}
const osName = __TEMPLATE_os_name__
// initial mousedown position for macOS
let initialX = 0
let initialY = 0
document.addEventListener('mousedown', (e) => {
if (
// was left mouse button
e.button === 0
// and was normal click to drag or double click to maximize
&& (e.detail === 1 || e.detail === 2)
// and is drag region
&& isDragRegion(e.composedPath())
) {
// macOS maximization happens on `mouseup`,
// so we save needed state and early return
if (osName === 'macos' && e.detail === 2) {
initialX = e.clientX
initialY = e.clientY
return
}
// prevents text cursor
e.preventDefault()
// fix #2549: double click on drag region edge causes content to maximize without window sizing change
// https://github.com/tauri-apps/tauri/issues/2549#issuecomment-1250036908
e.stopImmediatePropagation()
// start dragging if the element has a `tauri-drag-region` data attribute and maximize on double-clicking it
const cmd = e.detail === 2 ? 'internal_toggle_maximize' : 'start_dragging'
window.__TAURI_INTERNALS__.invoke('plugin:window|' + cmd)
}
})
// on macOS we maximize on mouseup instead, to match the system behavior where maximization can be canceled
// if the mouse moves outside the data-tauri-drag-region
if (osName === 'macos') {
document.addEventListener('mouseup', (e) => {
if (
// was left mouse button
e.button === 0
// and was double click
&& e.detail === 2
// and the cursor hasn't moved from initial mousedown
&& e.clientX === initialX
&& e.clientY === initialY
// and the event path contains a drag region (with no clickable element in between)
&& isDragRegion(e.composedPath())
) {
window.__TAURI_INTERNALS__.invoke(
'plugin:window|internal_toggle_maximize'
)
}
})
}
})()