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

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

View File

@@ -0,0 +1,14 @@
#!/bin/bash
# Copyright 2019-2024 Tauri Programme within The Commons Conservancy
# SPDX-License-Identifier: Apache-2.0
# SPDX-License-Identifier: MIT
# Loop all quickcheck tests for tauri.
while true
do
cargo test qc_
if [[ x$? != x0 ]] ; then
exit $?
fi
done

278
crates/tauri/Cargo.toml Normal file
View File

@@ -0,0 +1,278 @@
[package]
name = "tauri"
version = "2.11.5"
description = "Make tiny, secure apps for all desktop platforms with Tauri"
exclude = ["/test", "/.scripts", "CHANGELOG.md", "/target"]
links = "Tauri"
authors.workspace = true
homepage.workspace = true
repository.workspace = true
categories.workspace = true
license.workspace = true
edition.workspace = true
rust-version.workspace = true
[package.metadata.docs.rs]
no-default-features = true
features = [
"wry",
"unstable",
"custom-protocol",
"tray-icon",
"devtools",
"image-png",
"protocol-asset",
"test",
"specta",
"dynamic-acl",
"isolation",
]
default-target = "x86_64-unknown-linux-gnu"
targets = [
"x86_64-pc-windows-msvc",
"x86_64-unknown-linux-gnu",
"x86_64-apple-darwin",
"x86_64-linux-android",
"x86_64-apple-ios",
]
[package.metadata.cargo-udeps.ignore]
normal = ["reqwest"]
build = ["tauri-build"]
development = ["quickcheck_macros"]
[dependencies]
serde_json = { version = "1", features = ["raw_value"] }
serde = { version = "1", features = ["derive", "rc"] }
tokio = { version = "1", features = [
"rt",
"rt-multi-thread",
"sync",
"fs",
"io-util",
] }
uuid = { version = "1", features = ["v4"], optional = true }
url = "2"
anyhow = "1"
thiserror = "2"
tauri-runtime = { workspace = true }
tauri-macros = { workspace = true }
tauri-utils = { workspace = true, features = ["resources"] }
tauri-runtime-wry = { workspace = true, optional = true }
getrandom = "0.3"
serde_repr = "0.1"
http = "1"
dirs = "7"
percent-encoding = "2"
raw-window-handle = { version = "0.6", features = ["std"] }
glob = "0.3"
mime = "0.3"
data-url = { version = "0.3", optional = true }
serialize-to-javascript = "0.1.2"
image = { version = "0.25", default-features = false, optional = true }
http-range = { version = "0.1", optional = true }
tracing = { version = "0.1", optional = true }
heck = "0.5"
log = "0.4.21"
dunce = "1"
specta = { version = "^2.0.0-rc.16", optional = true, default-features = false, features = [
"function",
"derive",
] }
# WARNING: cookie::Cookie is re-exported so bumping this is a breaking change, documented to be done as a minor bump
cookie = "0.18"
# desktop
[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd", target_os = "windows", target_os = "macos"))'.dependencies]
muda = { version = "0.19", default-features = false, features = [
"serde",
"gtk",
] }
tray-icon = { version = "0.24", default-features = false, features = [
"serde",
"gtk",
], optional = true }
# linux
[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies]
gtk = { version = "0.18", features = ["v3_24"] }
webkit2gtk = { version = "2", features = ["v2_40"], optional = true }
# darwin
[target.'cfg(target_vendor = "apple")'.dependencies]
objc2 = "0.6"
# macOS
[target.'cfg(target_os = "macos")'.dependencies]
embed_plist = "1.2"
plist = "1"
objc2-foundation = { version = "0.3.2", default-features = false, features = [
"std",
"NSData",
"NSEnumerator",
"NSThread",
] }
objc2-app-kit = { version = "0.3.2", default-features = false, features = [
"std",
"NSApplication",
"NSColor",
"NSResponder",
"NSView",
"NSWindow",
"NSImage",
] }
objc2-web-kit = { version = "0.3.2", default-features = false, features = [
"objc2-app-kit",
"WKWebView",
"WKWebViewConfiguration",
"WKUserContentController",
] }
window-vibrancy = "0.8.1"
# windows
[target."cfg(windows)".dependencies]
webview2-com = { version = "0.39", optional = true }
window-vibrancy = "0.8.1"
windows = { version = "0.62", features = [
"Win32_Foundation",
"Win32_UI",
"Win32_UI_WindowsAndMessaging",
"Win32_System_LibraryLoader",
"Win32_Graphics_Gdi",
] }
# mobile
[target.'cfg(any(target_os = "android", all(target_vendor = "apple", not(target_os = "macos"))))'.dependencies]
bytes = { version = "1", features = ["serde"] }
reqwest = { version = "0.13", default-features = false, features = [
"json",
"stream",
] }
rustls = { version = "0.23", default-features = false, features = [
"ring",
], optional = true }
# android
[target.'cfg(target_os = "android")'.dependencies]
jni = "0.21"
# UIKit, i.e. iOS/tvOS/watchOS/visionOS
[target.'cfg(all(target_vendor = "apple", not(target_os = "macos")))'.dependencies]
libc = "0.2"
swift-rs = "1.0.8"
objc2-ui-kit = { version = "0.3.2", default-features = false, features = [
"UIApplication",
"UIResponder",
"UIView",
] }
[build-dependencies]
glob = "0.3"
heck = "0.5"
tauri-build = { workspace = true }
tauri-utils = { workspace = true, features = ["build-2"] }
[dev-dependencies]
proptest = "1.6.0"
quickcheck = "1.0.3"
quickcheck_macros = "1.0.0"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tauri = { path = ".", default-features = false, features = ["wry"] }
tokio = { version = "1", features = ["full"] }
cargo_toml = "1.0"
http-range = "0.1.5"
[features]
default = [
"wry",
"compression",
"common-controls-v6",
"dynamic-acl",
"x11",
"dbus",
]
unstable = ["tauri-runtime-wry?/unstable"]
x11 = ["tauri-runtime-wry?/x11"]
dbus = ["tauri-runtime-wry?/dbus"]
common-controls-v6 = [
"tray-icon?/common-controls-v6",
"muda/common-controls-v6",
"tauri-runtime-wry?/common-controls-v6",
]
tray-icon = ["dep:tray-icon"]
tracing = ["dep:tracing", "tauri-macros/tracing", "tauri-runtime-wry?/tracing"]
test = []
compression = ["tauri-macros/compression", "tauri-utils/compression"]
wry = ["webview2-com", "webkit2gtk", "tauri-runtime-wry"]
# TODO: Remove in v3 - wry does not have this feature anymore
objc-exception = []
linux-libxdo = ["tray-icon/libxdo", "muda/libxdo"]
isolation = ["tauri-utils/isolation", "tauri-macros/isolation", "uuid"]
custom-protocol = ["tauri-macros/custom-protocol"]
# TODO: Remove these flags in v3 and/or enable them by default behind a mobile flag https://github.com/tauri-apps/tauri/issues/12384
native-tls = ["reqwest/native-tls"]
native-tls-vendored = ["reqwest/native-tls-vendored"]
rustls-tls = ["reqwest/rustls-no-provider", "dep:rustls"]
devtools = ["tauri-runtime/devtools", "tauri-runtime-wry?/devtools"]
process-relaunch-dangerous-allow-symlink-macos = [
"tauri-utils/process-relaunch-dangerous-allow-symlink-macos",
]
macos-private-api = [
"tauri-runtime/macos-private-api",
"tauri-runtime-wry?/macos-private-api",
]
webview-data-url = ["data-url", "tauri-utils/html-manipulation-2"]
protocol-asset = ["http-range"]
config-json5 = ["tauri-macros/config-json5"]
config-toml = ["tauri-macros/config-toml"]
image-ico = ["image/ico"]
image-png = ["image/png"]
macos-proxy = ["tauri-runtime-wry?/macos-proxy"]
dynamic-acl = []
specta = ["dep:specta"]
[[example]]
name = "commands"
path = "../../examples/commands/main.rs"
[[example]]
name = "helloworld"
path = "../../examples/helloworld/main.rs"
[[example]]
name = "drag"
path = "../../examples/drag/main.rs"
[[example]]
name = "multiwebview"
path = "../../examples/multiwebview/main.rs"
required-features = ["unstable"]
[[example]]
name = "multiwindow"
path = "../../examples/multiwindow/main.rs"
[[example]]
name = "run-return"
path = "../../examples/run-return/main.rs"
[[example]]
name = "splashscreen"
path = "../../examples/splashscreen/main.rs"
[[example]]
name = "state"
path = "../../examples/state/main.rs"
[[example]]
name = "streaming"
path = "../../examples/streaming/main.rs"
[[example]]
name = "isolation"
path = "../../examples/isolation/main.rs"
required-features = ["isolation"]
[lints]
workspace = true

View File

@@ -0,0 +1,177 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

21
crates/tauri/LICENSE-MIT Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 - Present Tauri Apps Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

42
crates/tauri/README.md Normal file
View File

@@ -0,0 +1,42 @@
# Tauri
<img align="right" src="https://github.com/tauri-apps/tauri/raw/dev/.github/icon.png" height="128" width="128">
[![status](https://img.shields.io/badge/status-stable-blue.svg)](https://github.com/tauri-apps/tauri/tree/dev)
[![License](https://img.shields.io/badge/License-MIT%20or%20Apache%202-green.svg)](https://opencollective.com/tauri)
[![test core](https://img.shields.io/github/actions/workflow/status/tauri-apps/tauri/test-core.yml?label=test%20core&logo=github)](https://github.com/tauri-apps/tauri/actions/workflows/test-core.yml)
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Ftauri-apps%2Ftauri.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Ftauri-apps%2Ftauri?ref=badge_shield)
[![Chat Server](https://img.shields.io/badge/chat-discord-7289da.svg)](https://discord.gg/SpmNs4S)
[![website](https://img.shields.io/badge/website-tauri.app-purple.svg)](https://tauri.app)
[![https://good-labs.github.io/greater-good-affirmation/assets/images/badge.svg](https://good-labs.github.io/greater-good-affirmation/assets/images/badge.svg)](https://good-labs.github.io/greater-good-affirmation)
[![support](https://img.shields.io/badge/sponsor-Open%20Collective-blue.svg)](https://opencollective.com/tauri)
| Component | Version |
| --------- | ---------------------------------------------------------------------------------------------- |
| tauri | [![](https://img.shields.io/crates/v/tauri?style=flat-square)](https://crates.io/crates/tauri) |
## About Tauri
Tauri is a polyglot and generic system that is very composable and allows engineers to make a wide variety of applications. It is used for building applications for Desktop Computers using a combination of Rust tools and HTML rendered in a Webview. Apps built with Tauri can ship with any number of pieces of an optional JS API / Rust API so that webviews can control the system via message passing. In fact, developers can extend the default API with their own functionality and bridge the Webview and Rust-based backend easily.
Tauri apps can have custom menus and have tray-type interfaces. They can be updated, and are managed by the user's operating system as expected. They are very small, because they use the system's webview. They do not ship a runtime, since the final binary is compiled from rust. This makes the reversing of Tauri apps not a trivial task.
## This module
This is the glue crate that holds everything together. It brings the runtimes, macros, utilities and API into one final product. It reads the `tauri.conf.json` file at compile time in order to bring in features and undertake actual configuration of the app (and even the `Cargo.toml` file in the project's folder). It handles script injection (for polyfills / prototype revision) at runtime, hosts the API for systems interaction, and even manages updating.
To learn more about the details of how all of these pieces fit together, please consult this [ARCHITECTURE.md](https://github.com/tauri-apps/tauri/blob/dev/ARCHITECTURE.md) document.
## Semver
**tauri** is following [Semantic Versioning 2.0](https://semver.org/).
## Licenses
Code: (c) 2019 - 2021 - The Tauri Programme within The Commons Conservancy.
MIT or MIT/Apache 2.0 where applicable.
Logo: CC-BY-NC-ND
- Original Tauri Logo Designs by [Daniel Thompson-Yvetot](https://github.com/nothingismagick) and [Guillaume Chau](https://github.com/akryum)

500
crates/tauri/build.rs Normal file
View File

@@ -0,0 +1,500 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use heck::AsShoutySnakeCase;
use tauri_utils::write_if_changed;
use std::{
collections::BTreeMap,
env, fs,
path::{Path, PathBuf},
};
const PLUGINS: &[(&str, &[(&str, bool)])] = &[
// (plugin_name, &[(command, enabled-by_default)])
// TODO: Enable this in v3
// ("core:channel", &[("fetch", true)]),
(
"core:path",
&[
("resolve_directory", true),
("resolve", true),
("normalize", true),
("join", true),
("dirname", true),
("extname", true),
("basename", true),
("is_absolute", true),
],
),
(
"core:event",
&[
("listen", true),
("unlisten", true),
("emit", true),
("emit_to", true),
],
),
(
"core:window",
&[
("create", false),
// getters
("get_all_windows", true),
("scale_factor", true),
("inner_position", true),
("outer_position", true),
("inner_size", true),
("outer_size", true),
("is_fullscreen", true),
("is_minimized", true),
("is_maximized", true),
("is_focused", true),
("is_decorated", true),
("is_resizable", true),
("is_maximizable", true),
("is_minimizable", true),
("is_closable", true),
("is_visible", true),
("is_enabled", true),
("title", true),
("current_monitor", true),
("primary_monitor", true),
("monitor_from_point", true),
("available_monitors", true),
("cursor_position", true),
("theme", true),
("is_always_on_top", true),
("activity_name", true),
("scene_identifier", true),
// setters
("center", false),
("request_user_attention", false),
("set_enabled", false),
("set_resizable", false),
("set_maximizable", false),
("set_minimizable", false),
("set_closable", false),
("set_title", false),
("maximize", false),
("unmaximize", false),
("minimize", false),
("unminimize", false),
("show", false),
("hide", false),
("close", false),
("destroy", false),
("set_decorations", false),
("set_shadow", false),
("set_effects", false),
("set_always_on_top", false),
("set_always_on_bottom", false),
("set_visible_on_all_workspaces", false),
("set_content_protected", false),
("set_size", false),
("set_min_size", false),
("set_size_constraints", false),
("set_max_size", false),
("set_position", false),
("set_fullscreen", false),
("set_fullscreen_on_monitor", false),
("set_simple_fullscreen", false),
("set_focus", false),
("set_focusable", false),
("set_skip_taskbar", false),
("set_cursor_grab", false),
("set_cursor_visible", false),
("set_cursor_icon", false),
("set_cursor_position", false),
("set_ignore_cursor_events", false),
("start_dragging", false),
("start_resize_dragging", false),
("set_progress_bar", false),
("set_badge_count", false),
("set_overlay_icon", false),
("set_badge_label", false),
("set_icon", false),
("set_title_bar_style", false),
("set_theme", false),
("toggle_maximize", false),
("set_background_color", false),
// internal
("internal_toggle_maximize", true),
],
),
(
"core:webview",
&[
("create_webview", false),
("create_webview_window", false),
// getters
("get_all_webviews", true),
("webview_position", true),
("webview_size", true),
// setters
("webview_close", false),
("set_webview_size", false),
("set_webview_position", false),
("set_webview_focus", false),
("set_webview_auto_resize", false),
("set_webview_zoom", false),
("webview_hide", false),
("webview_show", false),
("print", false),
("reparent", false),
("clear_all_browsing_data", false),
("set_webview_background_color", false),
// internal
("internal_toggle_devtools", true),
],
),
(
"core:app",
&[
("version", true),
("name", true),
("tauri_version", true),
("identifier", true),
("app_show", false),
("app_hide", false),
("exit", false),
("fetch_data_store_identifiers", false),
("remove_data_store", false),
("default_window_icon", false),
("set_app_theme", false),
("set_dock_visibility", false),
("bundle_type", true),
("register_listener", true),
("remove_listener", true),
("supports_multiple_windows", true),
],
),
(
"core:image",
&[
("new", true),
("from_bytes", true),
("from_path", true),
("rgba", true),
("size", true),
],
),
("core:resources", &[("close", true)]),
(
"core:menu",
&[
("new", true),
("append", true),
("prepend", true),
("insert", true),
("remove", true),
("remove_at", true),
("items", true),
("get", true),
("popup", true),
("create_default", true),
("set_as_app_menu", true),
("set_as_window_menu", true),
("text", true),
("set_text", true),
("is_enabled", true),
("set_enabled", true),
("set_accelerator", true),
("set_as_windows_menu_for_nsapp", true),
("set_as_help_menu_for_nsapp", true),
("is_checked", true),
("set_checked", true),
("set_icon", true),
],
),
(
"core:tray",
&[
("new", true),
("get_by_id", true),
("remove_by_id", true),
("set_icon", true),
("set_menu", true),
("set_tooltip", true),
("set_title", true),
("set_visible", true),
("set_temp_dir_path", true),
("set_icon_as_template", true),
("set_icon_with_as_template", true),
("set_show_menu_on_left_click", true),
],
),
];
// checks if the given Cargo feature is enabled.
fn has_feature(checked_features: &mut Vec<&'static str>, feature: &'static str) -> bool {
checked_features.push(feature);
// when a feature is enabled, Cargo sets the `CARGO_FEATURE_<name>` env var to 1
// <https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-build-scripts>
std::env::var_os(format!("CARGO_FEATURE_{}", AsShoutySnakeCase(feature)))
.map(|x| x == "1")
.unwrap_or(false)
}
// creates a cfg alias if `has_feature` is true.
// `alias` must be a snake case string.
fn alias(alias: &str, has_feature: bool) {
println!("cargo:rustc-check-cfg=cfg({alias})");
if has_feature {
println!("cargo:rustc-cfg={alias}");
}
}
fn main() {
let mut checked_features = Vec::new();
let custom_protocol = has_feature(&mut checked_features, "custom-protocol");
let dev = !custom_protocol;
alias("custom_protocol", custom_protocol);
alias("dev", dev);
println!("cargo:dev={dev}");
let target_os = std::env::var_os("CARGO_CFG_TARGET_OS").unwrap();
let mobile = target_os == "ios" || target_os == "android";
alias("desktop", !mobile);
alias("mobile", mobile);
let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap());
let checked_features_out_path = out_dir.join("checked_features");
// file is used by a unit test in src/lib.rs to check that the features checked do exist in the Cargo.toml
std::fs::write(checked_features_out_path, checked_features.join(","))
.expect("failed to write checked_features file");
// workaround needed to prevent `STATUS_ENTRYPOINT_NOT_FOUND` error in tests
// see https://github.com/tauri-apps/tauri/pull/4383#issuecomment-1212221864
let target_env = std::env::var_os("CARGO_CFG_TARGET_ENV");
let is_tauri_workspace = std::env::var_os("__TAURI_WORKSPACE__").is_some_and(|v| v == "true");
if is_tauri_workspace && target_os == "windows" && target_env.is_some_and(|trgt| trgt == "msvc") {
embed_manifest_for_tests();
}
if target_os == "android" {
fn env_var(var: &str) -> String {
std::env::var(var).unwrap_or_else(|_| {
panic!("`{var}` is not set, which is needed to generate the kotlin files for android.")
})
}
if let Some(kotlin_out_dir) = std::env::var_os("WRY_ANDROID_KOTLIN_FILES_OUT_DIR") {
let package = env_var("WRY_ANDROID_PACKAGE");
let library = env_var("WRY_ANDROID_LIBRARY");
let kotlin_out_dir = PathBuf::from(&kotlin_out_dir)
.canonicalize()
.unwrap_or_else(move |_| {
panic!(
"Failed to canonicalize `WRY_ANDROID_KOTLIN_FILES_OUT_DIR` path {kotlin_out_dir:?}"
)
});
let kotlin_files_path =
PathBuf::from(env_var("CARGO_MANIFEST_DIR")).join("mobile/android-codegen");
println!("cargo:rerun-if-changed={}", kotlin_files_path.display());
let kotlin_files =
fs::read_dir(kotlin_files_path).expect("failed to read Android codegen directory");
for file in kotlin_files {
let file = file.unwrap();
let content = fs::read_to_string(file.path())
.expect("failed to read kotlin file as string")
.replace("{{package}}", &package)
.replace("{{library}}", &library);
let out_path = kotlin_out_dir.join(file.file_name());
// Overwrite only if changed to not trigger rebuilds
write_if_changed(&out_path, &content).expect("Failed to write kotlin file");
println!("cargo:rerun-if-changed={}", out_path.display());
}
}
if let Some(project_dir) = env::var_os("TAURI_ANDROID_PROJECT_PATH").map(PathBuf::from) {
let package_unescaped = env::var("TAURI_ANDROID_PACKAGE_UNESCAPED")
.unwrap_or_else(|_| env_var("WRY_ANDROID_PACKAGE").replace('`', ""));
let tauri_proguard =
include_str!("./mobile/proguard-tauri.pro").replace("$PACKAGE", &package_unescaped);
std::fs::write(
project_dir.join("app").join("proguard-tauri.pro"),
tauri_proguard,
)
.expect("failed to write proguard-tauri.pro");
}
let lib_path =
PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR").unwrap()).join("mobile/android");
println!("cargo:android_library_path={}", lib_path.display());
}
#[cfg(target_os = "macos")]
{
if target_os == "ios" {
let lib_path =
PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR").unwrap()).join("mobile/ios-api");
tauri_utils::build::link_apple_library("Tauri", &lib_path);
println!("cargo:ios_library_path={}", lib_path.display());
}
}
let tauri_global_scripts = PathBuf::from("./scripts/bundle.global.js")
.canonicalize()
.expect("failed to canonicalize tauri global API script path");
tauri_utils::plugin::define_global_api_script_path(&tauri_global_scripts);
// This should usually be done in `tauri-build`,
// but we need to do this here for the examples in this workspace to work as they don't have build scripts
if is_tauri_workspace {
tauri_utils::plugin::save_global_api_scripts_paths(&out_dir, Some(tauri_global_scripts));
}
let permissions = define_permissions(&out_dir);
tauri_utils::acl::build::generate_allowed_commands(&out_dir, None, permissions).unwrap();
}
const LICENSE_HEADER: &str = r"# Copyright 2019-2024 Tauri Programme within The Commons Conservancy
# SPDX-License-Identifier: Apache-2.0
# SPDX-License-Identifier: MIT
";
fn define_permissions(
out_dir: &Path,
) -> BTreeMap<String, Vec<tauri_utils::acl::manifest::PermissionFile>> {
let mut all_permissions = BTreeMap::new();
for (plugin, commands) in PLUGINS {
let plugin_directory_name = plugin.strip_prefix("core:").unwrap_or(plugin);
let permissions_out_dir = out_dir.join("permissions").join(plugin_directory_name);
let autogenerated =
permissions_out_dir.join(tauri_utils::acl::build::AUTOGENERATED_FOLDER_NAME);
let commands_dir = autogenerated.join("commands");
tauri_utils::acl::build::autogenerate_command_permissions(
&commands_dir,
&commands.iter().map(|(cmd, _)| *cmd).collect::<Vec<_>>(),
LICENSE_HEADER,
false,
);
let default_permissions: Vec<_> = commands.iter().filter(|(_cmd, default)| *default).collect();
let all_commands_enabled_by_default = commands.len() == default_permissions.len();
let default_permissions = default_permissions
.into_iter()
.map(|(cmd, _)| {
let slugified_command = cmd.replace('_', "-");
format!("\"allow-{slugified_command}\"")
})
.collect::<Vec<_>>()
.join(", ");
let all_enable_by_default = if all_commands_enabled_by_default {
", which enables all commands"
} else {
""
};
let default_toml = format!(
r#"{LICENSE_HEADER}# Automatically generated - DO NOT EDIT!
[default]
description = "Default permissions for the plugin{all_enable_by_default}."
permissions = [{default_permissions}]
"#,
);
let out_path = autogenerated.join("default.toml");
write_if_changed(out_path, default_toml)
.unwrap_or_else(|_| panic!("unable to autogenerate default permissions"));
let permissions = tauri_utils::acl::build::define_permissions(
&PathBuf::from(glob::Pattern::escape(
&permissions_out_dir.to_string_lossy(),
))
.join("**")
.join("*.toml")
.to_string_lossy(),
&format!("tauri:{plugin}"),
out_dir,
|_| true,
)
.unwrap_or_else(|e| panic!("failed to define permissions for {plugin}: {e}"));
let docs_out_dir = Path::new("permissions")
.join(plugin_directory_name)
.join("autogenerated");
fs::create_dir_all(&docs_out_dir).expect("failed to create plugin documentation directory");
tauri_utils::acl::build::generate_docs(
&permissions,
&docs_out_dir,
plugin.strip_prefix("tauri-plugin-").unwrap_or(plugin),
)
.expect("failed to generate plugin documentation page");
all_permissions.insert(plugin.to_string(), permissions);
}
let default_permissions = define_default_permission_set(out_dir);
all_permissions.insert("core".to_string(), default_permissions);
all_permissions
}
fn define_default_permission_set(
out_dir: &Path,
) -> Vec<tauri_utils::acl::manifest::PermissionFile> {
let permissions_out_dir = out_dir.join("permissions");
fs::create_dir_all(&permissions_out_dir)
.expect("failed to create core:default permissions directory");
let default_toml = permissions_out_dir.join("default.toml");
let toml_content = format!(
r#"{LICENSE_HEADER}
[default]
description = "Default core plugins set."
permissions = [{}]
"#,
PLUGINS
.iter()
.map(|(k, _)| format!("\"{k}:default\""))
.collect::<Vec<_>>()
.join(",")
);
write_if_changed(default_toml, toml_content)
.unwrap_or_else(|_| panic!("unable to autogenerate core:default set"));
tauri_utils::acl::build::define_permissions(
&PathBuf::from(glob::Pattern::escape(
&permissions_out_dir.to_string_lossy(),
))
.join("*.toml")
.to_string_lossy(),
"tauri:core",
out_dir,
|_| true,
)
.unwrap_or_else(|e| panic!("failed to define permissions for `core:default` : {e}"))
}
fn embed_manifest_for_tests() {
static WINDOWS_MANIFEST_FILE: &str = "windows-app-manifest.xml";
let manifest = std::env::current_dir()
.unwrap()
.join("../tauri-build/src")
.join(WINDOWS_MANIFEST_FILE);
println!("cargo:rerun-if-changed={}", manifest.display());
// Embed the Windows application manifest file.
println!("cargo:rustc-link-arg=/MANIFEST:EMBED");
println!(
"cargo:rustc-link-arg=/MANIFESTINPUT:{}",
manifest.to_str().unwrap()
);
// Turn linker warnings into errors.
println!("cargo:rustc-link-arg=/WX");
}

View File

@@ -0,0 +1,60 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
/* THIS FILE IS AUTO-GENERATED. DO NOT MODIFY!! */
package {{package}}
import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle
import app.tauri.plugin.PluginManager
abstract class TauriActivity : WryActivity() {
override val handleBackNavigation: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
PluginManager.onCreate(this)
}
fun getPluginManager(): PluginManager {
return PluginManager
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
PluginManager.onNewIntent(intent)
}
override fun onRestart() {
super.onRestart()
PluginManager.onRestart(this)
}
override fun onResume() {
super.onResume()
PluginManager.onResume(this)
}
override fun onPause() {
super.onPause()
PluginManager.onPause(this)
}
override fun onStop() {
super.onStop()
PluginManager.onStop(this)
}
override fun onDestroy() {
super.onDestroy()
PluginManager.onDestroy(this)
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
PluginManager.onConfigurationChanged(newConfig)
}
}

View File

@@ -0,0 +1 @@
/build

View File

@@ -0,0 +1,48 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "app.tauri"
compileSdk = 36
defaultConfig {
minSdk = 21
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
buildFeatures {
buildConfig = true
}
}
kotlin {
compilerOptions {
jvmTarget = JvmTarget.JVM_1_8
}
}
dependencies {
implementation("androidx.core:core-ktx:1.7.0")
implementation("androidx.appcompat:appcompat:1.6.0")
implementation("com.google.android.material:material:1.7.0")
implementation("com.fasterxml.jackson.core:jackson-databind:2.15.3")
// (de)serializes org.json values (JSObject, JSArray) as JSON objects and arrays
// instead of as beans; org.json itself is part of the Android platform
implementation("com.fasterxml.jackson.datatype:jackson-datatype-json-org:2.15.3") {
exclude(group = "org.json", module = "json")
}
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
}

View File

@@ -0,0 +1,41 @@
-keep class app.tauri.** {
@app.tauri.JniMethod public <methods>;
native <methods>;
}
-keep class app.tauri.plugin.JSArray {
public <init>(...);
}
-keepclassmembers class org.json.JSONArray {
public put(...);
}
-keep class app.tauri.plugin.JSObject {
public <init>(...);
public put(...);
}
-keep @app.tauri.annotation.TauriPlugin public class * {
@app.tauri.annotation.Command public <methods>;
@app.tauri.annotation.PermissionCallback <methods>;
@app.tauri.annotation.ActivityCallback <methods>;
@app.tauri.annotation.Permission <methods>;
public <init>(...);
}
-keep @app.tauri.annotation.InvokeArg public class * {
*;
}
-keep @com.fasterxml.jackson.databind.annotation.JsonDeserialize public class * {
*;
}
-keep @com.fasterxml.jackson.databind.annotation.JsonSerialize public class * {
*;
}
-keep class * extends com.fasterxml.jackson.databind.JsonDeserializer { *; }
-keep class * extends com.fasterxml.jackson.databind.JsonSerializer { *; }

View File

@@ -0,0 +1,28 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package app.tauri
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("app.tauri.test", appContext.packageName)
}
}

View File

@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>

View File

@@ -0,0 +1,56 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package app.tauri
import android.app.Activity
import android.webkit.WebView
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
import app.tauri.annotation.Command
import app.tauri.annotation.TauriPlugin
import app.tauri.plugin.Plugin
import app.tauri.plugin.Invoke
import app.tauri.plugin.JSObject
@TauriPlugin
class AppPlugin(private val activity: Activity): Plugin(activity) {
private val BACK_BUTTON_EVENT = "back-button"
private var webView: WebView? = null
override fun load(webView: WebView) {
this.webView = webView
}
init {
val callback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (!hasListener(BACK_BUTTON_EVENT)) {
if (this@AppPlugin.webView?.canGoBack() == true) {
this@AppPlugin.webView!!.goBack()
} else {
this.isEnabled = false
this@AppPlugin.activity.onBackPressed()
this.isEnabled = true
}
} else {
val data = JSObject().apply {
put("canGoBack", this@AppPlugin.webView?.canGoBack() ?: false)
}
trigger(BACK_BUTTON_EVENT, data)
}
}
}
(activity as AppCompatActivity).onBackPressedDispatcher.addCallback(activity, callback)
}
// Called by the Rust side when the app exits so the activity (and any other activity
// of the task, i.e. other windows) is closed gracefully instead of the process being killed
@Command
fun exit(invoke: Invoke) {
invoke.resolve()
activity.finishAffinity()
}
}

View File

@@ -0,0 +1,208 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package app.tauri
import android.content.ContentUris
import android.content.Context
import android.content.res.AssetManager
import android.database.Cursor
import android.net.Uri
import android.os.Environment
import android.provider.DocumentsContract
import android.provider.MediaStore
import android.provider.OpenableColumns
import java.io.File
import java.io.FileOutputStream
import kotlin.math.min
internal class FsUtils {
companion object {
fun readAsset(assetManager: AssetManager, fileName: String): String {
assetManager.open(fileName).bufferedReader().use {
return it.readText()
}
}
fun getFileUrlForUri(context: Context, uri: Uri): String? {
// DocumentProvider
if (DocumentsContract.isDocumentUri(context, uri)) {
// ExternalStorageProvider
if (isExternalStorageDocument(uri)) {
val docId: String = DocumentsContract.getDocumentId(uri)
val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }
.toTypedArray()
val type = split[0]
if ("primary".equals(type, ignoreCase = true)) {
return legacyPrimaryPath(split[1])
} else {
val splitIndex = docId.indexOf(':', 1)
val tag = docId.substring(0, splitIndex)
val path = docId.substring(splitIndex + 1)
val nonPrimaryVolume = getPathToNonPrimaryVolume(context, tag)
if (nonPrimaryVolume != null) {
val result = "$nonPrimaryVolume/$path"
val file = File(result)
return if (file.exists() && file.canRead()) {
result
} else null
}
}
} else if (isDownloadsDocument(uri)) {
val id: String = DocumentsContract.getDocumentId(uri)
val contentUri: Uri = ContentUris.withAppendedId(
Uri.parse("content://downloads/public_downloads"),
java.lang.Long.valueOf(id)
)
return getDataColumn(context, contentUri, null, null)
} else if (isMediaDocument(uri)) {
val docId: String = DocumentsContract.getDocumentId(uri)
val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }
.toTypedArray()
val type = split[0]
var contentUri: Uri? = null
when (type) {
"image" -> {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
}
"video" -> {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
}
"audio" -> {
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
}
}
val selection = "_id=?"
val selectionArgs = arrayOf(split[1])
if (contentUri != null) {
return getDataColumn(context, contentUri, selection, selectionArgs)
}
}
} else if ("content".equals(uri.scheme, ignoreCase = true)) {
// Return the remote address
return if (isGooglePhotosUri(uri)) uri.lastPathSegment else getDataColumn(
context,
uri,
null,
null
)
} else if ("file".equals(uri.scheme, ignoreCase = true)) {
return uri.path
}
return null
}
/**
* Get the value of the data column for this Uri. This is useful for
* MediaStore Uris, and other file-based ContentProviders.
*
* @param context The context.
* @param uri The Uri to query.
* @param selection (Optional) Filter used in the query.
* @param selectionArgs (Optional) Selection arguments used in the query.
* @return The value of the _data column, which is typically a file path.
*/
private fun getDataColumn(
context: Context,
uri: Uri,
selection: String?,
selectionArgs: Array<String>?
): String? {
var path: String? = null
var cursor: Cursor? = null
val column = "_data"
val projection = arrayOf(column)
try {
cursor = context.contentResolver.query(uri, projection, selection, selectionArgs, null)
if (cursor != null && cursor.moveToFirst()) {
val index = cursor.getColumnIndexOrThrow(column)
path = cursor.getString(index)
}
} catch (ex: IllegalArgumentException) {
return getCopyFilePath(uri, context)
} finally {
cursor?.close()
}
return path ?: getCopyFilePath(uri, context)
}
private fun getCopyFilePath(uri: Uri, context: Context): String? {
val cursor = context.contentResolver.query(uri, null, null, null, null)!!
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
cursor.moveToFirst()
val name = cursor.getString(nameIndex)
val file = File(context.filesDir, name)
try {
val inputStream = context.contentResolver.openInputStream(uri)
val outputStream = FileOutputStream(file)
var read: Int
val maxBufferSize = 1024 * 1024
val bufferSize = min(inputStream!!.available(), maxBufferSize)
val buffers = ByteArray(bufferSize)
while (inputStream.read(buffers).also { read = it } != -1) {
outputStream.write(buffers, 0, read)
}
inputStream.close()
outputStream.close()
} catch (e: Exception) {
return null
} finally {
cursor.close()
}
return file.path
}
private fun legacyPrimaryPath(pathPart: String): String {
return Environment.getExternalStorageDirectory().toString() + "/" + pathPart
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is ExternalStorageProvider.
*/
private fun isExternalStorageDocument(uri: Uri): Boolean {
return "com.android.externalstorage.documents" == uri.authority
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is DownloadsProvider.
*/
private fun isDownloadsDocument(uri: Uri): Boolean {
return "com.android.providers.downloads.documents" == uri.authority
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is MediaProvider.
*/
private fun isMediaDocument(uri: Uri): Boolean {
return "com.android.providers.media.documents" == uri.authority
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is Google Photos.
*/
private fun isGooglePhotosUri(uri: Uri): Boolean {
return "com.google.android.apps.photos.content" == uri.authority
}
private fun getPathToNonPrimaryVolume(context: Context, tag: String): String? {
val volumes = context.externalCacheDirs
if (volumes != null) {
for (volume in volumes) {
if (volume != null) {
val path = volume.absolutePath
val index = path.indexOf(tag)
if (index != -1) {
return path.substring(0, index) + tag
}
}
}
}
return null
}
}
}

View File

@@ -0,0 +1,8 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package app.tauri
@Retention(AnnotationRetention.RUNTIME)
internal annotation class JniMethod

View File

@@ -0,0 +1,85 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package app.tauri
// taken from https://github.com/ionic-team/capacitor/blob/6658bca41e78239347e458175b14ca8bd5c1d6e8/android/capacitor/src/main/java/com/getcapacitor/Logger.java
import android.text.TextUtils;
import android.util.Log;
class Logger {
companion object {
private const val LOG_TAG_CORE = "Tauri"
fun tags(vararg subtags: String): String {
return if (subtags.isNotEmpty()) {
LOG_TAG_CORE + "/" + TextUtils.join("/", subtags)
} else LOG_TAG_CORE
}
fun verbose(message: String) {
verbose(LOG_TAG_CORE, message)
}
fun verbose(tag: String, message: String) {
if (!shouldLog()) {
return
}
Log.v(tag, message)
}
fun debug(message: String) {
debug(LOG_TAG_CORE, message)
}
fun debug(tag: String, message: String) {
if (!shouldLog()) {
return
}
Log.d(tag, message)
}
fun info(message: String) {
info(LOG_TAG_CORE, message)
}
fun info(tag: String, message: String) {
if (!shouldLog()) {
return
}
Log.i(tag, message)
}
fun warn(message: String) {
warn(LOG_TAG_CORE, message)
}
fun warn(tag: String, message: String) {
if (!shouldLog()) {
return
}
Log.w(tag, message)
}
fun error(message: String) {
error(LOG_TAG_CORE, message, null)
}
fun error(message: String, e: Throwable?) {
error(LOG_TAG_CORE, message, e)
}
fun error(tag: String, message: String, e: Throwable?) {
if (!shouldLog()) {
return
}
Log.e(tag, message, e)
}
private fun shouldLog(): Boolean {
return BuildConfig.DEBUG
}
}
}

View File

@@ -0,0 +1,132 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package app.tauri
import android.app.Activity
import android.database.Cursor
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.OpenableColumns
import app.tauri.annotation.Command
import app.tauri.annotation.InvokeArg
import app.tauri.annotation.TauriPlugin
import app.tauri.plugin.Plugin
import app.tauri.plugin.Invoke
import app.tauri.plugin.JSObject
const val TAURI_ASSETS_DIRECTORY_URI = "asset://localhost/"
@InvokeArg
class GetFileNameFromUriArgs {
lateinit var uri: String
}
@TauriPlugin
class PathPlugin(private val activity: Activity): Plugin(activity) {
private fun resolvePath(invoke: Invoke, path: String?) {
val obj = JSObject()
obj.put("path", path)
invoke.resolve(obj)
}
@Command
fun getFileNameFromUri(invoke: Invoke) {
val args = invoke.parseArgs(GetFileNameFromUriArgs::class.java)
val name = getRealNameFromURI(activity, Uri.parse(args.uri))
val res = JSObject()
res.put("name", name)
invoke.resolve(res)
}
@Command
fun getAudioDir(invoke: Invoke) {
resolvePath(invoke, activity.getExternalFilesDir(Environment.DIRECTORY_MUSIC)?.absolutePath)
}
@Command
fun getExternalCacheDir(invoke: Invoke) {
resolvePath(invoke, activity.externalCacheDir?.absolutePath)
}
@Command
fun getConfigDir(invoke: Invoke) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
resolvePath(invoke, activity.dataDir.absolutePath)
} else {
resolvePath(invoke, activity.applicationInfo.dataDir)
}
}
@Command
fun getDataDir(invoke: Invoke) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
resolvePath(invoke, activity.dataDir.absolutePath)
} else {
resolvePath(invoke, activity.applicationInfo.dataDir)
}
}
@Command
fun getDocumentDir(invoke: Invoke) {
resolvePath(invoke, activity.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS)?.absolutePath)
}
@Command
fun getDownloadDir(invoke: Invoke) {
resolvePath(invoke, activity.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)?.absolutePath)
}
@Command
fun getPictureDir(invoke: Invoke) {
resolvePath(invoke, activity.getExternalFilesDir(Environment.DIRECTORY_PICTURES)?.absolutePath)
}
@Command
fun getPublicDir(invoke: Invoke) {
resolvePath(invoke, activity.getExternalFilesDir(Environment.DIRECTORY_DCIM)?.absolutePath)
}
@Command
fun getVideoDir(invoke: Invoke) {
resolvePath(invoke, activity.getExternalFilesDir(Environment.DIRECTORY_MOVIES)?.absolutePath)
}
@Command
fun getResourcesDir(invoke: Invoke) {
resolvePath(invoke, TAURI_ASSETS_DIRECTORY_URI)
}
@Command
fun getCacheDir(invoke: Invoke) {
resolvePath(invoke, activity.cacheDir.absolutePath)
}
@Command
fun getHomeDir(invoke: Invoke) {
resolvePath(invoke, Environment.getExternalStorageDirectory().absolutePath)
}
}
fun getRealNameFromURI(activity: Activity, contentUri: Uri): String? {
var cursor: Cursor? = null
try {
val projection = arrayOf(OpenableColumns.DISPLAY_NAME)
cursor = activity.contentResolver.query(contentUri, projection, null, null, null)
cursor?.let {
val columnIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME)
if (it.moveToFirst()) {
return it.getString(columnIndex)
}
}
} catch (e: Exception) {
Logger.error("failed to get real name from URI $e")
} finally {
cursor?.close()
}
return null // Return null if no file name could be resolved
}

View File

@@ -0,0 +1,107 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package app.tauri
// taken from https://github.com/ionic-team/capacitor/blob/6658bca41e78239347e458175b14ca8bd5c1d6e8/android/capacitor/src/main/java/com/getcapacitor/PermissionHelper.java
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import androidx.core.app.ActivityCompat;
import java.util.ArrayList;
object PermissionHelper {
/**
* Checks if a list of given permissions are all granted by the user
*
* @param permissions Permissions to check.
* @return True if all permissions are granted, false if at least one is not.
*/
fun hasPermissions(context: Context?, permissions: Array<String>): Boolean {
for (perm in permissions) {
if (ActivityCompat.checkSelfPermission(
context!!,
perm
) != PackageManager.PERMISSION_GRANTED
) {
return false
}
}
return true
}
/**
* Check whether the given permission has been defined in the AndroidManifest.xml
*
* @param permission A permission to check.
* @return True if the permission has been defined in the Manifest, false if not.
*/
fun hasDefinedPermission(context: Context, permission: String): Boolean {
val requestedPermissions = getManifestPermissions(context)
if (!requestedPermissions.isNullOrEmpty()) {
val requestedPermissionsList = listOf(*requestedPermissions)
val requestedPermissionsArrayList = ArrayList(requestedPermissionsList)
if (requestedPermissionsArrayList.contains(permission)) {
return true
}
}
return false
}
/**
* Check whether all of the given permissions have been defined in the AndroidManifest.xml
* @param context the app context
* @param permissions a list of permissions
* @return true only if all permissions are defined in the AndroidManifest.xml
*/
fun hasDefinedPermissions(context: Context, permissions: Array<String>): Boolean {
return permissions.all { hasDefinedPermission(context, it) }
}
/**
* Get the permissions defined in AndroidManifest.xml
*
* @return The permissions defined in AndroidManifest.xml
*/
private fun getManifestPermissions(context: Context): Array<String>? {
var requestedPermissions: Array<String>? = null
try {
val pm = context.packageManager
val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
pm.getPackageInfo(context.packageName, PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS.toLong()))
} else {
@Suppress("DEPRECATION")
pm.getPackageInfo(context.packageName, PackageManager.GET_PERMISSIONS)
}
if (packageInfo != null) {
requestedPermissions = packageInfo.requestedPermissions
}
} catch (_: Exception) {
}
return requestedPermissions
}
/**
* Given a list of permissions, return a new list with the ones not present in AndroidManifest.xml
*
* @param neededPermissions The permissions needed.
* @return The permissions not present in AndroidManifest.xml
*/
fun getUndefinedPermissions(context: Context, neededPermissions: Array<String>): Array<String> {
val requestedPermissions = getManifestPermissions(context)
if (!requestedPermissions.isNullOrEmpty()) {
val undefinedPermissions = ArrayList<String>()
val requestedPermissionsList = listOf(*requestedPermissions)
val requestedPermissionsArrayList = ArrayList(requestedPermissionsList)
for (permission in neededPermissions) {
if (!requestedPermissionsArrayList.contains(permission)) {
undefinedPermissions.add(permission)
}
}
return undefinedPermissions.toTypedArray()
}
return neededPermissions
}
}

View File

@@ -0,0 +1,21 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package app.tauri
import java.util.*
enum class PermissionState(private val state: String) {
GRANTED("granted"), DENIED("denied"), PROMPT("prompt"), PROMPT_WITH_RATIONALE("prompt-with-rationale");
override fun toString(): String {
return state
}
companion object {
fun byState(state: String): PermissionState {
return valueOf(state.uppercase(Locale.ROOT).replace('-', '_'))
}
}
}

View File

@@ -0,0 +1,9 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package app.tauri.annotation
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class ActivityCallback

View File

@@ -0,0 +1,9 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package app.tauri.annotation
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class InvokeArg

View File

@@ -0,0 +1,19 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package app.tauri.annotation
@Retention(AnnotationRetention.RUNTIME)
annotation class Permission(
/**
* An array of Android permission strings.
* Eg: {Manifest.permission.ACCESS_COARSE_LOCATION}
* or {"android.permission.ACCESS_COARSE_LOCATION"}
*/
val strings: Array<String> = [],
/**
* An optional name to use instead of the Android permission string.
*/
val alias: String = ""
)

View File

@@ -0,0 +1,9 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package app.tauri.annotation
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class PermissionCallback

View File

@@ -0,0 +1,8 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package app.tauri.annotation
@Retention(AnnotationRetention.RUNTIME)
annotation class Command

View File

@@ -0,0 +1,19 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package app.tauri.annotation
import app.tauri.annotation.Permission
/**
* Base annotation for all Plugins
*/
@Retention(AnnotationRetention.RUNTIME)
annotation class TauriPlugin(
/**
* Permissions this plugin needs, in order to make permission requests
* easy if the plugin only needs basic permission prompting
*/
val permissions: Array<Permission> = []
)

View File

@@ -0,0 +1,33 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package app.tauri.plugin
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.ObjectMapper
const val CHANNEL_PREFIX = "__CHANNEL__:"
internal class ChannelDeserializer(val sendChannelData: (channelId: Long, data: String) -> Unit, private val objectMapper: ObjectMapper): JsonDeserializer<Channel>() {
override fun deserialize(
jsonParser: JsonParser?,
deserializationContext: DeserializationContext
): Channel {
val channelDef = deserializationContext.readValue(jsonParser, String::class.java)
val callback = channelDef.substring(CHANNEL_PREFIX.length).toLongOrNull() ?: throw Error("unexpected channel value $channelDef")
return Channel(callback, { res -> sendChannelData(callback, res) }, objectMapper)
}
}
class Channel(val id: Long, private val handler: (data: String) -> Unit, private val objectMapper: ObjectMapper) {
fun send(data: JSObject) {
handler(PluginResult(data).toString())
}
fun sendObject(data: Any) {
handler(objectMapper.writeValueAsString(data))
}
}

View File

@@ -0,0 +1,11 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package app.tauri.plugin
internal class InvalidCommandException : Exception {
constructor(s: String?) : super(s) {}
constructor(t: Throwable?) : super(t) {}
constructor(s: String?, t: Throwable?) : super(s, t) {}
}

View File

@@ -0,0 +1,93 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package app.tauri.plugin
import app.tauri.Logger
import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.databind.ObjectMapper
class Invoke(
val id: Long,
val command: String,
val callback: Long,
val error: Long,
private val sendResponse: (callback: Long, data: String) -> Unit,
private val argsJson: String,
private val jsonMapper: ObjectMapper
) {
fun getRawArgs(): String {
return argsJson
}
fun getArgs(): JSObject {
return JSObject(argsJson)
}
fun<T> parseArgs(cls: Class<T>): T {
return jsonMapper.readValue(argsJson, cls)
}
fun<T> parseArgs(ref: TypeReference<T>): T {
return jsonMapper.readValue(argsJson, ref)
}
fun resolve(data: JSObject?) {
sendResponse(callback, PluginResult(data).toString())
}
fun resolveObject(data: Any) {
sendResponse(callback, jsonMapper.writeValueAsString(data))
}
fun resolve() {
sendResponse(callback, "null")
}
fun reject(msg: String?, code: String?, ex: Exception?, data: JSObject?) {
val errorResult = PluginResult()
if (ex != null) {
Logger.error(Logger.tags("Plugin"), msg!!, ex)
}
errorResult.put("message", msg)
if (code != null) {
errorResult.put("code", code)
}
if (data != null) {
errorResult.put("data", data)
}
sendResponse(error, errorResult.toString())
}
fun reject(msg: String?, ex: Exception?, data: JSObject?) {
reject(msg, null, ex, data)
}
fun reject(msg: String?, code: String?, data: JSObject?) {
reject(msg, code, null, data)
}
fun reject(msg: String?, code: String?, ex: Exception?) {
reject(msg, code, ex, null)
}
fun reject(msg: String?, data: JSObject?) {
reject(msg, null, null, data)
}
fun reject(msg: String?, ex: Exception?) {
reject(msg, null, ex, null)
}
fun reject(msg: String?, code: String?) {
reject(msg, code, null, null)
}
fun reject(msg: String?) {
reject(msg, null, null, null)
}
}

View File

@@ -0,0 +1,45 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package app.tauri.plugin
import org.json.JSONArray
import org.json.JSONException
class JSArray : JSONArray {
constructor() : super() {}
constructor(json: String?) : super(json) {}
constructor(copyFrom: Collection<*>?) : super(copyFrom) {}
constructor(array: Any?) : super(array) {}
@Suppress("UNCHECKED_CAST", "ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE")
@Throws(JSONException::class)
fun <E> toList(): List<E> {
val items: MutableList<E> = ArrayList()
var o: Any? = null
for (i in 0 until this.length()) {
this.get(i).also { o = it }
try {
items.add(this.get(i) as E)
} catch (ex: Exception) {
throw JSONException("Not all items are instances of the given type")
}
}
return items
}
companion object {
/**
* Create a new JSArray without throwing a error
*/
fun from(array: Any?): JSArray? {
try {
return JSArray(array)
} catch (ex: JSONException) {
//
}
return null
}
}
}

View File

@@ -0,0 +1,152 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package app.tauri.plugin
import org.json.JSONException
import org.json.JSONObject
class JSObject : JSONObject {
constructor() : super()
constructor(json: String) : super(json)
constructor(obj: JSONObject, names: Array<String>) : super(obj, names)
override fun getString(key: String): String {
return getString(key, "")!!
}
fun getString(key: String, defaultValue: String?): String? {
try {
if (!super.isNull(key)) {
return super.getString(key)
}
} catch (_: JSONException) {
}
return defaultValue
}
fun getInteger(key: String): Int? {
return getIntegerInternal(key, null)
}
fun getInteger(key: String, defaultValue: Int): Int {
return getIntegerInternal(key, defaultValue)!!
}
private fun getIntegerInternal(key: String, defaultValue: Int?): Int? {
try {
return super.getInt(key)
} catch (_: JSONException) {
}
return defaultValue
}
override fun getBoolean(key: String): Boolean {
return getBooleanInternal(key, false)!!
}
fun getBoolean(key: String, defaultValue: Boolean?): Boolean {
return getBooleanInternal(key, defaultValue)!!
}
private fun getBooleanInternal(key: String, defaultValue: Boolean?): Boolean? {
try {
return super.getBoolean(key)
} catch (_: JSONException) {
}
return defaultValue
}
fun getJSObject(name: String): JSObject? {
try {
return getJSObjectInternal(name, null)
} catch (_: JSONException) {
}
return null
}
fun getJSObject(name: String, defaultValue: JSObject): JSObject {
return getJSObjectInternal(name, defaultValue)!!
}
private fun getJSObjectInternal(name: String, defaultValue: JSObject?): JSObject? {
try {
val obj = get(name)
if (obj is JSONObject) {
val keysIter = obj.keys()
val keys: MutableList<String> = ArrayList()
while (keysIter.hasNext()) {
keys.add(keysIter.next())
}
return JSObject(obj, keys.toTypedArray())
}
} catch (_: JSONException) {
}
return defaultValue
}
override fun put(key: String, value: Boolean): JSObject {
try {
super.put(key, value)
} catch (_: JSONException) {
}
return this
}
override fun put(key: String, value: Int): JSObject {
try {
super.put(key, value)
} catch (_: JSONException) {
}
return this
}
override fun put(key: String, value: Long): JSObject {
try {
super.put(key, value)
} catch (_: JSONException) {
}
return this
}
override fun put(key: String, value: Double): JSObject {
try {
super.put(key, value)
} catch (_: JSONException) {
}
return this
}
override fun put(key: String, value: Any?): JSObject {
try {
super.put(key, value)
} catch (_: JSONException) {
}
return this
}
fun put(key: String, value: String?): JSObject {
try {
super.put(key, value)
} catch (_: JSONException) {
}
return this
}
companion object {
/**
* Convert a pathetic JSONObject into a JSObject
* @param obj
*/
@Throws(JSONException::class)
fun fromJSONObject(obj: JSONObject): JSObject {
val keysIter = obj.keys()
val keys: MutableList<String> = ArrayList()
while (keysIter.hasNext()) {
keys.add(keysIter.next())
}
return JSObject(obj, keys.toTypedArray())
}
}
}

View File

@@ -0,0 +1,536 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package app.tauri.plugin
import android.app.Activity
import android.content.res.Configuration
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.webkit.WebView
import androidx.activity.result.IntentSenderRequest
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import app.tauri.FsUtils
import app.tauri.Logger
import app.tauri.PermissionHelper
import app.tauri.PermissionState
import app.tauri.annotation.ActivityCallback
import app.tauri.annotation.Command
import app.tauri.annotation.InvokeArg
import app.tauri.annotation.PermissionCallback
import app.tauri.annotation.TauriPlugin
import com.fasterxml.jackson.databind.ObjectMapper
import java.util.*
import java.util.concurrent.CopyOnWriteArrayList
@InvokeArg
internal class RegisterListenerArgs {
lateinit var event: String
lateinit var handler: Channel
}
@InvokeArg
internal class RemoveListenerArgs {
lateinit var event: String
var channelId: Long = 0
}
@InvokeArg internal class RequestPermissionsArgs {
var permissions: List<String>? = null
}
abstract class Plugin(private val activity: Activity) {
var handle: PluginHandle? = null
private val listeners: MutableMap<String, MutableList<Channel>> = mutableMapOf()
open fun load(webView: WebView) {}
fun jsonMapper(): ObjectMapper {
return handle!!.jsonMapper
}
fun<T> getConfig(cls: Class<T>): T {
return jsonMapper().readValue(handle!!.config, cls)
}
/**
* Handle a new intent being received by the application
*/
open fun onNewIntent(intent: Intent) {}
/**
* This event is called just before another activity comes into the foreground.
*/
open fun onPause(activity: AppCompatActivity) {}
/**
* This event is called just before another activity comes into the foreground.
*/
@Deprecated("use onPause(activity: AppCompatActivity) instead")
open fun onPause() {}
/**
* This event is called when the user returns to the activity. It is also called on cold starts.
*/
open fun onResume(activity: AppCompatActivity) {}
/**
* This event is called when the user returns to the activity. It is also called on cold starts.
*/
@Deprecated("use onResume(activity: AppCompatActivity) instead")
open fun onResume() {}
/**
* This event is called after onStop() when the current activity is being re-displayed to the user (the user has navigated back to it).
* It will be followed by onStart() and then onResume().
*/
open fun onRestart(activity: AppCompatActivity) {}
/**
* This event is called after onStop() when the current activity is being re-displayed to the user (the user has navigated back to it).
* It will be followed by onStart() and then onResume().
*/
@Deprecated("use onRestart(activity: AppCompatActivity) instead")
open fun onRestart() {}
/**
* This event is called when the app is no longer visible to the user.
* You will next receive either onRestart(), onDestroy(), or nothing, depending on later user activity.
*/
open fun onStop(activity: AppCompatActivity) {}
/**
* This event is called when the app is no longer visible to the user.
* You will next receive either onRestart(), onDestroy(), or nothing, depending on later user activity.
*/
@Deprecated("use onStop(activity: AppCompatActivity) instead")
open fun onStop() {}
/**
* This event is called before the activity is destroyed.
*/
open fun onDestroy(activity: AppCompatActivity) {}
/**
* This event is called before an activity is destroyed.
*/
@Deprecated("use onDestroy(activity: AppCompatActivity) instead")
open fun onDestroy() {}
internal fun triggerOnDestroy(activity: AppCompatActivity) {
onDestroy(activity)
onDestroy()
}
internal fun triggerOnRestart(activity: AppCompatActivity) {
onRestart(activity)
onRestart()
}
internal fun triggerOnPause(activity: AppCompatActivity) {
onPause(activity)
onPause()
}
internal fun triggerOnResume(activity: AppCompatActivity) {
onResume(activity)
onResume()
}
internal fun triggerOnStop(activity: AppCompatActivity) {
onStop(activity)
onStop()
}
/**
* This event is called when a configuration change occurs but the app does not recreate the activity.
*/
open fun onConfigurationChanged(newConfig: Configuration) {}
/**
* Start activity for result with the provided Intent and resolve calling the provided callback method name.
*
* If there is no registered activity callback for the method name passed in, the call will
* be rejected. Make sure a valid activity result callback method is registered using the
* [ActivityCallback] annotation.
*
* @param invoke the invoke object
* @param intent the intent used to start an activity
* @param callbackName the name of the callback to run when the launched activity is finished
*/
fun startActivityForResult(invoke: Invoke, intent: Intent, callbackName: String) {
handle!!.startActivityForResult(invoke, intent, callbackName)
}
/**
* Like startActivityForResult() but taking an IntentSender to describe the activity to be started.
*
* If there is no registered activity callback for the method name passed in, the call will
* be rejected. Make sure a valid activity result callback method is registered using the
* [ActivityCallback] annotation.
*
* @param invoke the invoke object
* @param intentSender the intent used to start an activity
* @param callbackName the name of the callback to run when the launched activity is finished
*/
fun startIntentSenderForResult(invoke: Invoke, intentSender: IntentSenderRequest, callbackName: String) {
handle!!.startIntentSenderForResult(invoke, intentSender, callbackName)
}
/**
* Get the plugin log tags.
* @param subTags
*/
protected fun getLogTag(vararg subTags: String): String {
return Logger.tags(*subTags)
}
/**
* Gets a log tag with the plugin's class name as subTag.
*/
protected fun getLogTag(): String {
return Logger.tags(this.javaClass.simpleName)
}
/**
* Convert an URI to an URL that can be loaded by the webview.
*/
fun assetUrl(u: Uri): String {
var path = FsUtils.getFileUrlForUri(activity, u)
if (path?.startsWith("file://") == true) {
path = path.replace("file://", "")
}
return "asset://localhost$path"
}
fun trigger(event: String, payload: JSObject) {
val eventListeners = listeners[event]
if (!eventListeners.isNullOrEmpty()) {
val listeners = CopyOnWriteArrayList(eventListeners)
for (channel in listeners) {
channel.send(payload)
}
}
}
fun triggerObject(event: String, payload: Any) {
val eventListeners = listeners[event]
if (!eventListeners.isNullOrEmpty()) {
val listeners = CopyOnWriteArrayList(eventListeners)
for (channel in listeners) {
channel.sendObject(payload)
}
}
}
fun hasListener(event: String): Boolean {
return !listeners[event].isNullOrEmpty()
}
@Command
open fun registerListener(invoke: Invoke) {
val args = invoke.parseArgs(RegisterListenerArgs::class.java)
val eventListeners = listeners[args.event]
if (eventListeners.isNullOrEmpty()) {
listeners[args.event] = mutableListOf(args.handler)
} else {
eventListeners.add(args.handler)
}
invoke.resolve()
}
@Command
open fun removeListener(invoke: Invoke) {
val args = invoke.parseArgs(RemoveListenerArgs::class.java)
val eventListeners = listeners[args.event]
if (!eventListeners.isNullOrEmpty()) {
val c = eventListeners.find { c -> c.id == args.channelId }
if (c != null) {
eventListeners.remove(c)
}
}
invoke.resolve()
}
/**
* Exported plugin method for checking the granted status for each permission
* declared on the plugin. This plugin call responds with a mapping of permissions to
* the associated granted status.
*/
@Command
@PermissionCallback
open fun checkPermissions(invoke: Invoke) {
val permissionsResult: Map<String, PermissionState?> = getPermissionStates()
if (permissionsResult.isEmpty()) {
// if no permissions are defined on the plugin, resolve undefined
invoke.resolve()
} else {
val permissionsResultJSON = JSObject()
for ((key, value) in permissionsResult) {
permissionsResultJSON.put(key, value)
}
invoke.resolve(permissionsResultJSON)
}
}
/**
* Exported plugin method to request all permissions for this plugin.
* To manually request permissions within a plugin use:
* [.requestAllPermissions], or
* [.requestPermissionForAlias], or
* [.requestPermissionForAliases]
*
* @param invoke
*/
@Command
open fun requestPermissions(invoke: Invoke) {
val annotation = handle?.annotation
if (annotation != null) {
// handle permission requests for plugins defined with @TauriPlugin
var permAliases: Array<String>? = null
val autoGrantPerms: MutableSet<String> = HashSet()
val args = invoke.parseArgs(RequestPermissionsArgs::class.java)
args.permissions?.let {
val aliasSet: MutableSet<String> = HashSet()
for (perm in annotation.permissions) {
if (it.contains(perm.alias)) {
aliasSet.add(perm.alias)
}
}
if (aliasSet.isEmpty()) {
invoke.reject("No valid permission alias was requested of this plugin.")
return
} else {
permAliases = aliasSet.toTypedArray()
}
} ?: run {
val aliasSet: MutableSet<String> = HashSet()
for (perm in annotation.permissions) {
// If a permission is defined with no permission strings, separate it for auto-granting.
// Otherwise, the alias is added to the list to be requested.
if (perm.strings.isEmpty() || perm.strings.size == 1 && perm.strings[0]
.isEmpty()
) {
if (perm.alias.isNotEmpty()) {
autoGrantPerms.add(perm.alias)
}
} else {
aliasSet.add(perm.alias)
}
}
permAliases = aliasSet.toTypedArray()
}
permAliases?.let {
// request permissions using provided aliases or all defined on the plugin
requestPermissionForAliases(it, invoke, "checkPermissions")
} ?: run {
if (autoGrantPerms.isNotEmpty()) {
// if the plugin only has auto-grant permissions, return all as GRANTED
val permissionsResults = JSObject()
for (perm in autoGrantPerms) {
permissionsResults.put(perm, PermissionState.GRANTED.toString())
}
invoke.resolve(permissionsResults)
} else {
// no permissions are defined on the plugin, resolve undefined
invoke.resolve()
}
}
}
}
/**
* Checks if the given permission alias is correctly declared in AndroidManifest.xml
* @param alias a permission alias defined on the plugin
* @return true only if all permissions associated with the given alias are declared in the manifest
*/
fun isPermissionDeclared(alias: String): Boolean {
for (perm in handle?.annotation?.permissions.orEmpty()) {
if (alias.equals(perm.alias, ignoreCase = true)) {
var result = true
for (permString in perm.strings) {
result = result && PermissionHelper.hasDefinedPermission(activity, permString)
}
return result
}
}
Logger.error(
String.format(
"isPermissionDeclared: No alias defined for %s " + "or missing @TauriPlugin annotation.",
alias
)
)
return false
}
private fun permissionActivityResult(
invoke: Invoke,
permissionStrings: Array<String>,
callbackName: String
) {
handle!!.requestPermissions(invoke, permissionStrings, callbackName)
}
/**
* Request all of the specified permissions in the TauriPlugin annotation (if any)
*
* If there is no registered permission callback for the Invoke passed in, the call will
* be rejected. Make sure a valid permission callback method is registered using the
* [PermissionCallback] annotation.
*
* @param invoke
* @param callbackName the name of the callback to run when the permission request is complete
*/
protected fun requestAllPermissions(
invoke: Invoke,
callbackName: String
) {
val annotation = handle!!.annotation
if (annotation != null) {
val perms: HashSet<String> = HashSet()
for (perm in annotation.permissions) {
perms.addAll(perm.strings)
}
permissionActivityResult(invoke, perms.toArray(arrayOfNulls<String>(0)), callbackName)
}
}
/**
* Request permissions using an alias defined on the plugin.
*
* If there is no registered permission callback for the Invoke passed in, the call will
* be rejected. Make sure a valid permission callback method is registered using the
* [PermissionCallback] annotation.
*
* @param alias an alias defined on the plugin
* @param invoke the invoke involved in originating the request
* @param callbackName the name of the callback to run when the permission request is complete
*/
protected fun requestPermissionForAlias(
alias: String,
invoke: Invoke,
callbackName: String
) {
requestPermissionForAliases(arrayOf(alias), invoke, callbackName)
}
/**
* Request permissions using aliases defined on the plugin.
*
* If there is no registered permission callback for the Invoke passed in, the call will
* be rejected. Make sure a valid permission callback method is registered using the
* [PermissionCallback] annotation.
*
* @param aliases a set of aliases defined on the plugin
* @param invoke the invoke involved in originating the request
* @param callbackName the name of the callback to run when the permission request is complete
*/
fun requestPermissionForAliases(
aliases: Array<String>,
invoke: Invoke,
callbackName: String
) {
if (aliases.isEmpty()) {
Logger.error("No permission alias was provided")
return
}
val permissions = getPermissionStringsForAliases(aliases)
if (permissions.isNotEmpty()) {
permissionActivityResult(invoke, permissions, callbackName)
}
}
/**
* Gets the Android permission strings defined on the [TauriPlugin] annotation with
* the provided aliases.
*
* @param aliases aliases for permissions defined on the plugin
* @return Android permission strings associated with the provided aliases, if exists
*/
private fun getPermissionStringsForAliases(aliases: Array<String>): Array<String> {
val perms: HashSet<String> = HashSet()
for (perm in handle?.annotation?.permissions.orEmpty()) {
if (aliases.contains(perm.alias)) {
perms.addAll(perm.strings)
}
}
return perms.toArray(arrayOfNulls(0))
}
/**
* Get the permission state for the provided permission alias.
*
* @param alias the permission alias to get
* @return the state of the provided permission alias or null
*/
fun getPermissionState(alias: String): PermissionState? {
return getPermissionStates()[alias]
}
/**
* Helper to check all permissions defined on a plugin and see the state of each.
*
* @return A mapping of permission aliases to the associated granted status.
*/
open fun getPermissionStates(): Map<String, PermissionState> {
val permissionsResults: MutableMap<String, PermissionState> = HashMap()
for (perm in handle?.annotation?.permissions.orEmpty()) {
// If a permission is defined with no permission constants, return GRANTED for it.
// Otherwise, get its true state.
if (perm.strings.isEmpty() || perm.strings.size == 1 && perm.strings[0].isEmpty()) {
val key = perm.alias
if (key.isNotEmpty()) {
val existingResult = permissionsResults[key]
// auto set permission state to GRANTED if the alias is empty.
if (existingResult == null) {
permissionsResults[key] = PermissionState.GRANTED
}
}
} else {
for (permString in perm.strings) {
val key = perm.alias.ifEmpty { permString }
var permissionStatus: PermissionState
if (ActivityCompat.checkSelfPermission(
activity,
permString
) == PackageManager.PERMISSION_GRANTED
) {
permissionStatus = PermissionState.GRANTED
} else {
permissionStatus = PermissionState.PROMPT
// Check if there is a cached permission state for the "Never ask again" state
val prefs =
activity.getSharedPreferences("PluginPermStates", Activity.MODE_PRIVATE)
val state = prefs.getString(permString, null)
if (state != null) {
permissionStatus = PermissionState.byState(state)
}
}
val existingResult = permissionsResults[key]
// multiple permissions with the same alias must all be true, otherwise all false.
if (existingResult == null || existingResult === PermissionState.GRANTED) {
permissionsResults[key] = permissionStatus
}
}
}
}
return permissionsResults
}
}

View File

@@ -0,0 +1,178 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package app.tauri.plugin
import android.app.Activity
import android.content.Intent
import android.webkit.WebView
import androidx.activity.result.IntentSenderRequest
import androidx.core.app.ActivityCompat
import app.tauri.PermissionHelper
import app.tauri.PermissionState
import app.tauri.annotation.ActivityCallback
import app.tauri.annotation.Command
import app.tauri.annotation.PermissionCallback
import app.tauri.annotation.TauriPlugin
import com.fasterxml.jackson.databind.ObjectMapper
import java.lang.reflect.Method
import androidx.core.content.edit
class PluginHandle(
private val manager: PluginManager,
val name: String,
val instance: Plugin,
val config: String,
val jsonMapper: ObjectMapper
) {
private val commands: HashMap<String, CommandData> = HashMap()
private val permissionCallbackMethods: HashMap<String, Method> = HashMap()
private val startActivityCallbackMethods: HashMap<String, Method> = HashMap()
var annotation: TauriPlugin?
var loaded = false
init {
indexMethods()
instance.handle = this
annotation = instance.javaClass.getAnnotation(TauriPlugin::class.java)
}
fun load(webView: WebView) {
instance.load(webView)
loaded = true
}
fun startActivityForResult(invoke: Invoke, intent: Intent, callbackName: String) {
manager.startActivityForResult(intent) { result ->
val method = startActivityCallbackMethods[callbackName]
if (method != null) {
method.isAccessible = true
method(instance, invoke, result)
}
}
}
fun startIntentSenderForResult(
invoke: Invoke,
intentSender: IntentSenderRequest,
callbackName: String
) {
manager.startIntentSenderForResult(intentSender) { result ->
val method = startActivityCallbackMethods[callbackName]
if (method != null) {
method.isAccessible = true
method(instance, invoke, result)
}
}
}
fun requestPermissions(
invoke: Invoke,
permissions: Array<String>,
callbackName: String
) {
manager.requestPermissions(permissions) { result ->
if (validatePermissions(invoke, result)) {
val method = permissionCallbackMethods[callbackName]
if (method != null) {
method.isAccessible = true
method(instance, invoke)
}
}
}
}
/**
* Saves permission states and rejects if permissions were not correctly defined in
* the AndroidManifest.xml file.
*
* @param permissions
* @return true if permissions were saved and defined correctly, false if not
*/
private fun validatePermissions(
invoke: Invoke,
permissions: Map<String, Boolean>
): Boolean {
val activity = manager.activity!!
val prefs =
activity.getSharedPreferences("PluginPermStates", Activity.MODE_PRIVATE)
for ((permString, isGranted) in permissions) {
if (isGranted) {
// Permission granted. If previously denied, remove cached state
val state = prefs.getString(permString, null)
if (state != null) {
prefs.edit {
remove(permString)
}
}
} else {
prefs.edit {
if (ActivityCompat.shouldShowRequestPermissionRationale(
activity,
permString
)
) {
// Permission denied, can prompt again with rationale
putString(permString, PermissionState.PROMPT_WITH_RATIONALE.toString())
} else {
// Permission denied permanently, store this state for future reference
putString(permString, PermissionState.DENIED.toString())
}
}
}
}
val permStrings = permissions.keys.toTypedArray()
if (!PermissionHelper.hasDefinedPermissions(activity, permStrings)) {
val builder = StringBuilder()
builder.append("Missing the following permissions in AndroidManifest.xml:\n")
val missing = PermissionHelper.getUndefinedPermissions(activity, permStrings)
for (perm in missing) {
builder.append(
"""
$perm
""".trimIndent()
)
}
invoke.reject(builder.toString())
return false
}
return true
}
@Throws(
InvalidCommandException::class,
IllegalAccessException::class
)
fun invoke(invoke: Invoke) {
val methodMeta = commands[invoke.command]
?: throw InvalidCommandException("No command " + invoke.command + " found for plugin " + instance.javaClass.name)
methodMeta.method.invoke(instance, invoke)
}
private fun indexMethods() {
val methods = mutableListOf<Method>()
var pluginCursor: Class<*> = instance.javaClass
while (pluginCursor.name != Any::class.java.name) {
methods.addAll(listOf(*pluginCursor.declaredMethods))
pluginCursor = pluginCursor.superclass
}
for (method in methods) {
if (method.isAnnotationPresent(Command::class.java)) {
val command = method.getAnnotation(Command::class.java) ?: continue
val methodMeta = CommandData(method, command)
commands[method.name] = methodMeta
}
if (method.isAnnotationPresent(ActivityCallback::class.java)) {
startActivityCallbackMethods[method.name] = method
}
if (method.isAnnotationPresent(PermissionCallback::class.java)) {
permissionCallbackMethods[method.name] = method
}
}
}
}

View File

@@ -0,0 +1,245 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package app.tauri.plugin
import android.content.res.Configuration
import android.content.Context
import android.content.Intent
import android.webkit.WebView
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.IntentSenderRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import app.tauri.annotation.InvokeArg
import app.tauri.FsUtils
import app.tauri.JniMethod
import app.tauri.Logger
import com.fasterxml.jackson.annotation.JsonAutoDetect
import com.fasterxml.jackson.annotation.PropertyAccessor
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.datatype.jsonorg.JsonOrgModule
import com.fasterxml.jackson.databind.module.SimpleModule
import java.lang.reflect.InvocationTargetException
object PluginManager {
fun interface RequestPermissionsCallback {
fun onResult(permissions: Map<String, Boolean>)
}
fun interface ActivityResultCallback {
fun onResult(result: ActivityResult)
}
/** The result launchers belonging to one activity. */
private class ResultLaunchers(
val startActivityForResult: ActivityResultLauncher<Intent>,
val startIntentSenderForResult: ActivityResultLauncher<IntentSenderRequest>,
val requestPermissions: ActivityResultLauncher<Array<String>>
)
// Insertion ordered, so the activity taken over when the current one goes away is the oldest
// surviving one rather than an arbitrary member of a hash set.
private val launchers: LinkedHashMap<AppCompatActivity, ResultLaunchers> = LinkedHashMap()
var activity: AppCompatActivity? = null
private val plugins: HashMap<String, PluginHandle> = HashMap()
private var requestPermissionsCallback: RequestPermissionsCallback? = null
private var startActivityForResultCallback: ActivityResultCallback? = null
private var startIntentSenderForResultCallback: ActivityResultCallback? = null
private var jsonMapper: ObjectMapper = ObjectMapper()
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.enable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES)
.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY)
init {
val channelDeserializer = ChannelDeserializer({ channelId, payload ->
sendChannelData(channelId, payload)
}, jsonMapper)
jsonMapper
.registerModule(SimpleModule().addDeserializer(Channel::class.java, channelDeserializer))
// Without it, the mapper's field visibility serializes an org.json value through
// its private fields: a `JSArray` as `{"values":[...]}` and a `JSObject` as
// `{"nameValuePairs":{...}}`.
.registerModule(JsonOrgModule())
}
fun onCreate(activity: AppCompatActivity) {
// Every activity gets its own launchers, and gets them here: registerForActivityResult must
// be called before its owner reaches STARTED, so an activity that is already running can
// never be given launchers later.
launchers[activity] = registerResultLaunchers(activity)
if (this.activity == null) {
this.activity = activity
}
}
private fun registerResultLaunchers(activity: AppCompatActivity): ResultLaunchers =
ResultLaunchers(
activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()
) { result ->
startActivityForResultCallback?.onResult(result)
},
activity.registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()
) { result ->
startIntentSenderForResultCallback?.onResult(result)
},
activity.registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()
) { result ->
requestPermissionsCallback?.onResult(result)
}
)
private val currentLaunchers: ResultLaunchers
get() = launchers[activity]
?: throw IllegalStateException("the plugin manager has no activity to launch from")
fun onNewIntent(intent: Intent) {
for (plugin in plugins.values) {
plugin.instance.onNewIntent(intent)
}
}
fun onPause(activity: AppCompatActivity) {
for (plugin in plugins.values) {
plugin.instance.triggerOnPause(activity)
}
}
fun onResume(activity: AppCompatActivity) {
for (plugin in plugins.values) {
plugin.instance.triggerOnResume(activity)
}
}
fun onRestart(activity: AppCompatActivity) {
for (plugin in plugins.values) {
plugin.instance.triggerOnRestart(activity)
}
}
fun onStop(activity: AppCompatActivity) {
for (plugin in plugins.values) {
plugin.instance.triggerOnStop(activity)
}
}
fun onDestroy(activity: AppCompatActivity) {
for (plugin in plugins.values) {
plugin.instance.triggerOnDestroy(activity)
}
launchers.remove(activity)
if (this.activity == activity) {
// Whatever is left already holds its own launchers, registered when it was created. Moving
// this activity's launchers over instead would mean registering against an activity that is
// already running, which registerForActivityResult rejects with an IllegalStateException.
this.activity = launchers.keys.firstOrNull()
}
}
fun onConfigurationChanged(newConfig: Configuration) {
for (plugin in plugins.values) {
plugin.instance.onConfigurationChanged(newConfig)
}
}
fun startActivityForResult(intent: Intent, callback: ActivityResultCallback) {
startActivityForResultCallback = callback
currentLaunchers.startActivityForResult.launch(intent)
}
fun startIntentSenderForResult(intent: IntentSenderRequest, callback: ActivityResultCallback) {
startIntentSenderForResultCallback = callback
currentLaunchers.startIntentSenderForResult.launch(intent)
}
fun requestPermissions(
permissionStrings: Array<String>,
callback: RequestPermissionsCallback
) {
requestPermissionsCallback = callback
currentLaunchers.requestPermissions.launch(permissionStrings)
}
@JniMethod
fun onWebViewCreated(webView: WebView) {
for ((_, plugin) in plugins) {
if (!plugin.loaded) {
plugin.load(webView)
}
}
}
@JniMethod
fun load(webView: WebView?, name: String, plugin: Plugin, config: String) {
val handle = PluginHandle(this, name, plugin, config, jsonMapper)
plugins[name] = handle
if (webView != null) {
plugin.load(webView)
}
}
@JniMethod
fun runCommand(id: Int, pluginId: String, command: String, data: String) {
val successId = 0L
val errorId = 1L
val invoke = Invoke(id.toLong(), command, successId, errorId, { fn, result ->
var success: String? = null
var error: String? = null
if (fn == successId) {
success = result
} else {
error = result
}
handlePluginResponse(id, success, error)
}, data, jsonMapper)
dispatchPluginMessage(invoke, pluginId)
}
private fun dispatchPluginMessage(invoke: Invoke, pluginId: String) {
Logger.verbose(
Logger.tags("Plugin"),
"Tauri plugin: pluginId: $pluginId, command: ${invoke.command}"
)
try {
val plugin = plugins[pluginId]
if (plugin == null) {
invoke.reject("Plugin $pluginId not initialized")
} else {
plugins[pluginId]?.invoke(invoke)
}
} catch (e: Exception) {
var exception: Throwable = e
if (exception.message?.isEmpty() != false) {
if (e is InvocationTargetException) {
exception = e.targetException
}
}
invoke.reject(if (exception.message?.isEmpty() != false) { exception.toString() } else { exception.message })
}
}
fun<T> loadConfig(context: Context, plugin: String, cls: Class<T>): T {
val tauriConfigJson = FsUtils.readAsset(context.assets, "tauri.conf.json")
val mapper = ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
val config = mapper.readValue(tauriConfigJson, Config::class.java)
return mapper.readValue(config.plugins[plugin].toString(), cls)
}
private external fun handlePluginResponse(id: Int, success: String?, error: String?)
private external fun sendChannelData(id: Long, data: String)
}
@InvokeArg
internal class Config {
lateinit var plugins: Map<String, JsonNode>
}

View File

@@ -0,0 +1,16 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package app.tauri.plugin
import app.tauri.annotation.Command
import java.lang.reflect.Method
class CommandData(
val method: Method, methodDecorator: Command
) {
// The name of the method
val name: String = method.name
}

View File

@@ -0,0 +1,67 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package app.tauri.plugin
import android.annotation.SuppressLint
import app.tauri.Logger
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
class PluginResult @JvmOverloads constructor(json: JSObject? = JSObject()) {
private val json: JSObject
init {
this.json = json ?: JSObject()
}
fun put(name: String, value: Boolean): PluginResult {
return jsonPut(name, value)
}
fun put(name: String, value: Double): PluginResult {
return jsonPut(name, value)
}
fun put(name: String, value: Int): PluginResult {
return jsonPut(name, value)
}
fun put(name: String, value: Long): PluginResult {
return jsonPut(name, value)
}
/**
* Format a date as an ISO string
*/
@SuppressLint("SimpleDateFormat")
fun put(name: String, value: Date): PluginResult {
val tz: TimeZone = TimeZone.getTimeZone("UTC")
val df: DateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'")
df.timeZone = tz
return jsonPut(name, df.format(value))
}
fun put(name: String, value: Any?): PluginResult {
return jsonPut(name, value)
}
fun put(name: String, value: PluginResult): PluginResult {
return jsonPut(name, value.json)
}
private fun jsonPut(name: String, value: Any?): PluginResult {
try {
json.put(name, value)
} catch (ex: Exception) {
Logger.error(Logger.tags("Plugin"), "", ex)
}
return this
}
override fun toString(): String {
return json.toString()
}
}

View File

@@ -0,0 +1,21 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package app.tauri
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

10
crates/tauri/mobile/ios-api/.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
DerivedData/
.swiftpm/config/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
Package.resolved

View File

@@ -0,0 +1,40 @@
// swift-tools-version:5.3
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
import PackageDescription
let package = Package(
name: "Tauri",
platforms: [
.macOS(.v10_13),
.iOS(.v11),
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "Tauri",
type: .static,
targets: ["Tauri"])
],
dependencies: [
// Dependencies declare other packages that this package depends on.
.package(name: "SwiftRs", url: "https://github.com/Brendonovich/swift-rs", from: "1.0.0")
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "Tauri",
dependencies: [
.byName(name: "SwiftRs")
],
path: "Sources"
),
.testTarget(
name: "TauriTests",
dependencies: ["Tauri"]
),
]
)

View File

@@ -0,0 +1,3 @@
# Tauri
Tauri iOS API.

View File

@@ -0,0 +1,65 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
import Foundation
let CHANNEL_PREFIX = "__CHANNEL__:"
let channelDataKey = CodingUserInfoKey(rawValue: "sendChannelData")!
public class Channel: Decodable {
public let id: UInt64
let handler: (UInt64, String) -> Void
public required init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let channelDef = try container.decode(String.self)
let components = channelDef.components(separatedBy: CHANNEL_PREFIX)
if components.count < 2 {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "Invalid channel definition from \(channelDef)"
)
}
guard let channelId = UInt64(components[1]) else {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "Invalid channel ID from \(channelDef)"
)
}
guard let handler = decoder.userInfo[channelDataKey] as? (UInt64, String) -> Void else {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "missing userInfo for Channel handler. This is a Tauri issue"
)
}
self.id = channelId
self.handler = handler
}
func serialize(_ data: JsonValue) -> String {
do {
return try data.jsonRepresentation() ?? "\"Failed to serialize payload\""
} catch {
return "\"\(error)\""
}
}
public func send(_ data: JsonObject) {
send(.dictionary(data))
}
public func send(_ data: JsonValue) {
handler(id, serialize(data))
}
public func send<T: Encodable>(_ data: T) throws {
let json = try JSONEncoder().encode(data)
handler(id, String(decoding: json, as: UTF8.self))
}
}

View File

@@ -0,0 +1,118 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
import Foundation
import UIKit
@objc public class Invoke: NSObject {
public let command: String
let callback: UInt64
let error: UInt64
let data: String
let sendResponse: (UInt64, String?) -> Void
let sendChannelData: (UInt64, String) -> Void
public init(
command: String, callback: UInt64, error: UInt64,
sendResponse: @escaping (UInt64, String?) -> Void,
sendChannelData: @escaping (UInt64, String) -> Void, data: String
) {
self.command = command
self.callback = callback
self.error = error
self.data = data
self.sendResponse = sendResponse
self.sendChannelData = sendChannelData
}
public func getRawArgs() -> String {
return self.data
}
public func getArgs() throws -> JSObject {
let jsonData = self.data.data(using: .utf8)!
let data = try JSONSerialization.jsonObject(with: jsonData, options: [])
return JSTypes.coerceDictionaryToJSObject(
(data as! NSDictionary), formattingDatesAsStrings: true)!
}
public func parseArgs<T: Decodable>(_ type: T.Type) throws -> T {
let jsonData = self.data.data(using: .utf8)!
let decoder = JSONDecoder()
decoder.userInfo[channelDataKey] = sendChannelData
return try decoder.decode(type, from: jsonData)
}
func serialize(_ data: JsonValue) -> String {
do {
return try data.jsonRepresentation() ?? "\"Failed to serialize payload\""
} catch {
return "\"\(error)\""
}
}
public func resolve() {
sendResponse(callback, nil)
}
public func resolve(_ data: JsonObject) {
resolve(.dictionary(data))
}
public func resolve(_ data: JsonValue) {
sendResponse(callback, serialize(data))
}
public func resolve<T: Encodable>(_ data: T) {
do {
let json = try JSONEncoder().encode(data)
sendResponse(callback, String(decoding: json, as: UTF8.self))
} catch {
sendResponse(self.error, "\"\(error)\"")
}
}
public func reject(
_ message: String, code: String? = nil, error: Error? = nil, data: JsonValue? = nil
) {
let payload: NSMutableDictionary = [
"message": message
]
if let code = code {
payload["code"] = code
}
if let error = error {
payload["error"] = error
}
if let data = data {
switch data {
case .dictionary(let dict):
for entry in dict {
payload[entry.key] = entry.value
}
}
}
sendResponse(self.error, serialize(.dictionary(payload as! JsonObject)))
}
public func unimplemented() {
unimplemented("not implemented")
}
public func unimplemented(_ message: String) {
reject(message)
}
public func unavailable() {
unavailable("not available")
}
public func unavailable(_ message: String) {
reject(message)
}
}

View File

@@ -0,0 +1,123 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
import Foundation
// declare our empty protocol, and conformance, for typing
public protocol JSValue {}
extension String: JSValue {}
extension Bool: JSValue {}
extension Int: JSValue {}
extension Float: JSValue {}
extension Double: JSValue {}
extension NSNumber: JSValue {}
extension NSNull: JSValue {}
extension Array: JSValue {}
extension Date: JSValue {}
extension Dictionary: JSValue where Key == String, Value == JSValue {}
// convenience aliases
public typealias JSObject = [String: JSValue]
public typealias JSArray = [JSValue]
extension Dictionary where Key == String, Value == JSValue {
public func getValue(_ key: String) -> JSValue? {
return self[key]
}
public func getString(_ key: String) -> String? {
return self[key] as? String
}
public func getBool(_ key: String) -> Bool? {
return self[key] as? Bool
}
public func getInt(_ key: String) -> Int? {
return self[key] as? Int
}
public func getFloat(_ key: String) -> Float? {
if let floatValue = self[key] as? Float {
return floatValue
} else if let doubleValue = self[key] as? Double {
return Float(doubleValue)
}
return nil
}
public func getDouble(_ key: String) -> Double? {
return self[key] as? Double
}
public func getArray(_ key: String) -> JSArray? {
return self[key] as? JSArray
}
public func getObject(_ key: String) -> JSObject? {
return self[key] as? JSObject
}
}
/*
Simply casting objects from foundation class clusters (such as __NSArrayM)
doesn't work with the JSValue protocol and will always fail. So we need to
recursively and explicitly convert each value in the dictionary.
*/
public enum JSTypes {}
extension JSTypes {
public static func coerceDictionaryToJSObject(
_ dictionary: NSDictionary?, formattingDatesAsStrings: Bool = false
) -> JSObject? {
return coerceToJSValue(dictionary, formattingDates: formattingDatesAsStrings) as? JSObject
}
public static func coerceDictionaryToJSObject(
_ dictionary: [AnyHashable: Any]?, formattingDatesAsStrings: Bool = false
) -> JSObject? {
return coerceToJSValue(dictionary, formattingDates: formattingDatesAsStrings) as? JSObject
}
}
private let dateStringFormatter = ISO8601DateFormatter()
// We need a large switch statement because we have a lot of types.
// swiftlint:disable:next cyclomatic_complexity
private func coerceToJSValue(_ value: Any?, formattingDates: Bool) -> JSValue? {
guard let value = value else {
return nil
}
switch value {
case let stringValue as String:
return stringValue
case let numberValue as NSNumber:
return numberValue
case let boolValue as Bool:
return boolValue
case let intValue as Int:
return intValue
case let floatValue as Float:
return floatValue
case let doubleValue as Double:
return doubleValue
case let dateValue as Date:
if formattingDates {
return dateStringFormatter.string(from: dateValue)
}
return dateValue
case let nullValue as NSNull:
return nullValue
case let arrayValue as NSArray:
return arrayValue.compactMap { coerceToJSValue($0, formattingDates: formattingDates) }
case let dictionaryValue as NSDictionary:
let keys = dictionaryValue.allKeys.compactMap { $0 as? String }
var result: JSObject = [:]
for key in keys {
result[key] = coerceToJSValue(dictionaryValue[key], formattingDates: formattingDates)
}
return result
default:
return nil
}
}

View File

@@ -0,0 +1,58 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
import Foundation
public typealias JsonObject = [String: Any?]
public enum JsonValue {
case dictionary(JsonObject)
enum SerializationError: Error {
case invalidObject
}
public func jsonRepresentation(includingFields: JsonObject? = nil) throws -> String? {
switch self {
case .dictionary(var dictionary):
if let fields = includingFields {
dictionary.merge(fields) { (current, _) in current }
}
dictionary = prepare(dictionary: dictionary)
guard JSONSerialization.isValidJSONObject(dictionary) else {
throw SerializationError.invalidObject
}
let data = try JSONSerialization.data(withJSONObject: dictionary, options: [])
return String(data: data, encoding: .utf8)
}
}
private static let formatter = ISO8601DateFormatter()
private func prepare(dictionary: JsonObject) -> JsonObject {
return dictionary.mapValues { (value) -> Any in
if let date = value as? Date {
return JsonValue.formatter.string(from: date)
} else if let aDictionary = value as? JsonObject {
return prepare(dictionary: aDictionary)
} else if let anArray = value as? [Any] {
return prepare(array: anArray)
}
return value
}
}
private func prepare(array: [Any]) -> [Any] {
return array.map { (value) -> Any in
if let date = value as? Date {
return JsonValue.formatter.string(from: date)
} else if let aDictionary = value as? JsonObject {
return prepare(dictionary: aDictionary)
} else if let anArray = value as? [Any] {
return prepare(array: anArray)
}
return value
}
}
}

View File

@@ -0,0 +1,139 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
import os.log
import UIKit
import Foundation
class StdoutRedirector {
private var originalStdout: Int32 = -1
private var originalStderr: Int32 = -1
private var stdoutPipe: [Int32] = [-1, -1]
private var stderrPipe: [Int32] = [-1, -1]
private var stdoutReadSource: DispatchSourceRead?
private var stderrReadSource: DispatchSourceRead?
func start() {
originalStdout = dup(STDOUT_FILENO)
originalStderr = dup(STDERR_FILENO)
guard Darwin.pipe(&stdoutPipe) == 0,
Darwin.pipe(&stderrPipe) == 0 else {
Logger.error("Failed to create stdout/stderr pipes")
return
}
dup2(stdoutPipe[1], STDOUT_FILENO)
dup2(stderrPipe[1], STDERR_FILENO)
close(stdoutPipe[1])
close(stderrPipe[1])
stdoutReadSource = createReader(
readPipe: stdoutPipe[0],
writeToOriginal: originalStdout,
label: "stdout"
)
stderrReadSource = createReader(
readPipe: stderrPipe[0],
writeToOriginal: originalStderr,
label: "stderr"
)
}
private func createReader(
readPipe: Int32,
writeToOriginal: Int32,
label: String
) -> DispatchSourceRead {
let source = DispatchSource.makeReadSource(
fileDescriptor: readPipe,
queue: .global(qos: .utility)
)
source.setEventHandler {
let bufferSize = 4096
var buffer = [UInt8](repeating: 0, count: bufferSize)
let bytesRead = read(readPipe, &buffer, bufferSize)
if bytesRead > 0 {
let output = String(
bytes: buffer[0..<bytesRead],
encoding: .utf8
) ?? ""
let trimmed = output.trimmingCharacters(in: .newlines)
if !trimmed.isEmpty {
// we're sending stderr to oslog, so we need to avoid recursive calls
if trimmed.hasPrefix("OSLOG-") {
// make sure the system can parse the oslogs
write(writeToOriginal, &buffer, bytesRead)
} else {
Logger.info("[\(label)] \(trimmed)")
}
}
}
}
source.setCancelHandler {
close(readPipe)
}
source.resume()
return source
}
}
/// Wrapper class for os_log function
public class Logger {
private static var _enabled = false
public static var enabled: Bool {
get {
#if DEBUG
return true
#else
return _enabled
#endif
}
set {
Logger._enabled = newValue
}
}
static func log(_ items: [Any], category: String, type: OSLogType) {
if Logger.enabled {
var message = ""
let last = items.count - 1
for (index, item) in items.enumerated() {
message += "\(item)"
if index != last {
message += " "
}
}
let log = OSLog(subsystem: Bundle.main.bundleIdentifier ?? "-", category: category)
os_log("%{public}@", log: log, type: type, String(message.prefix(4068)))
}
}
public static func debug(_ items: Any..., category: String = "app") {
#if DEBUG
Logger.log(items, category: category, type: OSLogType.default)
#else
Logger.log(items, category: category, type: OSLogType.debug)
#endif
}
public static func info(_ items: Any..., category: String = "app") {
#if DEBUG
Logger.log(items, category: category, type: OSLogType.default)
#else
Logger.log(items, category: category, type: OSLogType.info)
#endif
}
public static func error(_ items: Any..., category: String = "app") {
Logger.log(items, category: category, type: OSLogType.error)
}
}

View File

@@ -0,0 +1,82 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
import WebKit
import os.log
struct RegisterListenerArgs: Decodable {
let event: String
let handler: Channel
}
struct RemoveListenerArgs: Decodable {
let event: String
let channelId: UInt64
}
open class Plugin: NSObject {
public let manager: PluginManager = PluginManager.shared
var config: String = "{}"
private var listeners = [String: [Channel]]()
internal func setConfig(_ config: String) {
self.config = config
}
public func parseConfig<T: Decodable>(_ type: T.Type) throws -> T {
let jsonData = self.config.data(using: .utf8)!
let decoder = JSONDecoder()
return try decoder.decode(type, from: jsonData)
}
@objc open func load(webview: WKWebView) {}
@objc open func checkPermissions(_ invoke: Invoke) {
invoke.resolve()
}
@objc open func requestPermissions(_ invoke: Invoke) {
invoke.resolve()
}
public func trigger(_ event: String, data: JSObject) {
if let eventListeners = listeners[event] {
for channel in eventListeners {
channel.send(data)
}
}
}
public func trigger<T: Encodable>(_ event: String, data: T) throws {
if let eventListeners = listeners[event] {
for channel in eventListeners {
try channel.send(data)
}
}
}
@objc func registerListener(_ invoke: Invoke) throws {
let args = try invoke.parseArgs(RegisterListenerArgs.self)
if var eventListeners = listeners[args.event] {
eventListeners.append(args.handler)
listeners[args.event] = eventListeners
} else {
listeners[args.event] = [args.handler]
}
invoke.resolve()
}
@objc func removeListener(_ invoke: Invoke) throws {
let args = try invoke.parseArgs(RemoveListenerArgs.self)
if let eventListeners = listeners[args.event] {
listeners[args.event] = eventListeners.filter { $0.id != args.channelId }
}
invoke.resolve()
}
}

View File

@@ -0,0 +1,157 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
import Foundation
import SwiftRs
import UIKit
import WebKit
import os.log
class PluginHandle {
var instance: Plugin
var loaded = false
init(plugin: Plugin) {
instance = plugin
}
}
public class PluginManager {
static let shared: PluginManager = PluginManager()
public var viewController: UIViewController?
var plugins: [String: PluginHandle] = [:]
var ipcDispatchQueue = DispatchQueue(label: "ipc")
public var isSimEnvironment: Bool {
#if targetEnvironment(simulator)
return true
#else
return false
#endif
}
public func assetUrl(fromLocalURL url: URL?) -> URL? {
guard let inputURL = url else {
return nil
}
return URL(string: "asset://localhost")!.appendingPathComponent(inputURL.path)
}
func onWebviewCreated(_ webview: WKWebView) {
for (_, handle) in plugins {
if !handle.loaded {
handle.instance.load(webview: webview)
}
}
}
func load<P: Plugin>(name: String, plugin: P, config: String, webview: WKWebView?) {
plugin.setConfig(config)
let handle = PluginHandle(plugin: plugin)
if let webview = webview {
handle.instance.load(webview: webview)
handle.loaded = true
}
plugins[name] = handle
}
func invoke(name: String, invoke: Invoke) {
if let plugin = plugins[name] {
ipcDispatchQueue.async {
let selectorWithCompletionHandler = Selector(("\(invoke.command):completionHandler:"))
let selectorWithThrows = Selector(("\(invoke.command):error:"))
if plugin.instance.responds(to: selectorWithCompletionHandler) {
let completion: @convention(block) (NSError?) -> Void = { error in
if let error = error {
invoke.reject("\(error)")
}
}
let blockObj: AnyObject = unsafeBitCast(completion, to: AnyObject.self)
let imp = plugin.instance.method(for: selectorWithCompletionHandler)
typealias Fn = @convention(c) (AnyObject, Selector, Invoke, AnyObject) -> Void
let fn = unsafeBitCast(imp, to: Fn.self)
fn(plugin.instance, selectorWithCompletionHandler, invoke, blockObj)
} else if plugin.instance.responds(to: selectorWithThrows) {
var error: NSError? = nil
withUnsafeMutablePointer(to: &error) {
let methodIMP: IMP! = plugin.instance.method(for: selectorWithThrows)
unsafeBitCast(
methodIMP, to: (@convention(c) (Any?, Selector, Invoke, OpaquePointer) -> Void).self)(
plugin.instance, selectorWithThrows, invoke, OpaquePointer($0))
}
if let error = error {
invoke.reject("\(error)")
// TODO: app crashes without this leak
let _ = Unmanaged.passRetained(error)
}
} else {
let selector = Selector(("\(invoke.command):"))
if plugin.instance.responds(to: selector) {
plugin.instance.perform(selector, with: invoke)
} else {
invoke.reject("No command \(invoke.command) found for plugin \(name)")
}
}
}
} else {
invoke.reject("Plugin \(name) not initialized")
}
}
}
extension PluginManager: NSCopying {
public func copy(with zone: NSZone? = nil) -> Any {
return self
}
}
private var stdoutRedirector: StdoutRedirector?
@_cdecl("log_stdout")
func logStdout() {
stdoutRedirector = StdoutRedirector()
stdoutRedirector!.start()
}
@_cdecl("register_plugin")
func registerPlugin(name: SRString, plugin: NSObject, config: SRString, webview: WKWebView?) {
PluginManager.shared.load(
name: name.toString(),
plugin: plugin as! Plugin,
config: config.toString(),
webview: webview
)
}
@_cdecl("on_webview_created")
func onWebviewCreated(webview: WKWebView, viewController: UIViewController) {
PluginManager.shared.viewController = viewController
PluginManager.shared.onWebviewCreated(webview)
}
@_cdecl("run_plugin_command")
func runCommand(
id: Int,
name: SRString,
command: SRString,
data: SRString,
callback: @escaping @convention(c) (Int, Bool, UnsafePointer<CChar>) -> Void,
sendChannelData: @escaping @convention(c) (UInt64, UnsafePointer<CChar>) -> Void
) {
let callbackId: UInt64 = 0
let errorId: UInt64 = 1
let invoke = Invoke(
command: command.toString(), callback: callbackId, error: errorId,
sendResponse: { (fn: UInt64, payload: String?) -> Void in
let success = fn == callbackId
callback(id, success, payload ?? "null")
},
sendChannelData: { (id: UInt64, payload: String) -> Void in
sendChannelData(id, payload)
}, data: data.toString())
PluginManager.shared.invoke(name: name.toString(), invoke: invoke)
}

View File

@@ -0,0 +1,15 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
import UIKit
public class UIUtils {
public static func centerPopover(rootViewController: UIViewController?, popoverController: UIViewController) {
if let viewController = rootViewController {
popoverController.popoverPresentationController?.sourceRect = CGRect(x: viewController.view.center.x, y: viewController.view.center.y, width: 0, height: 0)
popoverController.popoverPresentationController?.sourceView = viewController.view
popoverController.popoverPresentationController?.permittedArrowDirections = UIPopoverArrowDirection.up
}
}
}

View File

@@ -0,0 +1,15 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
import XCTest
@testable import Tauri
final class TauriTests: XCTestCase {
func testExample() throws {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct
// results.
XCTAssertEqual(Tauri().text, "Hello, World!")
}
}

View File

@@ -0,0 +1,5 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
-keep class $PACKAGE.TauriActivity {
public app.tauri.plugin.PluginManager getPluginManager();
}

View File

@@ -0,0 +1,440 @@
## Default Permission
Default permissions for the plugin.
#### This default permission set includes the following:
- `allow-version`
- `allow-name`
- `allow-tauri-version`
- `allow-identifier`
- `allow-bundle-type`
- `allow-register-listener`
- `allow-remove-listener`
- `allow-supports-multiple-windows`
## Permission Table
<table>
<tr>
<th>Identifier</th>
<th>Description</th>
</tr>
<tr>
<td>
`core:app:allow-app-hide`
</td>
<td>
Enables the app_hide command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:app:deny-app-hide`
</td>
<td>
Denies the app_hide command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:app:allow-app-show`
</td>
<td>
Enables the app_show command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:app:deny-app-show`
</td>
<td>
Denies the app_show command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:app:allow-bundle-type`
</td>
<td>
Enables the bundle_type command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:app:deny-bundle-type`
</td>
<td>
Denies the bundle_type command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:app:allow-default-window-icon`
</td>
<td>
Enables the default_window_icon command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:app:deny-default-window-icon`
</td>
<td>
Denies the default_window_icon command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:app:allow-exit`
</td>
<td>
Enables the exit command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:app:deny-exit`
</td>
<td>
Denies the exit command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:app:allow-fetch-data-store-identifiers`
</td>
<td>
Enables the fetch_data_store_identifiers command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:app:deny-fetch-data-store-identifiers`
</td>
<td>
Denies the fetch_data_store_identifiers command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:app:allow-identifier`
</td>
<td>
Enables the identifier command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:app:deny-identifier`
</td>
<td>
Denies the identifier command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:app:allow-name`
</td>
<td>
Enables the name command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:app:deny-name`
</td>
<td>
Denies the name command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:app:allow-register-listener`
</td>
<td>
Enables the register_listener command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:app:deny-register-listener`
</td>
<td>
Denies the register_listener command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:app:allow-remove-data-store`
</td>
<td>
Enables the remove_data_store command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:app:deny-remove-data-store`
</td>
<td>
Denies the remove_data_store command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:app:allow-remove-listener`
</td>
<td>
Enables the remove_listener command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:app:deny-remove-listener`
</td>
<td>
Denies the remove_listener command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:app:allow-set-app-theme`
</td>
<td>
Enables the set_app_theme command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:app:deny-set-app-theme`
</td>
<td>
Denies the set_app_theme command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:app:allow-set-dock-visibility`
</td>
<td>
Enables the set_dock_visibility command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:app:deny-set-dock-visibility`
</td>
<td>
Denies the set_dock_visibility command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:app:allow-supports-multiple-windows`
</td>
<td>
Enables the supports_multiple_windows command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:app:deny-supports-multiple-windows`
</td>
<td>
Denies the supports_multiple_windows command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:app:allow-tauri-version`
</td>
<td>
Enables the tauri_version command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:app:deny-tauri-version`
</td>
<td>
Denies the tauri_version command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:app:allow-version`
</td>
<td>
Enables the version command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:app:deny-version`
</td>
<td>
Denies the version command without any pre-configured scope.
</td>
</tr>
</table>

View File

@@ -0,0 +1,124 @@
## Default Permission
Default permissions for the plugin, which enables all commands.
#### This default permission set includes the following:
- `allow-listen`
- `allow-unlisten`
- `allow-emit`
- `allow-emit-to`
## Permission Table
<table>
<tr>
<th>Identifier</th>
<th>Description</th>
</tr>
<tr>
<td>
`core:event:allow-emit`
</td>
<td>
Enables the emit command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:event:deny-emit`
</td>
<td>
Denies the emit command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:event:allow-emit-to`
</td>
<td>
Enables the emit_to command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:event:deny-emit-to`
</td>
<td>
Denies the emit_to command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:event:allow-listen`
</td>
<td>
Enables the listen command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:event:deny-listen`
</td>
<td>
Denies the listen command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:event:allow-unlisten`
</td>
<td>
Enables the unlisten command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:event:deny-unlisten`
</td>
<td>
Denies the unlisten command without any pre-configured scope.
</td>
</tr>
</table>

View File

@@ -0,0 +1,151 @@
## Default Permission
Default permissions for the plugin, which enables all commands.
#### This default permission set includes the following:
- `allow-new`
- `allow-from-bytes`
- `allow-from-path`
- `allow-rgba`
- `allow-size`
## Permission Table
<table>
<tr>
<th>Identifier</th>
<th>Description</th>
</tr>
<tr>
<td>
`core:image:allow-from-bytes`
</td>
<td>
Enables the from_bytes command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:image:deny-from-bytes`
</td>
<td>
Denies the from_bytes command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:image:allow-from-path`
</td>
<td>
Enables the from_path command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:image:deny-from-path`
</td>
<td>
Denies the from_path command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:image:allow-new`
</td>
<td>
Enables the new command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:image:deny-new`
</td>
<td>
Denies the new command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:image:allow-rgba`
</td>
<td>
Enables the rgba command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:image:deny-rgba`
</td>
<td>
Denies the rgba command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:image:allow-size`
</td>
<td>
Enables the size command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:image:deny-size`
</td>
<td>
Denies the size command without any pre-configured scope.
</td>
</tr>
</table>

View File

@@ -0,0 +1,610 @@
## Default Permission
Default permissions for the plugin, which enables all commands.
#### This default permission set includes the following:
- `allow-new`
- `allow-append`
- `allow-prepend`
- `allow-insert`
- `allow-remove`
- `allow-remove-at`
- `allow-items`
- `allow-get`
- `allow-popup`
- `allow-create-default`
- `allow-set-as-app-menu`
- `allow-set-as-window-menu`
- `allow-text`
- `allow-set-text`
- `allow-is-enabled`
- `allow-set-enabled`
- `allow-set-accelerator`
- `allow-set-as-windows-menu-for-nsapp`
- `allow-set-as-help-menu-for-nsapp`
- `allow-is-checked`
- `allow-set-checked`
- `allow-set-icon`
## Permission Table
<table>
<tr>
<th>Identifier</th>
<th>Description</th>
</tr>
<tr>
<td>
`core:menu:allow-append`
</td>
<td>
Enables the append command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:menu:deny-append`
</td>
<td>
Denies the append command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:menu:allow-create-default`
</td>
<td>
Enables the create_default command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:menu:deny-create-default`
</td>
<td>
Denies the create_default command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:menu:allow-get`
</td>
<td>
Enables the get command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:menu:deny-get`
</td>
<td>
Denies the get command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:menu:allow-insert`
</td>
<td>
Enables the insert command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:menu:deny-insert`
</td>
<td>
Denies the insert command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:menu:allow-is-checked`
</td>
<td>
Enables the is_checked command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:menu:deny-is-checked`
</td>
<td>
Denies the is_checked command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:menu:allow-is-enabled`
</td>
<td>
Enables the is_enabled command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:menu:deny-is-enabled`
</td>
<td>
Denies the is_enabled command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:menu:allow-items`
</td>
<td>
Enables the items command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:menu:deny-items`
</td>
<td>
Denies the items command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:menu:allow-new`
</td>
<td>
Enables the new command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:menu:deny-new`
</td>
<td>
Denies the new command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:menu:allow-popup`
</td>
<td>
Enables the popup command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:menu:deny-popup`
</td>
<td>
Denies the popup command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:menu:allow-prepend`
</td>
<td>
Enables the prepend command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:menu:deny-prepend`
</td>
<td>
Denies the prepend command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:menu:allow-remove`
</td>
<td>
Enables the remove command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:menu:deny-remove`
</td>
<td>
Denies the remove command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:menu:allow-remove-at`
</td>
<td>
Enables the remove_at command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:menu:deny-remove-at`
</td>
<td>
Denies the remove_at command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:menu:allow-set-accelerator`
</td>
<td>
Enables the set_accelerator command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:menu:deny-set-accelerator`
</td>
<td>
Denies the set_accelerator command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:menu:allow-set-as-app-menu`
</td>
<td>
Enables the set_as_app_menu command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:menu:deny-set-as-app-menu`
</td>
<td>
Denies the set_as_app_menu command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:menu:allow-set-as-help-menu-for-nsapp`
</td>
<td>
Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:menu:deny-set-as-help-menu-for-nsapp`
</td>
<td>
Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:menu:allow-set-as-window-menu`
</td>
<td>
Enables the set_as_window_menu command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:menu:deny-set-as-window-menu`
</td>
<td>
Denies the set_as_window_menu command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:menu:allow-set-as-windows-menu-for-nsapp`
</td>
<td>
Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:menu:deny-set-as-windows-menu-for-nsapp`
</td>
<td>
Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:menu:allow-set-checked`
</td>
<td>
Enables the set_checked command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:menu:deny-set-checked`
</td>
<td>
Denies the set_checked command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:menu:allow-set-enabled`
</td>
<td>
Enables the set_enabled command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:menu:deny-set-enabled`
</td>
<td>
Denies the set_enabled command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:menu:allow-set-icon`
</td>
<td>
Enables the set_icon command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:menu:deny-set-icon`
</td>
<td>
Denies the set_icon command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:menu:allow-set-text`
</td>
<td>
Enables the set_text command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:menu:deny-set-text`
</td>
<td>
Denies the set_text command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:menu:allow-text`
</td>
<td>
Enables the text command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:menu:deny-text`
</td>
<td>
Denies the text command without any pre-configured scope.
</td>
</tr>
</table>

View File

@@ -0,0 +1,232 @@
## Default Permission
Default permissions for the plugin, which enables all commands.
#### This default permission set includes the following:
- `allow-resolve-directory`
- `allow-resolve`
- `allow-normalize`
- `allow-join`
- `allow-dirname`
- `allow-extname`
- `allow-basename`
- `allow-is-absolute`
## Permission Table
<table>
<tr>
<th>Identifier</th>
<th>Description</th>
</tr>
<tr>
<td>
`core:path:allow-basename`
</td>
<td>
Enables the basename command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:path:deny-basename`
</td>
<td>
Denies the basename command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:path:allow-dirname`
</td>
<td>
Enables the dirname command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:path:deny-dirname`
</td>
<td>
Denies the dirname command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:path:allow-extname`
</td>
<td>
Enables the extname command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:path:deny-extname`
</td>
<td>
Denies the extname command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:path:allow-is-absolute`
</td>
<td>
Enables the is_absolute command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:path:deny-is-absolute`
</td>
<td>
Denies the is_absolute command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:path:allow-join`
</td>
<td>
Enables the join command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:path:deny-join`
</td>
<td>
Denies the join command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:path:allow-normalize`
</td>
<td>
Enables the normalize command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:path:deny-normalize`
</td>
<td>
Denies the normalize command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:path:allow-resolve`
</td>
<td>
Enables the resolve command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:path:deny-resolve`
</td>
<td>
Denies the resolve command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:path:allow-resolve-directory`
</td>
<td>
Enables the resolve_directory command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:path:deny-resolve-directory`
</td>
<td>
Denies the resolve_directory command without any pre-configured scope.
</td>
</tr>
</table>

View File

@@ -0,0 +1,43 @@
## Default Permission
Default permissions for the plugin, which enables all commands.
#### This default permission set includes the following:
- `allow-close`
## Permission Table
<table>
<tr>
<th>Identifier</th>
<th>Description</th>
</tr>
<tr>
<td>
`core:resources:allow-close`
</td>
<td>
Enables the close command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:resources:deny-close`
</td>
<td>
Denies the close command without any pre-configured scope.
</td>
</tr>
</table>

View File

@@ -0,0 +1,340 @@
## Default Permission
Default permissions for the plugin, which enables all commands.
#### This default permission set includes the following:
- `allow-new`
- `allow-get-by-id`
- `allow-remove-by-id`
- `allow-set-icon`
- `allow-set-menu`
- `allow-set-tooltip`
- `allow-set-title`
- `allow-set-visible`
- `allow-set-temp-dir-path`
- `allow-set-icon-as-template`
- `allow-set-icon-with-as-template`
- `allow-set-show-menu-on-left-click`
## Permission Table
<table>
<tr>
<th>Identifier</th>
<th>Description</th>
</tr>
<tr>
<td>
`core:tray:allow-get-by-id`
</td>
<td>
Enables the get_by_id command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:tray:deny-get-by-id`
</td>
<td>
Denies the get_by_id command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:tray:allow-new`
</td>
<td>
Enables the new command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:tray:deny-new`
</td>
<td>
Denies the new command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:tray:allow-remove-by-id`
</td>
<td>
Enables the remove_by_id command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:tray:deny-remove-by-id`
</td>
<td>
Denies the remove_by_id command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:tray:allow-set-icon`
</td>
<td>
Enables the set_icon command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:tray:deny-set-icon`
</td>
<td>
Denies the set_icon command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:tray:allow-set-icon-as-template`
</td>
<td>
Enables the set_icon_as_template command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:tray:deny-set-icon-as-template`
</td>
<td>
Denies the set_icon_as_template command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:tray:allow-set-icon-with-as-template`
</td>
<td>
Enables the set_icon_with_as_template command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:tray:deny-set-icon-with-as-template`
</td>
<td>
Denies the set_icon_with_as_template command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:tray:allow-set-menu`
</td>
<td>
Enables the set_menu command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:tray:deny-set-menu`
</td>
<td>
Denies the set_menu command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:tray:allow-set-show-menu-on-left-click`
</td>
<td>
Enables the set_show_menu_on_left_click command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:tray:deny-set-show-menu-on-left-click`
</td>
<td>
Denies the set_show_menu_on_left_click command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:tray:allow-set-temp-dir-path`
</td>
<td>
Enables the set_temp_dir_path command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:tray:deny-set-temp-dir-path`
</td>
<td>
Denies the set_temp_dir_path command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:tray:allow-set-title`
</td>
<td>
Enables the set_title command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:tray:deny-set-title`
</td>
<td>
Denies the set_title command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:tray:allow-set-tooltip`
</td>
<td>
Enables the set_tooltip command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:tray:deny-set-tooltip`
</td>
<td>
Denies the set_tooltip command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:tray:allow-set-visible`
</td>
<td>
Enables the set_visible command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:tray:deny-set-visible`
</td>
<td>
Denies the set_visible command without any pre-configured scope.
</td>
</tr>
</table>

View File

@@ -0,0 +1,488 @@
## Default Permission
Default permissions for the plugin.
#### This default permission set includes the following:
- `allow-get-all-webviews`
- `allow-webview-position`
- `allow-webview-size`
- `allow-internal-toggle-devtools`
## Permission Table
<table>
<tr>
<th>Identifier</th>
<th>Description</th>
</tr>
<tr>
<td>
`core:webview:allow-clear-all-browsing-data`
</td>
<td>
Enables the clear_all_browsing_data command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:webview:deny-clear-all-browsing-data`
</td>
<td>
Denies the clear_all_browsing_data command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:webview:allow-create-webview`
</td>
<td>
Enables the create_webview command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:webview:deny-create-webview`
</td>
<td>
Denies the create_webview command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:webview:allow-create-webview-window`
</td>
<td>
Enables the create_webview_window command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:webview:deny-create-webview-window`
</td>
<td>
Denies the create_webview_window command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:webview:allow-get-all-webviews`
</td>
<td>
Enables the get_all_webviews command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:webview:deny-get-all-webviews`
</td>
<td>
Denies the get_all_webviews command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:webview:allow-internal-toggle-devtools`
</td>
<td>
Enables the internal_toggle_devtools command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:webview:deny-internal-toggle-devtools`
</td>
<td>
Denies the internal_toggle_devtools command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:webview:allow-print`
</td>
<td>
Enables the print command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:webview:deny-print`
</td>
<td>
Denies the print command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:webview:allow-reparent`
</td>
<td>
Enables the reparent command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:webview:deny-reparent`
</td>
<td>
Denies the reparent command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:webview:allow-set-webview-auto-resize`
</td>
<td>
Enables the set_webview_auto_resize command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:webview:deny-set-webview-auto-resize`
</td>
<td>
Denies the set_webview_auto_resize command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:webview:allow-set-webview-background-color`
</td>
<td>
Enables the set_webview_background_color command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:webview:deny-set-webview-background-color`
</td>
<td>
Denies the set_webview_background_color command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:webview:allow-set-webview-focus`
</td>
<td>
Enables the set_webview_focus command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:webview:deny-set-webview-focus`
</td>
<td>
Denies the set_webview_focus command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:webview:allow-set-webview-position`
</td>
<td>
Enables the set_webview_position command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:webview:deny-set-webview-position`
</td>
<td>
Denies the set_webview_position command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:webview:allow-set-webview-size`
</td>
<td>
Enables the set_webview_size command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:webview:deny-set-webview-size`
</td>
<td>
Denies the set_webview_size command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:webview:allow-set-webview-zoom`
</td>
<td>
Enables the set_webview_zoom command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:webview:deny-set-webview-zoom`
</td>
<td>
Denies the set_webview_zoom command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:webview:allow-webview-close`
</td>
<td>
Enables the webview_close command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:webview:deny-webview-close`
</td>
<td>
Denies the webview_close command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:webview:allow-webview-hide`
</td>
<td>
Enables the webview_hide command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:webview:deny-webview-hide`
</td>
<td>
Denies the webview_hide command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:webview:allow-webview-position`
</td>
<td>
Enables the webview_position command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:webview:deny-webview-position`
</td>
<td>
Denies the webview_position command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:webview:allow-webview-show`
</td>
<td>
Enables the webview_show command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:webview:deny-webview-show`
</td>
<td>
Denies the webview_show command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:webview:allow-webview-size`
</td>
<td>
Enables the webview_size command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:webview:deny-webview-size`
</td>
<td>
Denies the webview_size command without any pre-configured scope.
</td>
</tr>
</table>

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,114 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
;(function () {
function uid() {
return window.crypto.getRandomValues(new Uint32Array(1))[0]
}
const osName = __TEMPLATE_os_name__
const protocolScheme = __TEMPLATE_protocol_scheme__
Object.defineProperty(window.__TAURI_INTERNALS__, 'convertFileSrc', {
value: function (filePath, protocol = 'asset') {
const path = encodeURIComponent(filePath)
return osName === 'windows' || osName === 'android'
? `${protocolScheme}://${protocol}.localhost/${path}`
: `${protocol}://localhost/${path}`
}
})
const callbacks = new Map()
function registerCallback(callback, once) {
const identifier = uid()
callbacks.set(identifier, (data) => {
if (once) {
unregisterCallback(identifier)
}
return callback && callback(data)
})
return identifier
}
function unregisterCallback(id) {
callbacks.delete(id)
}
function runCallback(id, data) {
const callback = callbacks.get(id)
if (callback) {
callback(data)
} else {
console.warn(
`[TAURI] Couldn't find callback id ${id}. This might happen when the app is reloaded while Rust is running an asynchronous operation.`
)
}
}
// Maybe let's rename it to `registerCallback`?
Object.defineProperty(window.__TAURI_INTERNALS__, 'transformCallback', {
value: registerCallback
})
Object.defineProperty(window.__TAURI_INTERNALS__, 'unregisterCallback', {
value: unregisterCallback
})
Object.defineProperty(window.__TAURI_INTERNALS__, 'runCallback', {
value: runCallback
})
// This is just for the debugging purposes
Object.defineProperty(window.__TAURI_INTERNALS__, 'callbacks', {
value: callbacks
})
const ipcQueue = []
let isWaitingForIpc = false
function waitForIpc() {
if ('ipc' in window.__TAURI_INTERNALS__) {
for (const action of ipcQueue) {
action()
}
} else {
setTimeout(waitForIpc, 50)
}
}
Object.defineProperty(window.__TAURI_INTERNALS__, 'invoke', {
value: function (cmd, payload = {}, options) {
return new Promise(function (resolve, reject) {
const callback = registerCallback((r) => {
resolve(r)
unregisterCallback(error)
}, true)
const error = registerCallback((e) => {
reject(e)
unregisterCallback(callback)
}, true)
const action = () => {
window.__TAURI_INTERNALS__.ipc({
cmd,
callback,
error,
payload,
options
})
}
if ('ipc' in window.__TAURI_INTERNALS__) {
action()
} else {
ipcQueue.push(action)
if (!isWaitingForIpc) {
waitForIpc()
isWaitingForIpc = true
}
}
})
}
})
})()

View File

@@ -0,0 +1,5 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
Object.freeze(Object.prototype)

View File

@@ -0,0 +1,15 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
;(function () {
__RAW_freeze_prototype__
__RAW_pattern_script__
__RAW_ipc_script__
__RAW_core_script__
__RAW_event_initialization_script__
})()

View File

@@ -0,0 +1,91 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
;(function () {
/**
* A runtime generated key to ensure an IPC call comes from an initialized frame.
*
* This is declared outside the `window.__TAURI_INVOKE__` definition to prevent
* the key from being leaked by `window.__TAURI_INVOKE__.toString()`.
*/
const __TAURI_INVOKE_KEY__ = __TEMPLATE_invoke_key__
const processIpcMessage = __RAW_process_ipc_message_fn__
const osName = __TEMPLATE_os_name__
const fetchChannelDataCommand = __TEMPLATE_fetch_channel_data_command__
let customProtocolIpcFailed = false
// on Android we never use it because Android does not have support to reading the request body
const canUseCustomProtocol = osName !== 'android'
function sendIpcMessage(message) {
const { cmd, callback, error, payload, options } = message
if (
!customProtocolIpcFailed
&& (canUseCustomProtocol || cmd === fetchChannelDataCommand)
) {
const { contentType, data } = processIpcMessage(payload)
const headers = new Headers((options && options.headers) || {})
headers.set('Content-Type', contentType)
headers.set('Tauri-Callback', callback)
headers.set('Tauri-Error', error)
headers.set('Tauri-Invoke-Key', __TAURI_INVOKE_KEY__)
fetch(window.__TAURI_INTERNALS__.convertFileSrc(cmd, 'ipc'), {
method: 'POST',
body: data,
headers
})
.then((response) => {
const callbackId =
response.headers.get('Tauri-Response') === 'ok' ? callback : error
// we need to split here because on Android the content-type gets duplicated
switch ((response.headers.get('content-type') || '').split(',')[0]) {
case 'application/json':
return response.json().then((r) => [callbackId, r])
case 'text/plain':
return response.text().then((r) => [callbackId, r])
default:
return response.arrayBuffer().then((r) => [callbackId, r])
}
})
.then(
([callbackId, data]) => {
window.__TAURI_INTERNALS__.runCallback(callbackId, data)
},
(e) => {
console.warn(
'IPC custom protocol failed, Tauri will now use the postMessage interface instead',
e
)
// failed to use the custom protocol IPC (either the webview blocked a custom protocol or it was a CSP error)
// so we need to fallback to the postMessage interface
customProtocolIpcFailed = true
sendIpcMessage(message)
}
)
} else {
// otherwise use the postMessage interface
const { data } = processIpcMessage({
cmd,
callback,
error,
options: {
...options,
customProtocolIpcBlocked: customProtocolIpcFailed
},
payload,
__TAURI_INVOKE_KEY__
})
// `window.ipc.postMessage` came from `tauri-runtime-wry` > `wry` [`with_ipc_handler`](https://github.com/tauri-apps/wry/blob/a0403b9e2f1ff9d73be7dce1184f058afcaa1d82/src/lib.rs#L1130)
window.ipc.postMessage(data)
}
}
Object.defineProperty(window.__TAURI_INTERNALS__, 'postMessage', {
value: sendIpcMessage
})
})()

210
crates/tauri/scripts/ipc.js Normal file
View File

@@ -0,0 +1,210 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
/**
* @typedef {{callback: string, error: string, data: *}} IsolationPayload - a valid isolation payload
*/
;(function () {
/**
* @type {string}
*/
const pattern = window.__TAURI_INTERNALS__.__TAURI_PATTERN__.pattern
/**
* @type {string}
*/
const isolationOrigin = __TEMPLATE_isolation_origin__
/**
* @type {{queue: object[], ready: boolean, frame: HTMLElement | null}}
*/
const isolation = Object.create(null)
isolation.queue = []
isolation.ready = false
isolation.frame = null
/**
* Detects if a message event is a valid isolation message.
*
* @param {MessageEvent<object>} event - a message event that is expected to be an isolation message
* @return {boolean} - if the event was a valid isolation message
*/
function isIsolationMessage(event) {
if (
typeof event.data === 'object'
&& typeof event.data.payload === 'object'
) {
const keys = Object.keys(event.data.payload || {})
return (
keys.length > 0
&& keys.every(
(key) => key === 'contentType' || key === 'nonce' || key === 'payload'
)
)
}
return false
}
/**
* Detects if data is able to transform into an isolation payload.
*
* @param {object} data - object that is expected to contain at least a callback and error identifier
* @return {boolean} - if the data is able to transform into an isolation payload
*/
function isIsolationPayload(data) {
return (
typeof data === 'object'
&& 'callback' in data
&& 'error' in data
&& !isIsolationMessage(data)
)
}
/**
* Sends a properly formatted message to the isolation frame.
*
* @param {IsolationPayload} data - data that has been validated to be an isolation payload
*/
function sendIsolationMessage(data) {
// set the frame dom element if it's not been set before
if (!isolation.frame) {
const frame = document.querySelector('iframe#__tauri_isolation__')
if (frame.src.startsWith(isolationOrigin)) {
isolation.frame = frame
} else {
console.error(
'Tauri IPC found an isolation iframe, but it had the wrong origin'
)
}
}
// ensure we have the target to send the message to
if (!isolation.frame || !isolation.frame.contentWindow) {
console.error(
'Tauri "Isolation" Pattern could not find the Isolation iframe window'
)
return
}
// `postMessage` uses `structuredClone` to serialize the data before sending it
// unlike `JSON.stringify`, we can't extend a type with a method similar to `toJSON`
// so that `structuredClone` would use, so until https://github.com/whatwg/html/issues/7428
// we manually call `toIPC`
function serializeIpcPayload(data) {
// if this value changes, make sure to update it in:
// 1. process-ipc-message-fn.js
// 2. core.ts
const SERIALIZE_TO_IPC_FN = '__TAURI_TO_IPC_KEY__'
if (
typeof data === 'object'
&& data !== null
&& 'constructor' in data
&& data.constructor === Array
) {
return data.map((v) => serializeIpcPayload(v))
}
if (
typeof data === 'object'
&& data !== null
&& SERIALIZE_TO_IPC_FN in data
) {
return data[SERIALIZE_TO_IPC_FN]()
}
if (
typeof data === 'object'
&& data !== null
&& 'constructor' in data
&& data.constructor === Object
) {
const acc = {}
Object.entries(data).forEach(([k, v]) => {
acc[k] = serializeIpcPayload(v)
})
return acc
}
return data
}
data.payload = serializeIpcPayload(data.payload)
isolation.frame.contentWindow.postMessage(
data,
'*' /* todo: set this to the secure origin */
)
}
Object.defineProperty(window.__TAURI_INTERNALS__, 'ipc', {
// todo: JSDoc this function
value: Object.freeze((message) => {
switch (pattern) {
case 'brownfield':
window.__TAURI_INTERNALS__.postMessage(message)
break
case 'isolation':
if (!isIsolationPayload(message)) {
console.error(
'Tauri "Isolation" Pattern found an invalid isolation message payload',
message
)
break
}
if (isolation.ready) {
sendIsolationMessage(message)
} else {
isolation.queue.push(message)
}
break
case 'error':
console.error(
'Tauri IPC found a Tauri Pattern, but it was an error. Check for other log messages to find the cause.'
)
break
default:
console.error(
'Tauri IPC did not find a Tauri Pattern that it understood.'
)
break
}
})
})
/**
* IMPORTANT: See isolation_secure.js for the isolation frame implementation.
* main frame -> isolation frame = isolation payload
* isolation frame -> main frame = isolation message
*/
if (pattern === 'isolation') {
window.addEventListener(
'message',
(event) => {
// watch for the isolation frame being ready and flush any queued messages
if (event.data === '__TAURI_ISOLATION_READY__') {
isolation.ready = true
for (const message of isolation.queue) {
sendIsolationMessage(message)
}
isolation.queue = []
return
}
if (isIsolationMessage(event)) {
window.__TAURI_INTERNALS__.postMessage(event.data)
}
},
false
)
}
})()

View File

@@ -0,0 +1,17 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
if (location.href !== __TEMPLATE_isolation_src__) {
window.addEventListener('DOMContentLoaded', () => {
let style = document.createElement('style')
style.textContent = __TEMPLATE_style__
document.head.append(style)
let iframe = document.createElement('iframe')
iframe.id = '__tauri_isolation__'
iframe.sandbox.add('allow-scripts')
iframe.src = __TEMPLATE_isolation_src__
document.body.append(iframe)
})
}

View File

@@ -0,0 +1,21 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
;(function () {
function __tauriDeepFreeze(object) {
const props = Object.getOwnPropertyNames(object)
for (const prop of props) {
if (typeof object[prop] === 'object') {
__tauriDeepFreeze(object[prop])
}
}
return Object.freeze(object)
}
Object.defineProperty(window.__TAURI_INTERNALS__, '__TAURI_PATTERN__', {
value: __tauriDeepFreeze(__TEMPLATE_pattern__)
})
})()

View File

@@ -0,0 +1,46 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
// this is a function and not an iife so use it carefully
(function (message) {
if (
message instanceof ArrayBuffer
|| ArrayBuffer.isView(message)
|| Array.isArray(message)
) {
return {
contentType: 'application/octet-stream',
data: message
}
} else {
const data = JSON.stringify(message, (_k, val) => {
// if this value changes, make sure to update it in:
// 1. ipc.js
// 2. core.ts
const SERIALIZE_TO_IPC_FN = '__TAURI_TO_IPC_KEY__'
if (val instanceof Map) {
return Object.fromEntries(val.entries())
} else if (val instanceof Uint8Array) {
return Array.from(val)
} else if (val instanceof ArrayBuffer) {
return Array.from(new Uint8Array(val))
} else if (
typeof val === 'object'
&& val !== null
&& SERIALIZE_TO_IPC_FN in val
) {
return val[SERIALIZE_TO_IPC_FN]()
} else {
return val
}
})
return {
contentType: 'application/json',
data
}
}
})

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More