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
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:
2842
crates/tauri/src/app.rs
Normal file
2842
crates/tauri/src/app.rs
Normal file
File diff suppressed because it is too large
Load Diff
158
crates/tauri/src/app/plugin.rs
Normal file
158
crates/tauri/src/app/plugin.rs
Normal 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>);
|
||||
363
crates/tauri/src/async_runtime.rs
Normal file
363
crates/tauri/src/async_runtime.rs
Normal 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
198
crates/tauri/src/error.rs
Normal 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>();
|
||||
}
|
||||
}
|
||||
34
crates/tauri/src/event/event-system-spec.md
Normal file
34
crates/tauri/src/event/event-system-spec.md
Normal 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).
|
||||
70
crates/tauri/src/event/event_name.rs
Normal file
70
crates/tauri/src/event/event_name.rs
Normal 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 `_`.",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
10
crates/tauri/src/event/init.js
Normal file
10
crates/tauri/src/event/init.js
Normal 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__
|
||||
}
|
||||
})
|
||||
488
crates/tauri/src/event/listener.rs
Normal file
488
crates/tauri/src/event/listener.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
258
crates/tauri/src/event/mod.rs
Normal file
258
crates/tauri/src/event/mod.rs
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
82
crates/tauri/src/event/plugin.rs
Normal file
82
crates/tauri/src/event/plugin.rs
Normal 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()
|
||||
}
|
||||
527
crates/tauri/src/image/mod.rs
Normal file
527
crates/tauri/src/image/mod.rs
Normal 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:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
88
crates/tauri/src/image/plugin.rs
Normal file
88
crates/tauri/src/image/plugin.rs
Normal 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
49
crates/tauri/src/ios.rs
Normal 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());
|
||||
1485
crates/tauri/src/ipc/authority.rs
Normal file
1485
crates/tauri/src/ipc/authority.rs
Normal file
File diff suppressed because it is too large
Load Diff
170
crates/tauri/src/ipc/capability_builder.rs
Normal file
170
crates/tauri/src/ipc/capability_builder.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
530
crates/tauri/src/ipc/channel.rs
Normal file
530
crates/tauri/src/ipc/channel.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
332
crates/tauri/src/ipc/command.rs
Normal file
332
crates/tauri/src/ipc/command.rs
Normal 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()?)
|
||||
}
|
||||
}
|
||||
}
|
||||
344
crates/tauri/src/ipc/format_callback.rs
Normal file
344
crates/tauri/src/ipc/format_callback.rs
Normal 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
606
crates/tauri/src/ipc/mod.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
753
crates/tauri/src/ipc/protocol.rs
Normal file
753
crates/tauri/src/ipc/protocol.rs
Normal 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
1267
crates/tauri/src/lib.rs
Normal file
File diff suppressed because it is too large
Load Diff
100
crates/tauri/src/manager/menu.rs
Normal file
100
crates/tauri/src/manager/menu.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
1040
crates/tauri/src/manager/mod.rs
Normal file
1040
crates/tauri/src/manager/mod.rs
Normal file
File diff suppressed because it is too large
Load Diff
92
crates/tauri/src/manager/tray.rs
Normal file
92
crates/tauri/src/manager/tray.rs
Normal 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))
|
||||
}
|
||||
}
|
||||
793
crates/tauri/src/manager/webview.rs
Normal file
793
crates/tauri/src/manager/webview.rs
Normal 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()));
|
||||
}
|
||||
}
|
||||
285
crates/tauri/src/manager/window.rs
Normal file
285
crates/tauri/src/manager/window.rs
Normal 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>,
|
||||
}
|
||||
90
crates/tauri/src/menu/builders/check.rs
Normal file
90
crates/tauri/src/menu/builders/check.rs
Normal 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
130
crates/tauri/src/menu/builders/icon.rs
Normal file
130
crates/tauri/src/menu/builders/icon.rs
Normal 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
675
crates/tauri/src/menu/builders/menu.rs
Normal file
675
crates/tauri/src/menu/builders/menu.rs
Normal 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>);
|
||||
17
crates/tauri/src/menu/builders/mod.rs
Normal file
17
crates/tauri/src/menu/builders/mod.rs
Normal 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;
|
||||
68
crates/tauri/src/menu/builders/normal.rs
Normal file
68
crates/tauri/src/menu/builders/normal.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
129
crates/tauri/src/menu/check.rs
Normal file
129
crates/tauri/src/menu/check.rs
Normal 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))
|
||||
}
|
||||
}
|
||||
220
crates/tauri/src/menu/icon.rs
Normal file
220
crates/tauri/src/menu/icon.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
400
crates/tauri/src/menu/menu.rs
Normal file
400
crates/tauri/src/menu/menu.rs
Normal 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())
|
||||
}
|
||||
}
|
||||
771
crates/tauri/src/menu/mod.rs
Normal file
771
crates/tauri/src/menu/mod.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
117
crates/tauri/src/menu/normal.rs
Normal file
117
crates/tauri/src/menu/normal.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
941
crates/tauri/src/menu/plugin.rs
Normal file
941
crates/tauri/src/menu/plugin.rs
Normal 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()
|
||||
}
|
||||
360
crates/tauri/src/menu/predefined.rs
Normal file
360
crates/tauri/src/menu/predefined.rs
Normal 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
|
||||
}
|
||||
}
|
||||
419
crates/tauri/src/menu/submenu.rs
Normal file
419
crates/tauri/src/menu/submenu.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
187
crates/tauri/src/path/android.rs
Normal file
187
crates/tauri/src/path/android.rs
Normal 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")
|
||||
}
|
||||
}
|
||||
296
crates/tauri/src/path/desktop.rs
Normal file
296
crates/tauri/src/path/desktop.rs
Normal 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())
|
||||
}
|
||||
}
|
||||
10
crates/tauri/src/path/init.js
Normal file
10
crates/tauri/src/path/init.js
Normal 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__
|
||||
}
|
||||
})
|
||||
386
crates/tauri/src/path/mod.rs
Normal file
386
crates/tauri/src/path/mod.rs
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
315
crates/tauri/src/path/plugin.rs
Normal file
315
crates/tauri/src/path/plugin.rs
Normal 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()), "..");
|
||||
}
|
||||
}
|
||||
87
crates/tauri/src/pattern.rs
Normal file
87
crates/tauri/src/pattern.rs
Normal 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
1151
crates/tauri/src/plugin.rs
Normal file
File diff suppressed because it is too large
Load Diff
511
crates/tauri/src/plugin/mobile.rs
Normal file
511
crates/tauri/src/plugin/mobile.rs
Normal 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
130
crates/tauri/src/process.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
309
crates/tauri/src/protocol/asset.rs
Normal file
309
crates/tauri/src/protocol/asset.rs
Normal 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())
|
||||
);
|
||||
}
|
||||
}
|
||||
112
crates/tauri/src/protocol/isolation.rs
Normal file
112
crates/tauri/src/protocol/isolation.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
53
crates/tauri/src/protocol/mod.rs
Normal file
53
crates/tauri/src/protocol/mod.rs
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
267
crates/tauri/src/protocol/tauri.rs
Normal file
267
crates/tauri/src/protocol/tauri.rs
Normal 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)
|
||||
}
|
||||
223
crates/tauri/src/resources/mod.rs
Normal file
223
crates/tauri/src/resources/mod.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
31
crates/tauri/src/resources/plugin.rs
Normal file
31
crates/tauri/src/resources/plugin.rs
Normal 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()
|
||||
}
|
||||
739
crates/tauri/src/scope/fs.rs
Normal file
739
crates/tauri/src/scope/fs.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
80
crates/tauri/src/scope/mod.rs
Normal file
80
crates/tauri/src/scope/mod.rs
Normal 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
291
crates/tauri/src/state.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
1430
crates/tauri/src/test/mock_runtime.rs
Normal file
1430
crates/tauri/src/test/mock_runtime.rs
Normal file
File diff suppressed because it is too large
Load Diff
338
crates/tauri/src/test/mod.rs
Normal file
338
crates/tauri/src/test/mod.rs
Normal 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:?}");
|
||||
});
|
||||
}
|
||||
}
|
||||
701
crates/tauri/src/tray/mod.rs
Normal file
701
crates/tauri/src/tray/mod.rs
Normal 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,
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
253
crates/tauri/src/tray/plugin.rs
Normal file
253
crates/tauri/src/tray/plugin.rs
Normal 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()
|
||||
}
|
||||
120
crates/tauri/src/vibrancy/macos.rs
Normal file
120
crates/tauri/src/vibrancy/macos.rs
Normal 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);
|
||||
}
|
||||
32
crates/tauri/src/vibrancy/mod.rs
Normal file
32
crates/tauri/src/vibrancy/mod.rs
Normal 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(())
|
||||
}
|
||||
53
crates/tauri/src/vibrancy/windows.rs
Normal file
53
crates/tauri/src/vibrancy/windows.rs
Normal 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);
|
||||
}
|
||||
2842
crates/tauri/src/webview/mod.rs
Normal file
2842
crates/tauri/src/webview/mod.rs
Normal file
File diff suppressed because it is too large
Load Diff
255
crates/tauri/src/webview/plugin.rs
Normal file
255
crates/tauri/src/webview/plugin.rs
Normal 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()
|
||||
}
|
||||
7
crates/tauri/src/webview/scripts/print.js
Normal file
7
crates/tauri/src/webview/scripts/print.js
Normal 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')
|
||||
}
|
||||
31
crates/tauri/src/webview/scripts/toggle-devtools.js
Normal file
31
crates/tauri/src/webview/scripts/toggle-devtools.js
Normal 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)
|
||||
}
|
||||
})()
|
||||
43
crates/tauri/src/webview/scripts/zoom-hotkey.js
Normal file
43
crates/tauri/src/webview/scripts/zoom-hotkey.js
Normal 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
|
||||
})
|
||||
}
|
||||
})
|
||||
2879
crates/tauri/src/webview/webview_window.rs
Normal file
2879
crates/tauri/src/webview/webview_window.rs
Normal file
File diff suppressed because it is too large
Load Diff
2625
crates/tauri/src/window/mod.rs
Normal file
2625
crates/tauri/src/window/mod.rs
Normal file
File diff suppressed because it is too large
Load Diff
351
crates/tauri/src/window/plugin.rs
Normal file
351
crates/tauri/src/window/plugin.rs
Normal 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()
|
||||
}
|
||||
129
crates/tauri/src/window/scripts/drag.js
Normal file
129
crates/tauri/src/window/scripts/drag.js
Normal 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'
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
})()
|
||||
Reference in New Issue
Block a user