snapshot of https://github.com/tauri-apps/tauri.git (dev, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Audit / audit-js (push) Failing after 0s
bench / bench (map[os:ubuntu-latest runner:xvfb-run --auto-servernum target:x86_64-unknown-linux-gnu], nightly) (push) Failing after 0s
covector version or publish / run-integration-tests (ubuntu-latest) (push) Failing after 0s
Audit / audit-rust (push) Failing after 0s
test `@tauri-apps/cli` / test (ubuntu-latest) (push) Failing after 0s
test `tauri-cli` / test (map[os:ubuntu-latest target:x86_64-unknown-linux-gnu]) (push) Failing after 0s
test core / test (map[args:--all-features key:all], map[cargo:cargo command:test os:ubuntu-latest target:x86_64-unknown-linux-gnu toolchain:1.90]) (push) Failing after 0s
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:test os:ubuntu-latest target:x86_64-unknown-linux-gnu toolchain:1.90]) (push) Failing after 0s
test core / test (map[args:--no-default-features key:no-default], map[cargo:cross command:build os:ubuntu-latest target:aarch64-linux-android toolchain:1.90]) (push) Failing after 0s
deploy schema worker / deploy (push) Failing after 0s
lint rust / clippy (map[cargo:cargo include: os:ubuntu-latest target:x86_64-unknown-linux-gnu]) (push) Failing after 0s
lint rust / clippy (map[cargo:cross include:--package api os:ubuntu-latest target:aarch64-linux-android]) (push) Failing after 0s
supply chain health status / check rust dependencies with cargo vet (push) Failing after 0s
test `@tauri-apps/api` e2e (mobile) / android (push) Failing after 0s
test `@tauri-apps/api` e2e / test (map[name:Linux os:ubuntu-latest]) (push) Failing after 0s
test core / test (map[args:--all-features key:all], map[cargo:cross command:build os:ubuntu-latest target:aarch64-linux-android toolchain:1.90]) (push) Failing after 0s
Udeps / changes (push) Failing after 0s
Udeps / setup (push) Has been skipped
Udeps / udeps (push) Has been skipped
bench / bench (map[os:macos-latest runner: target:aarch64-apple-darwin], nightly) (push) Has been cancelled
bench / bench (map[os:windows-latest runner: target:x86_64-pc-windows-msvc], nightly) (push) Has been cancelled
covector version or publish / run-integration-tests (macos-latest) (push) Has been cancelled
covector version or publish / run-integration-tests (windows-latest) (push) Has been cancelled
covector version or publish / version-or-publish (push) Has been cancelled
test `@tauri-apps/cli` / test (macos-latest) (push) Has been cancelled
test `tauri-cli` / test (map[args:--no-default-features --features native-tls-vendored os:windows-latest restore-only:true target:aarch64-pc-windows-msvc]) (push) Has been cancelled
test `tauri-cli` / test (map[os:macos-latest target:x86_64-apple-darwin]) (push) Has been cancelled
test `tauri-cli` / test (map[os:windows-latest target:x86_64-pc-windows-msvc]) (push) Has been cancelled
test core / test (map[args:--all-features key:all], map[cargo:cargo command:build os:macos-latest target:aarch64-apple-ios toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--all-features key:all], map[cargo:cargo command:test os:macos-14 target:aarch64-apple-darwin toolchain:1.90]) (push) Has been cancelled
lint rust / clippy (map[cargo:cargo include: os:macos-latest target:aarch64-apple-darwin]) (push) Has been cancelled
lint rust / clippy (map[cargo:cargo include: os:windows-latest target:x86_64-pc-windows-msvc]) (push) Has been cancelled
lint rust / clippy (map[cargo:cargo include:--package api os:macos-latest target:aarch64-apple-ios]) (push) Has been cancelled
test `@tauri-apps/api` e2e (mobile) / ios (push) Has been cancelled
test `@tauri-apps/api` e2e / test (map[name:Windows os:windows-latest]) (push) Has been cancelled
test `@tauri-apps/api` e2e / test (map[name:macOS os:macos-latest]) (push) Has been cancelled
test `@tauri-apps/cli` / test (windows-latest) (push) Has been cancelled
test core / test (map[args:--all-features key:all], map[cargo:cargo command:test os:windows-latest target:x86_64-pc-windows-msvc toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:build os:macos-latest target:aarch64-apple-ios toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:test os:macos-14 target:aarch64-apple-darwin toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:test os:windows-latest target:x86_64-pc-windows-msvc toolchain:1.90]) (push) Has been cancelled
Some checks failed
Audit / audit-js (push) Failing after 0s
bench / bench (map[os:ubuntu-latest runner:xvfb-run --auto-servernum target:x86_64-unknown-linux-gnu], nightly) (push) Failing after 0s
covector version or publish / run-integration-tests (ubuntu-latest) (push) Failing after 0s
Audit / audit-rust (push) Failing after 0s
test `@tauri-apps/cli` / test (ubuntu-latest) (push) Failing after 0s
test `tauri-cli` / test (map[os:ubuntu-latest target:x86_64-unknown-linux-gnu]) (push) Failing after 0s
test core / test (map[args:--all-features key:all], map[cargo:cargo command:test os:ubuntu-latest target:x86_64-unknown-linux-gnu toolchain:1.90]) (push) Failing after 0s
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:test os:ubuntu-latest target:x86_64-unknown-linux-gnu toolchain:1.90]) (push) Failing after 0s
test core / test (map[args:--no-default-features key:no-default], map[cargo:cross command:build os:ubuntu-latest target:aarch64-linux-android toolchain:1.90]) (push) Failing after 0s
deploy schema worker / deploy (push) Failing after 0s
lint rust / clippy (map[cargo:cargo include: os:ubuntu-latest target:x86_64-unknown-linux-gnu]) (push) Failing after 0s
lint rust / clippy (map[cargo:cross include:--package api os:ubuntu-latest target:aarch64-linux-android]) (push) Failing after 0s
supply chain health status / check rust dependencies with cargo vet (push) Failing after 0s
test `@tauri-apps/api` e2e (mobile) / android (push) Failing after 0s
test `@tauri-apps/api` e2e / test (map[name:Linux os:ubuntu-latest]) (push) Failing after 0s
test core / test (map[args:--all-features key:all], map[cargo:cross command:build os:ubuntu-latest target:aarch64-linux-android toolchain:1.90]) (push) Failing after 0s
Udeps / changes (push) Failing after 0s
Udeps / setup (push) Has been skipped
Udeps / udeps (push) Has been skipped
bench / bench (map[os:macos-latest runner: target:aarch64-apple-darwin], nightly) (push) Has been cancelled
bench / bench (map[os:windows-latest runner: target:x86_64-pc-windows-msvc], nightly) (push) Has been cancelled
covector version or publish / run-integration-tests (macos-latest) (push) Has been cancelled
covector version or publish / run-integration-tests (windows-latest) (push) Has been cancelled
covector version or publish / version-or-publish (push) Has been cancelled
test `@tauri-apps/cli` / test (macos-latest) (push) Has been cancelled
test `tauri-cli` / test (map[args:--no-default-features --features native-tls-vendored os:windows-latest restore-only:true target:aarch64-pc-windows-msvc]) (push) Has been cancelled
test `tauri-cli` / test (map[os:macos-latest target:x86_64-apple-darwin]) (push) Has been cancelled
test `tauri-cli` / test (map[os:windows-latest target:x86_64-pc-windows-msvc]) (push) Has been cancelled
test core / test (map[args:--all-features key:all], map[cargo:cargo command:build os:macos-latest target:aarch64-apple-ios toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--all-features key:all], map[cargo:cargo command:test os:macos-14 target:aarch64-apple-darwin toolchain:1.90]) (push) Has been cancelled
lint rust / clippy (map[cargo:cargo include: os:macos-latest target:aarch64-apple-darwin]) (push) Has been cancelled
lint rust / clippy (map[cargo:cargo include: os:windows-latest target:x86_64-pc-windows-msvc]) (push) Has been cancelled
lint rust / clippy (map[cargo:cargo include:--package api os:macos-latest target:aarch64-apple-ios]) (push) Has been cancelled
test `@tauri-apps/api` e2e (mobile) / ios (push) Has been cancelled
test `@tauri-apps/api` e2e / test (map[name:Windows os:windows-latest]) (push) Has been cancelled
test `@tauri-apps/api` e2e / test (map[name:macOS os:macos-latest]) (push) Has been cancelled
test `@tauri-apps/cli` / test (windows-latest) (push) Has been cancelled
test core / test (map[args:--all-features key:all], map[cargo:cargo command:test os:windows-latest target:x86_64-pc-windows-msvc toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:build os:macos-latest target:aarch64-apple-ios toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:test os:macos-14 target:aarch64-apple-darwin toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:test os:windows-latest target:x86_64-pc-windows-msvc toolchain:1.90]) (push) Has been cancelled
This commit is contained in:
14
crates/tauri/.scripts/loop_qc.sh
Normal file
14
crates/tauri/.scripts/loop_qc.sh
Normal 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
278
crates/tauri/Cargo.toml
Normal 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
|
||||
177
crates/tauri/LICENSE-APACHE-2.0
Normal file
177
crates/tauri/LICENSE-APACHE-2.0
Normal 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
21
crates/tauri/LICENSE-MIT
Normal 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
42
crates/tauri/README.md
Normal 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">
|
||||
|
||||
[](https://github.com/tauri-apps/tauri/tree/dev)
|
||||
[](https://opencollective.com/tauri)
|
||||
[](https://github.com/tauri-apps/tauri/actions/workflows/test-core.yml)
|
||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2Ftauri-apps%2Ftauri?ref=badge_shield)
|
||||
[](https://discord.gg/SpmNs4S)
|
||||
[](https://tauri.app)
|
||||
[](https://good-labs.github.io/greater-good-affirmation)
|
||||
[](https://opencollective.com/tauri)
|
||||
|
||||
| Component | Version |
|
||||
| --------- | ---------------------------------------------------------------------------------------------- |
|
||||
| tauri | [](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
500
crates/tauri/build.rs
Normal 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");
|
||||
}
|
||||
60
crates/tauri/mobile/android-codegen/TauriActivity.kt
Normal file
60
crates/tauri/mobile/android-codegen/TauriActivity.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
1
crates/tauri/mobile/android/.gitignore
vendored
Normal file
1
crates/tauri/mobile/android/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
||||
48
crates/tauri/mobile/android/build.gradle.kts
Normal file
48
crates/tauri/mobile/android/build.gradle.kts
Normal 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")
|
||||
}
|
||||
41
crates/tauri/mobile/android/consumer-rules.pro
Normal file
41
crates/tauri/mobile/android/consumer-rules.pro
Normal 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 { *; }
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
3
crates/tauri/mobile/android/src/main/AndroidManifest.xml
Normal file
3
crates/tauri/mobile/android/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
</manifest>
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
208
crates/tauri/mobile/android/src/main/java/app/tauri/FsUtils.kt
Normal file
208
crates/tauri/mobile/android/src/main/java/app/tauri/FsUtils.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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('-', '_'))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 = ""
|
||||
)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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> = []
|
||||
)
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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) {}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
10
crates/tauri/mobile/ios-api/.gitignore
vendored
Normal 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
|
||||
40
crates/tauri/mobile/ios-api/Package.swift
Normal file
40
crates/tauri/mobile/ios-api/Package.swift
Normal 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"]
|
||||
),
|
||||
]
|
||||
)
|
||||
3
crates/tauri/mobile/ios-api/README.md
Normal file
3
crates/tauri/mobile/ios-api/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Tauri
|
||||
|
||||
Tauri iOS API.
|
||||
65
crates/tauri/mobile/ios-api/Sources/Tauri/Channel.swift
Normal file
65
crates/tauri/mobile/ios-api/Sources/Tauri/Channel.swift
Normal 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))
|
||||
}
|
||||
|
||||
}
|
||||
118
crates/tauri/mobile/ios-api/Sources/Tauri/Invoke.swift
Normal file
118
crates/tauri/mobile/ios-api/Sources/Tauri/Invoke.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
123
crates/tauri/mobile/ios-api/Sources/Tauri/JSTypes.swift
Normal file
123
crates/tauri/mobile/ios-api/Sources/Tauri/JSTypes.swift
Normal 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
|
||||
}
|
||||
}
|
||||
58
crates/tauri/mobile/ios-api/Sources/Tauri/JsonValue.swift
Normal file
58
crates/tauri/mobile/ios-api/Sources/Tauri/JsonValue.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
139
crates/tauri/mobile/ios-api/Sources/Tauri/Logger.swift
Normal file
139
crates/tauri/mobile/ios-api/Sources/Tauri/Logger.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
157
crates/tauri/mobile/ios-api/Sources/Tauri/Tauri.swift
Normal file
157
crates/tauri/mobile/ios-api/Sources/Tauri/Tauri.swift
Normal 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)
|
||||
}
|
||||
15
crates/tauri/mobile/ios-api/Sources/Tauri/UiUtils.swift
Normal file
15
crates/tauri/mobile/ios-api/Sources/Tauri/UiUtils.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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!")
|
||||
}
|
||||
}
|
||||
5
crates/tauri/mobile/proguard-tauri.pro
Normal file
5
crates/tauri/mobile/proguard-tauri.pro
Normal 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();
|
||||
}
|
||||
440
crates/tauri/permissions/app/autogenerated/reference.md
Normal file
440
crates/tauri/permissions/app/autogenerated/reference.md
Normal 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>
|
||||
124
crates/tauri/permissions/event/autogenerated/reference.md
Normal file
124
crates/tauri/permissions/event/autogenerated/reference.md
Normal 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>
|
||||
151
crates/tauri/permissions/image/autogenerated/reference.md
Normal file
151
crates/tauri/permissions/image/autogenerated/reference.md
Normal 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>
|
||||
610
crates/tauri/permissions/menu/autogenerated/reference.md
Normal file
610
crates/tauri/permissions/menu/autogenerated/reference.md
Normal 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>
|
||||
232
crates/tauri/permissions/path/autogenerated/reference.md
Normal file
232
crates/tauri/permissions/path/autogenerated/reference.md
Normal 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>
|
||||
@@ -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>
|
||||
340
crates/tauri/permissions/tray/autogenerated/reference.md
Normal file
340
crates/tauri/permissions/tray/autogenerated/reference.md
Normal 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>
|
||||
488
crates/tauri/permissions/webview/autogenerated/reference.md
Normal file
488
crates/tauri/permissions/webview/autogenerated/reference.md
Normal 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>
|
||||
2098
crates/tauri/permissions/window/autogenerated/reference.md
Normal file
2098
crates/tauri/permissions/window/autogenerated/reference.md
Normal file
File diff suppressed because it is too large
Load Diff
1
crates/tauri/scripts/bundle.global.js
Normal file
1
crates/tauri/scripts/bundle.global.js
Normal file
File diff suppressed because one or more lines are too long
114
crates/tauri/scripts/core.js
Normal file
114
crates/tauri/scripts/core.js
Normal 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
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})()
|
||||
5
crates/tauri/scripts/freeze_prototype.js
Normal file
5
crates/tauri/scripts/freeze_prototype.js
Normal 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)
|
||||
15
crates/tauri/scripts/init.js
Normal file
15
crates/tauri/scripts/init.js
Normal 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__
|
||||
})()
|
||||
91
crates/tauri/scripts/ipc-protocol.js
Normal file
91
crates/tauri/scripts/ipc-protocol.js
Normal 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
210
crates/tauri/scripts/ipc.js
Normal 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
|
||||
)
|
||||
}
|
||||
})()
|
||||
17
crates/tauri/scripts/isolation.js
Normal file
17
crates/tauri/scripts/isolation.js
Normal file
@@ -0,0 +1,17 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
21
crates/tauri/scripts/pattern.js
Normal file
21
crates/tauri/scripts/pattern.js
Normal 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__)
|
||||
})
|
||||
})()
|
||||
46
crates/tauri/scripts/process-ipc-message-fn.js
Normal file
46
crates/tauri/scripts/process-ipc-message-fn.js
Normal 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
2842
crates/tauri/src/app.rs
Normal file
File diff suppressed because it is too large
Load Diff
158
crates/tauri/src/app/plugin.rs
Normal file
158
crates/tauri/src/app/plugin.rs
Normal file
@@ -0,0 +1,158 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use tauri_utils::{Theme, config::BundleType};
|
||||
|
||||
use crate::{
|
||||
AppHandle, Manager, ResourceId, Runtime, Webview, command,
|
||||
plugin::{Builder, TauriPlugin},
|
||||
};
|
||||
|
||||
#[command(root = "crate")]
|
||||
pub fn version<R: Runtime>(app: AppHandle<R>) -> String {
|
||||
app.package_info().version.to_string()
|
||||
}
|
||||
|
||||
#[command(root = "crate")]
|
||||
pub fn name<R: Runtime>(app: AppHandle<R>) -> String {
|
||||
app.package_info().name.clone()
|
||||
}
|
||||
|
||||
#[command(root = "crate")]
|
||||
pub fn tauri_version() -> &'static str {
|
||||
crate::VERSION
|
||||
}
|
||||
|
||||
#[command(root = "crate")]
|
||||
pub fn identifier<R: Runtime>(app: AppHandle<R>) -> String {
|
||||
app.config().identifier.clone()
|
||||
}
|
||||
|
||||
#[command(root = "crate")]
|
||||
#[allow(unused_variables)]
|
||||
pub fn app_show<R: Runtime>(app: AppHandle<R>) -> crate::Result<()> {
|
||||
#[cfg(target_os = "macos")]
|
||||
app.show()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command(root = "crate")]
|
||||
#[allow(unused_variables)]
|
||||
pub fn app_hide<R: Runtime>(app: AppHandle<R>) -> crate::Result<()> {
|
||||
#[cfg(target_os = "macos")]
|
||||
app.hide()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command(root = "crate")]
|
||||
pub fn exit<R: Runtime>(app: AppHandle<R>, code: i32) {
|
||||
app.exit(code)
|
||||
}
|
||||
|
||||
#[command(root = "crate")]
|
||||
#[allow(unused_variables)]
|
||||
pub async fn fetch_data_store_identifiers<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
) -> crate::Result<Vec<[u8; 16]>> {
|
||||
#[cfg(target_vendor = "apple")]
|
||||
return app.fetch_data_store_identifiers().await;
|
||||
#[cfg(not(target_vendor = "apple"))]
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
#[command(root = "crate")]
|
||||
#[allow(unused_variables)]
|
||||
pub async fn remove_data_store<R: Runtime>(app: AppHandle<R>, uuid: [u8; 16]) -> crate::Result<()> {
|
||||
#[cfg(target_vendor = "apple")]
|
||||
app.remove_data_store(uuid).await?;
|
||||
#[cfg(not(target_vendor = "apple"))]
|
||||
let _ = uuid;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command(root = "crate")]
|
||||
pub fn default_window_icon<R: Runtime>(
|
||||
webview: Webview<R>,
|
||||
app: AppHandle<R>,
|
||||
) -> Option<ResourceId> {
|
||||
app.default_window_icon().cloned().map(|icon| {
|
||||
let mut resources_table = webview.resources_table();
|
||||
resources_table.add(icon.to_owned())
|
||||
})
|
||||
}
|
||||
|
||||
#[command(root = "crate")]
|
||||
pub async fn set_app_theme<R: Runtime>(app: AppHandle<R>, theme: Option<Theme>) {
|
||||
app.set_theme(theme);
|
||||
}
|
||||
|
||||
#[command(root = "crate")]
|
||||
pub async fn set_dock_visibility<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
visible: bool,
|
||||
) -> crate::Result<()> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let mut focused_window = None;
|
||||
for window in app.manager.windows().into_values() {
|
||||
if window.is_focused().unwrap_or_default() {
|
||||
focused_window.replace(window);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
app.set_dock_visibility(visible)?;
|
||||
|
||||
// retain focus
|
||||
if let Some(focused_window) = focused_window {
|
||||
let _ = focused_window.set_focus();
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let (_app, _visible) = (app, visible);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command(root = "crate")]
|
||||
pub fn bundle_type() -> Option<BundleType> {
|
||||
tauri_utils::platform::bundle_type()
|
||||
}
|
||||
|
||||
#[command(root = "crate")]
|
||||
pub fn supports_multiple_windows<R: Runtime>(app: AppHandle<R>) -> bool {
|
||||
app.supports_multiple_windows()
|
||||
}
|
||||
|
||||
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
Builder::new("app")
|
||||
.invoke_handler(crate::generate_handler![
|
||||
#![plugin(app)]
|
||||
version,
|
||||
name,
|
||||
tauri_version,
|
||||
identifier,
|
||||
app_show,
|
||||
app_hide,
|
||||
exit,
|
||||
fetch_data_store_identifiers,
|
||||
remove_data_store,
|
||||
default_window_icon,
|
||||
set_app_theme,
|
||||
set_dock_visibility,
|
||||
bundle_type,
|
||||
supports_multiple_windows,
|
||||
])
|
||||
.setup(|_app, _api| {
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
let handle = _api.register_android_plugin("app.tauri", "AppPlugin")?;
|
||||
_app.manage(AppPlugin(handle));
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.build()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub(crate) struct AppPlugin<R: Runtime>(pub crate::plugin::PluginHandle<R>);
|
||||
363
crates/tauri/src/async_runtime.rs
Normal file
363
crates/tauri/src/async_runtime.rs
Normal file
@@ -0,0 +1,363 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! The singleton async runtime used by Tauri and exposed to users.
|
||||
//!
|
||||
//! Tauri uses [`tokio`] Runtime to initialize code, such as
|
||||
//! [`Plugin::initialize`](../plugin/trait.Plugin.html#method.initialize) and [`crate::Builder::setup`] hooks.
|
||||
//! This module also re-export some common items most developers need from [`tokio`]. If there's
|
||||
//! one you need isn't here, you could use types in [`tokio`] directly.
|
||||
//! For custom command handlers, it's recommended to use a plain `async fn` command.
|
||||
|
||||
pub use tokio::{
|
||||
runtime::{Handle as TokioHandle, Runtime as TokioRuntime},
|
||||
sync::{
|
||||
Mutex, RwLock,
|
||||
mpsc::{Receiver, Sender, channel},
|
||||
},
|
||||
task::JoinHandle as TokioJoinHandle,
|
||||
};
|
||||
|
||||
use std::{
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
sync::OnceLock,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
static RUNTIME: OnceLock<GlobalRuntime> = OnceLock::new();
|
||||
|
||||
struct GlobalRuntime {
|
||||
runtime: Option<Runtime>,
|
||||
handle: RuntimeHandle,
|
||||
}
|
||||
|
||||
impl GlobalRuntime {
|
||||
fn handle(&self) -> RuntimeHandle {
|
||||
if let Some(r) = &self.runtime {
|
||||
r.handle()
|
||||
} else {
|
||||
self.handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn spawn<F>(&self, task: F) -> JoinHandle<F::Output>
|
||||
where
|
||||
F: Future + Send + 'static,
|
||||
F::Output: Send + 'static,
|
||||
{
|
||||
if let Some(r) = &self.runtime {
|
||||
r.spawn(task)
|
||||
} else {
|
||||
self.handle.spawn(task)
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
/// Runs the provided function on an executor dedicated to blocking operations.
|
||||
pub fn spawn_blocking<F, R>(&self, func: F) -> JoinHandle<R>
|
||||
where
|
||||
F: FnOnce() -> R + Send + 'static,
|
||||
R: Send + 'static,
|
||||
{
|
||||
if let Some(r) = &self.runtime {
|
||||
r.spawn_blocking(func)
|
||||
} else {
|
||||
self.handle.spawn_blocking(func)
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn block_on<F: Future>(&self, task: F) -> F::Output {
|
||||
if let Some(r) = &self.runtime {
|
||||
r.block_on(task)
|
||||
} else {
|
||||
self.handle.block_on(task)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A runtime used to execute asynchronous tasks.
|
||||
pub enum Runtime {
|
||||
/// The tokio runtime.
|
||||
Tokio(TokioRuntime),
|
||||
}
|
||||
|
||||
impl Runtime {
|
||||
/// Gets a reference to the [`TokioRuntime`].
|
||||
pub fn inner(&self) -> &TokioRuntime {
|
||||
let Self::Tokio(r) = self;
|
||||
r
|
||||
}
|
||||
|
||||
/// Returns a handle of the async runtime.
|
||||
pub fn handle(&self) -> RuntimeHandle {
|
||||
match self {
|
||||
Self::Tokio(r) => RuntimeHandle::Tokio(r.handle().clone()),
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
/// Spawns a future onto the runtime.
|
||||
pub fn spawn<F>(&self, task: F) -> JoinHandle<F::Output>
|
||||
where
|
||||
F: Future + Send + 'static,
|
||||
F::Output: Send + 'static,
|
||||
{
|
||||
match self {
|
||||
Self::Tokio(r) => {
|
||||
let _guard = r.enter();
|
||||
JoinHandle::Tokio(tokio::spawn(task))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
/// Runs the provided function on an executor dedicated to blocking operations.
|
||||
pub fn spawn_blocking<F, R>(&self, func: F) -> JoinHandle<R>
|
||||
where
|
||||
F: FnOnce() -> R + Send + 'static,
|
||||
R: Send + 'static,
|
||||
{
|
||||
match self {
|
||||
Self::Tokio(r) => JoinHandle::Tokio(r.spawn_blocking(func)),
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
/// Runs a future to completion on runtime.
|
||||
pub fn block_on<F: Future>(&self, task: F) -> F::Output {
|
||||
match self {
|
||||
Self::Tokio(r) => r.block_on(task),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An owned permission to join on a task (await its termination).
|
||||
#[derive(Debug)]
|
||||
pub enum JoinHandle<T> {
|
||||
/// The tokio JoinHandle.
|
||||
Tokio(TokioJoinHandle<T>),
|
||||
}
|
||||
|
||||
impl<T> JoinHandle<T> {
|
||||
/// Gets a reference to the [`TokioJoinHandle`].
|
||||
pub fn inner(&self) -> &TokioJoinHandle<T> {
|
||||
let Self::Tokio(t) = self;
|
||||
t
|
||||
}
|
||||
|
||||
/// Abort the task associated with the handle.
|
||||
///
|
||||
/// Awaiting a cancelled task might complete as usual if the task was
|
||||
/// already completed at the time it was cancelled, but most likely it
|
||||
/// will fail with a cancelled `JoinError`.
|
||||
pub fn abort(&self) {
|
||||
match self {
|
||||
Self::Tokio(t) => t.abort(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Future for JoinHandle<T> {
|
||||
type Output = crate::Result<T>;
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
match self.get_mut() {
|
||||
Self::Tokio(t) => Pin::new(t).poll(cx).map_err(Into::into),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A handle to the async runtime
|
||||
#[derive(Clone)]
|
||||
pub enum RuntimeHandle {
|
||||
/// The tokio handle.
|
||||
Tokio(TokioHandle),
|
||||
}
|
||||
|
||||
impl RuntimeHandle {
|
||||
/// Gets a reference to the [`TokioHandle`].
|
||||
pub fn inner(&self) -> &TokioHandle {
|
||||
let Self::Tokio(h) = self;
|
||||
h
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
/// Runs the provided function on an executor dedicated to blocking operations.
|
||||
///
|
||||
/// Use this instead of [`Self::spawn`] for code that blocks the thread it runs on
|
||||
/// (synchronous file or network I/O, heavy computation, FFI calls) so the async executor
|
||||
/// is not blocked. The returned [`JoinHandle`] resolves to the closure's return value.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use tauri::Manager;
|
||||
///
|
||||
/// tauri::Builder::default()
|
||||
/// .setup(|app| {
|
||||
/// let handle = app.handle().clone();
|
||||
/// tauri::async_runtime::handle().spawn_blocking(move || {
|
||||
/// let contents = std::fs::read_to_string("file.txt").unwrap_or_default();
|
||||
/// handle.package_info().name.len() + contents.len()
|
||||
/// });
|
||||
/// Ok(())
|
||||
/// });
|
||||
/// ```
|
||||
pub fn spawn_blocking<F, R>(&self, func: F) -> JoinHandle<R>
|
||||
where
|
||||
F: FnOnce() -> R + Send + 'static,
|
||||
R: Send + 'static,
|
||||
{
|
||||
match self {
|
||||
Self::Tokio(h) => JoinHandle::Tokio(h.spawn_blocking(func)),
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
/// Spawns a future onto the runtime.
|
||||
pub fn spawn<F>(&self, task: F) -> JoinHandle<F::Output>
|
||||
where
|
||||
F: Future + Send + 'static,
|
||||
F::Output: Send + 'static,
|
||||
{
|
||||
match self {
|
||||
Self::Tokio(h) => {
|
||||
let _guard = h.enter();
|
||||
JoinHandle::Tokio(tokio::spawn(task))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
/// Runs a future to completion on runtime.
|
||||
pub fn block_on<F: Future>(&self, task: F) -> F::Output {
|
||||
match self {
|
||||
Self::Tokio(h) => h.block_on(task),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_runtime() -> GlobalRuntime {
|
||||
let runtime = Runtime::Tokio(TokioRuntime::new().unwrap());
|
||||
let handle = runtime.handle();
|
||||
GlobalRuntime {
|
||||
runtime: Some(runtime),
|
||||
handle,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the runtime to use to execute asynchronous tasks.
|
||||
/// For convenience, this method takes a [`TokioHandle`].
|
||||
/// Note that you cannot drop the underlying [`TokioRuntime`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
/// // perform some async task before initializing the app
|
||||
/// do_something().await;
|
||||
/// // share the current runtime with Tauri
|
||||
/// tauri::async_runtime::set(tokio::runtime::Handle::current());
|
||||
///
|
||||
/// // bootstrap the tauri app...
|
||||
/// // tauri::Builder::default().run().unwrap();
|
||||
/// }
|
||||
///
|
||||
/// async fn do_something() {}
|
||||
/// ```
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the runtime is already set.
|
||||
pub fn set(handle: TokioHandle) {
|
||||
RUNTIME
|
||||
.set(GlobalRuntime {
|
||||
runtime: None,
|
||||
handle: RuntimeHandle::Tokio(handle),
|
||||
})
|
||||
.unwrap_or_else(|_| panic!("runtime already initialized"))
|
||||
}
|
||||
|
||||
/// Returns a handle of the async runtime.
|
||||
pub fn handle() -> RuntimeHandle {
|
||||
let runtime = RUNTIME.get_or_init(default_runtime);
|
||||
runtime.handle()
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
/// Runs a future to completion on runtime.
|
||||
pub fn block_on<F: Future>(task: F) -> F::Output {
|
||||
let runtime = RUNTIME.get_or_init(default_runtime);
|
||||
runtime.block_on(task)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
/// Spawns a future onto the runtime.
|
||||
pub fn spawn<F>(task: F) -> JoinHandle<F::Output>
|
||||
where
|
||||
F: Future + Send + 'static,
|
||||
F::Output: Send + 'static,
|
||||
{
|
||||
let runtime = RUNTIME.get_or_init(default_runtime);
|
||||
runtime.spawn(task)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
/// Runs the provided function on an executor dedicated to blocking operations.
|
||||
pub fn spawn_blocking<F, R>(func: F) -> JoinHandle<R>
|
||||
where
|
||||
F: FnOnce() -> R + Send + 'static,
|
||||
R: Send + 'static,
|
||||
{
|
||||
let runtime = RUNTIME.get_or_init(default_runtime);
|
||||
runtime.spawn_blocking(func)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn runtime_spawn() {
|
||||
let join = spawn(async { 5 });
|
||||
assert_eq!(join.await.unwrap(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_block_on() {
|
||||
assert_eq!(block_on(async { 0 }), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn handle_spawn() {
|
||||
let handle = handle();
|
||||
let join = handle.spawn(async { 5 });
|
||||
assert_eq!(join.await.unwrap(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handle_block_on() {
|
||||
let handle = handle();
|
||||
assert_eq!(handle.block_on(async { 0 }), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn handle_abort() {
|
||||
let handle = handle();
|
||||
let join = handle.spawn(async {
|
||||
// Here we sleep 1 second to ensure this task to be uncompleted when abort() invoked.
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
|
||||
5
|
||||
});
|
||||
join.abort();
|
||||
if let crate::Error::JoinError(raw_error) = join.await.unwrap_err() {
|
||||
assert!(raw_error.is_cancelled());
|
||||
} else {
|
||||
panic!("Abort did not result in the expected `JoinError`");
|
||||
}
|
||||
}
|
||||
}
|
||||
198
crates/tauri/src/error.rs
Normal file
198
crates/tauri/src/error.rs
Normal file
@@ -0,0 +1,198 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::fmt;
|
||||
|
||||
/// A generic boxed error.
|
||||
#[derive(Debug)]
|
||||
pub struct SetupError(Box<dyn std::error::Error>);
|
||||
|
||||
impl From<Box<dyn std::error::Error>> for SetupError {
|
||||
fn from(error: Box<dyn std::error::Error>) -> Self {
|
||||
Self(error)
|
||||
}
|
||||
}
|
||||
|
||||
// safety: the setup error is only used on the main thread
|
||||
// and we exit the process immediately.
|
||||
unsafe impl Send for SetupError {}
|
||||
unsafe impl Sync for SetupError {}
|
||||
|
||||
impl fmt::Display for SetupError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for SetupError {}
|
||||
|
||||
/// Runtime errors that can happen inside a Tauri application.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum Error {
|
||||
/// Runtime error.
|
||||
#[error("runtime error: {0}")]
|
||||
Runtime(#[from] tauri_runtime::Error),
|
||||
/// Window label must be unique.
|
||||
#[error("a window with label `{0}` already exists")]
|
||||
WindowLabelAlreadyExists(String),
|
||||
/// Webview label must be unique.
|
||||
#[error("a webview with label `{0}` already exists")]
|
||||
WebviewLabelAlreadyExists(String),
|
||||
/// Cannot use the webview reparent function on webview windows.
|
||||
#[error("cannot reparent when using a WebviewWindow")]
|
||||
CannotReparentWebviewWindow,
|
||||
/// Embedded asset not found.
|
||||
#[error("asset not found: {0}")]
|
||||
AssetNotFound(String),
|
||||
/// Failed to serialize/deserialize.
|
||||
#[error("JSON error: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
/// IO error.
|
||||
#[error("{0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
/// Failed to load window icon.
|
||||
#[error("invalid icon: {0}")]
|
||||
InvalidIcon(std::io::Error),
|
||||
/// Invalid args when running a command.
|
||||
#[error("invalid args `{1}` for command `{0}`: {2}")]
|
||||
InvalidArgs(&'static str, &'static str, serde_json::Error),
|
||||
/// Encountered an error in the setup hook,
|
||||
#[error("error encountered during setup hook: {0}")]
|
||||
Setup(SetupError),
|
||||
/// Error initializing plugin.
|
||||
#[error("failed to initialize plugin `{0}`: {1}")]
|
||||
PluginInitialization(String, String),
|
||||
/// A part of the URL is malformed or invalid. This may occur when parsing and combining
|
||||
/// user-provided URLs and paths.
|
||||
#[error("invalid url: {0}")]
|
||||
InvalidUrl(url::ParseError),
|
||||
/// Task join error.
|
||||
#[error(transparent)]
|
||||
JoinError(#[from] tokio::task::JoinError),
|
||||
/// An error happened inside the isolation pattern.
|
||||
#[cfg(feature = "isolation")]
|
||||
#[error("isolation pattern error: {0}")]
|
||||
IsolationPattern(#[from] tauri_utils::pattern::isolation::Error),
|
||||
/// An invalid window URL was provided. Includes details about the error.
|
||||
#[error("invalid window url: {0}")]
|
||||
InvalidWebviewUrl(&'static str),
|
||||
/// Invalid glob pattern.
|
||||
#[error("invalid glob pattern: {0}")]
|
||||
GlobPattern(#[from] glob::PatternError),
|
||||
/// Image error.
|
||||
#[cfg(any(feature = "image-png", feature = "image-ico"))]
|
||||
#[error("failed to process image: {0}")]
|
||||
Image(#[from] image::error::ImageError),
|
||||
/// The Window's raw handle is invalid for the platform.
|
||||
#[error("Unexpected `raw_window_handle` for the current platform")]
|
||||
InvalidWindowHandle,
|
||||
/// JNI error.
|
||||
#[cfg(target_os = "android")]
|
||||
#[error("jni error: {0}")]
|
||||
Jni(#[from] jni::errors::Error),
|
||||
/// Failed to receive message .
|
||||
#[error("failed to receive message")]
|
||||
FailedToReceiveMessage,
|
||||
/// Menu error.
|
||||
#[error("menu error: {0}")]
|
||||
#[cfg(desktop)]
|
||||
Menu(#[from] muda::Error),
|
||||
/// Bad menu icon error.
|
||||
#[error(transparent)]
|
||||
#[cfg(desktop)]
|
||||
BadMenuIcon(#[from] muda::BadIcon),
|
||||
/// Tray icon error.
|
||||
#[error("tray icon error: {0}")]
|
||||
#[cfg(all(desktop, feature = "tray-icon"))]
|
||||
#[cfg_attr(docsrs, doc(cfg(all(desktop, feature = "tray-icon"))))]
|
||||
Tray(#[from] tray_icon::Error),
|
||||
/// Bad tray icon error.
|
||||
#[error(transparent)]
|
||||
#[cfg(all(desktop, feature = "tray-icon"))]
|
||||
#[cfg_attr(docsrs, doc(cfg(all(desktop, feature = "tray-icon"))))]
|
||||
BadTrayIcon(#[from] tray_icon::BadIcon),
|
||||
/// Path does not have a parent.
|
||||
#[error("path does not have a parent")]
|
||||
NoParent,
|
||||
/// Path does not have an extension.
|
||||
#[error("path does not have an extension")]
|
||||
NoExtension,
|
||||
/// Path does not have a basename.
|
||||
#[error("path does not have a basename")]
|
||||
NoBasename,
|
||||
/// Path is not valid UTF-8 and cannot be represented in a URL.
|
||||
#[error("path is not valid UTF-8: {}", .0.display())]
|
||||
NonUtf8Path(std::path::PathBuf),
|
||||
/// Cannot resolve current directory.
|
||||
#[error("failed to read current dir: {0}")]
|
||||
CurrentDir(std::io::Error),
|
||||
/// Unknown path.
|
||||
#[cfg(not(target_os = "android"))]
|
||||
#[error("unknown path")]
|
||||
UnknownPath,
|
||||
/// Failed to invoke mobile plugin.
|
||||
#[cfg(target_os = "android")]
|
||||
#[error(transparent)]
|
||||
PluginInvoke(#[from] crate::plugin::mobile::PluginInvokeError),
|
||||
/// window not found.
|
||||
#[error("window not found")]
|
||||
WindowNotFound,
|
||||
/// The resource id is invalid.
|
||||
#[error("The resource id {0} is invalid.")]
|
||||
BadResourceId(crate::resources::ResourceId),
|
||||
/// The anyhow crate error.
|
||||
#[error(transparent)]
|
||||
Anyhow(#[from] anyhow::Error),
|
||||
/// webview not found.
|
||||
#[error("webview not found")]
|
||||
WebviewNotFound,
|
||||
/// API requires the unstable feature flag.
|
||||
#[error("this feature requires the `unstable` flag on Cargo.toml")]
|
||||
UnstableFeatureNotSupported,
|
||||
/// Failed to deserialize scope object.
|
||||
#[error("error deserializing scope: {0}")]
|
||||
CannotDeserializeScope(Box<dyn std::error::Error + Send + Sync>),
|
||||
/// Failed to get a raw handle.
|
||||
#[error(transparent)]
|
||||
RawHandleError(#[from] raw_window_handle::HandleError),
|
||||
/// Something went wrong with the CSPRNG.
|
||||
#[error("unable to generate random bytes from the operating system: {0}")]
|
||||
Csprng(getrandom::Error),
|
||||
/// Bad `__TAURI_INVOKE_KEY__` value received in ipc message.
|
||||
#[error("bad __TAURI_INVOKE_KEY__ value received in ipc message")]
|
||||
InvokeKey,
|
||||
/// Illegal event name.
|
||||
#[error("only alphanumeric, '-', '/', ':', '_' permitted for event names: {0:?}")]
|
||||
IllegalEventName(String),
|
||||
/// tokio oneshot channel failed to receive message
|
||||
#[error(transparent)]
|
||||
TokioOneshotRecv(#[from] tokio::sync::oneshot::error::RecvError),
|
||||
/// Unexpected menu kind passed to menu/tray plugin command
|
||||
#[error("Unexpected menu kind")]
|
||||
UnexpectedMenuKind,
|
||||
/// [`crate::image::Image::from_icon_resource`] failed
|
||||
#[cfg(windows)]
|
||||
#[cfg_attr(docsrs, doc(cfg(windows)))]
|
||||
#[error("Can not load Image from icon resources: {0}")]
|
||||
ImageFromResource(windows::core::Error),
|
||||
}
|
||||
|
||||
impl From<getrandom::Error> for Error {
|
||||
fn from(value: getrandom::Error) -> Self {
|
||||
Self::Csprng(value)
|
||||
}
|
||||
}
|
||||
|
||||
/// `Result<T, ::tauri::Error>`
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn error_is_send_sync() {
|
||||
crate::test_utils::assert_send::<super::Error>();
|
||||
crate::test_utils::assert_sync::<super::Error>();
|
||||
}
|
||||
}
|
||||
34
crates/tauri/src/event/event-system-spec.md
Normal file
34
crates/tauri/src/event/event-system-spec.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Tauri Event System Specification
|
||||
|
||||
## Emitters
|
||||
|
||||
Emitters can emit to any and all listeners.
|
||||
|
||||
- `App` and `AppHandle`
|
||||
- `Window`
|
||||
- `Webview`
|
||||
- `WebviewWindow`
|
||||
- Any type that implements `Manager` trait.
|
||||
|
||||
## Emit functions
|
||||
|
||||
- `emit`: emits an event to all listeners.
|
||||
- `emit_to`: emits an event to a specified target.
|
||||
- `emit_filter`: emits an event to targets based on a filtering callback.
|
||||
|
||||
## Listeners
|
||||
|
||||
Emitters can emit to any and all listeners.
|
||||
|
||||
- `App` and `AppHandle`
|
||||
- `Window`
|
||||
- `Webview`
|
||||
- `WebviewWindow`
|
||||
- Any type that implements `Manager` trait but is limited to only using `listen_any/once_any`.
|
||||
|
||||
## Listen functions
|
||||
|
||||
- `listen`: Listens to all events targeting this listener type only.
|
||||
- `once`: Listens to a single event targeting this listener type only.
|
||||
- `listen_any` (available only through `Manager` trait): Listens to all events to any target (aka event sniffer).
|
||||
- `once_any` (available only through `Manager` trait): Listens to a single event to any target (aka event sniffer).
|
||||
70
crates/tauri/src/event/event_name.rs
Normal file
70
crates/tauri/src/event/event_name.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use serde::{Deserialize, Deserializer};
|
||||
|
||||
/// Checks if an event name is valid.
|
||||
fn is_event_name_valid(event: &str) -> bool {
|
||||
event
|
||||
.chars()
|
||||
.all(|c| c.is_alphanumeric() || c == '-' || c == '/' || c == ':' || c == '_')
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub(crate) struct EventName<S = String>(S);
|
||||
|
||||
impl Copy for EventName<&str> {}
|
||||
|
||||
impl<S: AsRef<str>> EventName<S> {
|
||||
pub(crate) fn new(s: S) -> crate::Result<EventName<S>> {
|
||||
if !is_event_name_valid(s.as_ref()) {
|
||||
return Err(crate::Error::IllegalEventName(s.as_ref().to_string()));
|
||||
}
|
||||
Ok(EventName(s))
|
||||
}
|
||||
|
||||
pub(crate) fn as_str_event(&self) -> EventName<&str> {
|
||||
EventName(self.0.as_ref())
|
||||
}
|
||||
|
||||
pub(crate) fn as_str(&self) -> &str {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: std::fmt::Display> std::fmt::Display for EventName<S> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl EventName<&'static str> {
|
||||
// this convenience method is for using in const contexts to discharge the preconditions
|
||||
// &'static prevents using this function accidentally with dynamically built string slices
|
||||
pub(crate) const fn from_str(s: &'static str) -> EventName<&'static str> {
|
||||
EventName(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl EventName<&str> {
|
||||
pub fn into_owned(self) -> EventName {
|
||||
EventName(self.0.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for EventName {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let event_id = String::deserialize(deserializer)?;
|
||||
if is_event_name_valid(&event_id) {
|
||||
Ok(EventName(event_id))
|
||||
} else {
|
||||
Err(serde::de::Error::custom(
|
||||
"Event name must include only alphanumeric characters, `-`, `/`, `:` and `_`.",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
10
crates/tauri/src/event/init.js
Normal file
10
crates/tauri/src/event/init.js
Normal file
@@ -0,0 +1,10 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// eslint-disable-next-line
|
||||
Object.defineProperty(window, '__TAURI_EVENT_PLUGIN_INTERNALS__', {
|
||||
value: {
|
||||
unregisterListener: __RAW_unregister_listener_function__
|
||||
}
|
||||
})
|
||||
488
crates/tauri/src/event/listener.rs
Normal file
488
crates/tauri/src/event/listener.rs
Normal file
@@ -0,0 +1,488 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use crate::{Runtime, Webview};
|
||||
|
||||
use super::{EmitArgs, Event, EventId, EventTarget};
|
||||
|
||||
use std::{
|
||||
boxed::Box,
|
||||
cell::Cell,
|
||||
collections::{HashMap, HashSet},
|
||||
sync::{
|
||||
Arc, Mutex,
|
||||
atomic::{AtomicU32, Ordering},
|
||||
},
|
||||
};
|
||||
|
||||
/// What to do with the pending handler when resolving it?
|
||||
enum Pending {
|
||||
Unlisten(EventId),
|
||||
Listen {
|
||||
id: EventId,
|
||||
event: crate::EventName,
|
||||
handler: Handler,
|
||||
},
|
||||
Emit(EmitArgs),
|
||||
RemoveForTarget(EventTarget),
|
||||
}
|
||||
|
||||
/// Stored in [`Listeners`] to be called upon, when the event that stored it, is triggered.
|
||||
struct Handler {
|
||||
target: EventTarget,
|
||||
callback: Box<dyn Fn(Event) + Send>,
|
||||
}
|
||||
|
||||
impl Handler {
|
||||
fn new<F: Fn(Event) + Send + 'static>(target: EventTarget, callback: F) -> Self {
|
||||
Self {
|
||||
target,
|
||||
callback: Box::new(callback),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
struct JsHandler {
|
||||
target: EventTarget,
|
||||
id: EventId,
|
||||
}
|
||||
|
||||
impl JsHandler {
|
||||
fn new(target: EventTarget, id: EventId) -> Self {
|
||||
Self { target, id }
|
||||
}
|
||||
}
|
||||
|
||||
type WebviewLabel = String;
|
||||
|
||||
/// Holds event handlers and pending event handlers, along with the salts associating them.
|
||||
struct InnerListeners {
|
||||
pending: Mutex<Vec<Pending>>,
|
||||
handlers: Mutex<HashMap<crate::EventName, HashMap<EventId, Handler>>>,
|
||||
js_event_listeners: Mutex<HashMap<WebviewLabel, HashMap<crate::EventName, HashSet<JsHandler>>>>,
|
||||
function_name: &'static str,
|
||||
listeners_object_name: &'static str,
|
||||
next_event_id: Arc<AtomicU32>,
|
||||
}
|
||||
|
||||
/// A self-contained event manager.
|
||||
#[derive(Clone)]
|
||||
pub struct Listeners {
|
||||
inner: Arc<InnerListeners>,
|
||||
}
|
||||
|
||||
impl Default for Listeners {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
inner: Arc::new(InnerListeners {
|
||||
pending: Mutex::default(),
|
||||
handlers: Mutex::default(),
|
||||
js_event_listeners: Mutex::default(),
|
||||
function_name: "__internal_unstable_listeners_function_id__",
|
||||
listeners_object_name: "__internal_unstable_listeners_object_id__",
|
||||
next_event_id: Default::default(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Listeners {
|
||||
pub(crate) fn next_event_id(&self) -> EventId {
|
||||
self.inner.next_event_id.fetch_add(1, Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Function name to represent the JavaScript event function.
|
||||
pub(crate) fn function_name(&self) -> &str {
|
||||
self.inner.function_name
|
||||
}
|
||||
|
||||
/// Listener object name to represent the JavaScript event listener object.
|
||||
pub(crate) fn listeners_object_name(&self) -> &str {
|
||||
self.inner.listeners_object_name
|
||||
}
|
||||
|
||||
/// Insert a pending event action to the queue.
|
||||
fn insert_pending(&self, action: Pending) {
|
||||
self
|
||||
.inner
|
||||
.pending
|
||||
.lock()
|
||||
.expect("poisoned pending event queue")
|
||||
.push(action)
|
||||
}
|
||||
|
||||
/// Finish all pending event actions.
|
||||
fn flush_pending(&self) -> crate::Result<()> {
|
||||
let pending = {
|
||||
let mut lock = self
|
||||
.inner
|
||||
.pending
|
||||
.lock()
|
||||
.expect("poisoned pending event queue");
|
||||
std::mem::take(&mut *lock)
|
||||
};
|
||||
|
||||
for action in pending {
|
||||
match action {
|
||||
Pending::Unlisten(id) => self.unlisten(id),
|
||||
Pending::Listen { id, event, handler } => self.listen_with_id(id, event, handler),
|
||||
Pending::Emit(args) => self.emit(args)?,
|
||||
Pending::RemoveForTarget(target) => self.remove_listeners_for_target(target),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn listen_with_id(&self, id: EventId, event: crate::EventName, handler: Handler) {
|
||||
match self.inner.handlers.try_lock() {
|
||||
Err(_) => self.insert_pending(Pending::Listen { id, event, handler }),
|
||||
Ok(mut lock) => {
|
||||
lock.entry(event).or_default().insert(id, handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds an event listener.
|
||||
pub(crate) fn listen<F: Fn(Event) + Send + 'static>(
|
||||
&self,
|
||||
event: crate::EventName,
|
||||
target: EventTarget,
|
||||
handler: F,
|
||||
) -> EventId {
|
||||
let id = self.next_event_id();
|
||||
let handler = Handler::new(target, handler);
|
||||
self.listen_with_id(id, event, handler);
|
||||
id
|
||||
}
|
||||
|
||||
/// Listen to an event and immediately unlisten.
|
||||
pub(crate) fn once<F: FnOnce(Event) + Send + 'static>(
|
||||
&self,
|
||||
event: crate::EventName,
|
||||
target: EventTarget,
|
||||
handler: F,
|
||||
) -> EventId {
|
||||
let self_ = self.clone();
|
||||
let handler = Cell::new(Some(handler));
|
||||
|
||||
self.listen(event, target, move |event| {
|
||||
let id = event.id;
|
||||
self_.unlisten(id);
|
||||
let handler = handler
|
||||
.take()
|
||||
.expect("attempted to call handler more than once");
|
||||
handler(event);
|
||||
})
|
||||
}
|
||||
|
||||
/// Removes an event listener.
|
||||
pub(crate) fn unlisten(&self, id: EventId) {
|
||||
match self.inner.handlers.try_lock() {
|
||||
Err(_) => self.insert_pending(Pending::Unlisten(id)),
|
||||
Ok(mut lock) => lock.values_mut().for_each(|handler| {
|
||||
handler.remove(&id);
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes every Rust-side event listener registered for the exact given target.
|
||||
///
|
||||
/// Called when a window or webview is destroyed so its listeners don't leak.
|
||||
/// Global targets such as [`EventTarget::App`], [`EventTarget::Any`] and [`EventTarget::AnyLabel`] are never matched here.
|
||||
fn remove_listeners_for_target(&self, target: EventTarget) {
|
||||
match self.inner.handlers.try_lock() {
|
||||
Err(_) => self.insert_pending(Pending::RemoveForTarget(target)),
|
||||
Ok(mut lock) => {
|
||||
for handlers in lock.values_mut() {
|
||||
handlers.retain(|_, handler| handler.target != target);
|
||||
}
|
||||
lock.retain(|_, handlers| !handlers.is_empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Emits the given event with its payload based on a filter.
|
||||
pub(crate) fn emit_filter<F>(&self, emit_args: EmitArgs, filter: Option<F>) -> crate::Result<()>
|
||||
where
|
||||
F: Fn(&EventTarget) -> bool,
|
||||
{
|
||||
let mut maybe_pending = false;
|
||||
|
||||
match self.inner.handlers.try_lock() {
|
||||
Err(_) => self.insert_pending(Pending::Emit(emit_args)),
|
||||
Ok(lock) => {
|
||||
if let Some(handlers) = lock.get(&emit_args.event) {
|
||||
let handlers = handlers.iter();
|
||||
let handlers = handlers.filter(|(_, h)| match_any_or_filter(&h.target, &filter));
|
||||
for (&id, Handler { callback, .. }) in handlers {
|
||||
maybe_pending = true;
|
||||
(callback)(Event::new(id, emit_args.payload.clone()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if maybe_pending {
|
||||
self.flush_pending()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Emits the given event with its payload.
|
||||
pub(crate) fn emit(&self, emit_args: EmitArgs) -> crate::Result<()> {
|
||||
self.emit_filter(emit_args, None::<&dyn Fn(&EventTarget) -> bool>)
|
||||
}
|
||||
|
||||
pub(crate) fn listen_js(
|
||||
&self,
|
||||
event: crate::EventName<&str>,
|
||||
source_webview_label: &str,
|
||||
target: EventTarget,
|
||||
id: EventId,
|
||||
) {
|
||||
let event = event.into_owned();
|
||||
let mut listeners = self.inner.js_event_listeners.lock().unwrap();
|
||||
listeners
|
||||
.entry(source_webview_label.to_string())
|
||||
.or_default()
|
||||
.entry(event)
|
||||
.or_default()
|
||||
.insert(JsHandler::new(target, id));
|
||||
}
|
||||
|
||||
pub(crate) fn unlisten_js(&self, event: crate::EventName<&str>, id: EventId) {
|
||||
let event = event.into_owned();
|
||||
let mut js_listeners = self.inner.js_event_listeners.lock().unwrap();
|
||||
let js_listeners = js_listeners.values_mut();
|
||||
for js_listeners in js_listeners {
|
||||
if let Some(handlers) = js_listeners.get_mut(&event) {
|
||||
handlers.retain(|h| h.id != id);
|
||||
|
||||
if handlers.is_empty() {
|
||||
js_listeners.remove(&event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes all JS event listeners registered from the given webview.
|
||||
fn remove_webview_js_listeners(&self, webview_label: &str) {
|
||||
self
|
||||
.inner
|
||||
.js_event_listeners
|
||||
.lock()
|
||||
.unwrap()
|
||||
.remove(webview_label);
|
||||
}
|
||||
|
||||
/// Removes all event listeners associated with a destroyed webview.
|
||||
pub(crate) fn remove_webview_listeners(&self, label: &str) {
|
||||
self.remove_webview_js_listeners(label);
|
||||
self.remove_listeners_for_target(EventTarget::webview(label));
|
||||
}
|
||||
|
||||
/// Removes all event listeners associated with a destroyed window.
|
||||
pub(crate) fn remove_window_listeners(&self, label: &str) {
|
||||
self.remove_listeners_for_target(EventTarget::window(label));
|
||||
self.remove_listeners_for_target(EventTarget::webview_window(label));
|
||||
}
|
||||
|
||||
pub(crate) fn has_js_listener<F: Fn(&EventTarget) -> bool>(
|
||||
&self,
|
||||
event: crate::EventName<&str>,
|
||||
filter: F,
|
||||
) -> bool {
|
||||
let event = event.into_owned();
|
||||
let js_listeners = self.inner.js_event_listeners.lock().unwrap();
|
||||
js_listeners.values().any(|events| {
|
||||
events
|
||||
.get(&event)
|
||||
.map(|handlers| handlers.iter().any(|handler| filter(&handler.target)))
|
||||
.unwrap_or(false)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn emit_js_filter<'a, R, I, F>(
|
||||
&self,
|
||||
mut webviews: I,
|
||||
emit_args: &EmitArgs,
|
||||
filter: Option<F>,
|
||||
) -> crate::Result<()>
|
||||
where
|
||||
R: Runtime,
|
||||
I: Iterator<Item = &'a Webview<R>>,
|
||||
F: Fn(&EventTarget) -> bool,
|
||||
{
|
||||
let event = &emit_args.event;
|
||||
let js_listeners = self.inner.js_event_listeners.lock().unwrap();
|
||||
webviews.try_for_each(|webview| {
|
||||
if let Some(handlers) = js_listeners.get(webview.label()).and_then(|s| s.get(event)) {
|
||||
let ids = handlers
|
||||
.iter()
|
||||
.filter(|handler| match_any_or_filter(&handler.target, &filter))
|
||||
.map(|handler| handler.id)
|
||||
.collect::<Vec<_>>();
|
||||
webview.emit_js(emit_args, &ids)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn emit_js<'a, R, I>(&self, webviews: I, emit_args: &EmitArgs) -> crate::Result<()>
|
||||
where
|
||||
R: Runtime,
|
||||
I: Iterator<Item = &'a Webview<R>>,
|
||||
{
|
||||
self.emit_js_filter(webviews, emit_args, None::<&dyn Fn(&EventTarget) -> bool>)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn match_any_or_filter<F: Fn(&EventTarget) -> bool>(
|
||||
target: &EventTarget,
|
||||
filter: &Option<F>,
|
||||
) -> bool {
|
||||
*target == EventTarget::Any || filter.as_ref().map(|f| f(target)).unwrap_or(true)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::event::EventTarget;
|
||||
use proptest::prelude::*;
|
||||
|
||||
// dummy event handler function
|
||||
fn event_fn(s: Event) {
|
||||
println!("{s:?}");
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#![proptest_config(ProptestConfig::with_cases(10000))]
|
||||
|
||||
// check to see if listen() is properly passing keys into the LISTENERS map
|
||||
#[test]
|
||||
fn listeners_check_key(e in "[a-z]+") {
|
||||
let listeners: Listeners = Default::default();
|
||||
// clone e as the key
|
||||
let key = crate::EventName::new(e).unwrap();
|
||||
// pass e and an dummy func into listen
|
||||
listeners.listen(key.clone(), EventTarget::Any, event_fn);
|
||||
|
||||
// lock mutex
|
||||
let l = listeners.inner.handlers.lock().unwrap();
|
||||
|
||||
// check if the generated key is in the map
|
||||
assert!(l.contains_key(&key));
|
||||
}
|
||||
|
||||
// check to see if listen inputs a handler function properly into the LISTENERS map.
|
||||
#[test]
|
||||
fn listeners_check_fn(e in "[a-z]+") {
|
||||
let listeners: Listeners = Default::default();
|
||||
let key = crate::EventName::new(e).unwrap();
|
||||
// pass e and an dummy func into listen
|
||||
listeners.listen(key.clone(), EventTarget::Any, event_fn);
|
||||
|
||||
// lock mutex
|
||||
let mut l = listeners.inner.handlers.lock().unwrap();
|
||||
|
||||
// check if l contains key
|
||||
if l.contains_key(&key) {
|
||||
// grab key if it exists
|
||||
let handler = l.get_mut(&key);
|
||||
// check to see if we get back a handler or not
|
||||
match handler {
|
||||
// pass on Some(handler)
|
||||
Some(_) => {},
|
||||
// Fail on None
|
||||
None => panic!("handler is None")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check to see if on_event properly grabs the stored function from listen.
|
||||
#[test]
|
||||
fn check_on_event(e in "[a-z]+", d in "[a-z]+") {
|
||||
let listeners: Listeners = Default::default();
|
||||
let key = crate::EventName::new(e).unwrap();
|
||||
// call listen with key and the event_fn dummy func
|
||||
listeners.listen(key.clone(), EventTarget::Any, event_fn);
|
||||
// call on event with key and d.
|
||||
listeners.emit(EmitArgs::new(key.as_str_event(), &d)?)?;
|
||||
|
||||
// lock the mutex
|
||||
let l = listeners.inner.handlers.lock().unwrap();
|
||||
|
||||
// assert that the key is contained in the listeners map
|
||||
assert!(l.contains_key(&key));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn event_no_deadlocks() {
|
||||
let listeners = Listeners::default();
|
||||
let listeners_clone = listeners.clone();
|
||||
let event = crate::EventName::new("test-event".to_owned()).unwrap();
|
||||
let event_clone = event.clone();
|
||||
listeners.once(event.clone(), EventTarget::Any, move |_event| {
|
||||
listeners_clone
|
||||
.emit(EmitArgs::new(event_clone.as_str_event(), &()).unwrap())
|
||||
.unwrap();
|
||||
});
|
||||
listeners
|
||||
.emit(EmitArgs::new(event.as_str_event(), &()).unwrap())
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn listeners_removed_on_webview_close() {
|
||||
let listeners = Listeners::default();
|
||||
let event = crate::EventName::new("test-event".to_owned()).unwrap();
|
||||
let webview_label = "main";
|
||||
|
||||
let window = listeners.listen(event.clone(), EventTarget::window(webview_label), event_fn);
|
||||
// Same label as the window, but a different target kind (multi-webview mode).
|
||||
let webview = listeners.listen(event.clone(), EventTarget::webview(webview_label), event_fn);
|
||||
let webview_window = listeners.listen(
|
||||
event.clone(),
|
||||
EventTarget::webview_window(webview_label),
|
||||
event_fn,
|
||||
);
|
||||
let other = listeners.listen(event.clone(), EventTarget::webview("other"), event_fn);
|
||||
let app = listeners.listen(event.clone(), EventTarget::App, event_fn);
|
||||
let any = listeners.listen(event.clone(), EventTarget::Any, event_fn);
|
||||
let labeled = listeners.listen(event.clone(), EventTarget::labeled(webview_label), event_fn);
|
||||
|
||||
let id = listeners.next_event_id();
|
||||
listeners.listen_js(
|
||||
event.as_str_event(),
|
||||
webview_label,
|
||||
EventTarget::webview(webview_label),
|
||||
id,
|
||||
);
|
||||
assert!(listeners.has_js_listener(event.as_str_event(), |_| true));
|
||||
|
||||
listeners.remove_webview_listeners(webview_label);
|
||||
|
||||
assert!(!listeners.has_js_listener(event.as_str_event(), |_| true));
|
||||
|
||||
let lock = listeners.inner.handlers.lock().unwrap();
|
||||
let remaining: HashSet<EventId> = lock.get(&event).unwrap().keys().copied().collect();
|
||||
|
||||
// only the exact `Webview { label: "main" }` listener is gone
|
||||
assert!(!remaining.contains(&webview));
|
||||
// same label but different kind is kept
|
||||
assert!(remaining.contains(&window));
|
||||
assert!(remaining.contains(&webview_window));
|
||||
// unrelated and global listeners are kept
|
||||
assert!(remaining.contains(&other));
|
||||
assert!(remaining.contains(&app));
|
||||
assert!(remaining.contains(&any));
|
||||
assert!(remaining.contains(&labeled));
|
||||
}
|
||||
}
|
||||
258
crates/tauri/src/event/mod.rs
Normal file
258
crates/tauri/src/event/mod.rs
Normal file
@@ -0,0 +1,258 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
mod listener;
|
||||
pub(crate) mod plugin;
|
||||
use std::{convert::Infallible, str::FromStr};
|
||||
|
||||
pub(crate) use listener::Listeners;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
mod event_name;
|
||||
|
||||
pub(crate) use event_name::EventName;
|
||||
|
||||
use crate::ipc::CallbackFn;
|
||||
|
||||
/// Unique id of an event.
|
||||
pub type EventId = u32;
|
||||
|
||||
/// Event Target
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
|
||||
#[serde(tag = "kind")]
|
||||
#[non_exhaustive]
|
||||
pub enum EventTarget {
|
||||
/// Any and all event targets.
|
||||
Any,
|
||||
|
||||
/// Any [`Window`](crate::Window), [`Webview`](crate::Webview) or [`WebviewWindow`](crate::WebviewWindow) that have this label.
|
||||
AnyLabel {
|
||||
/// Target label.
|
||||
label: String,
|
||||
},
|
||||
|
||||
/// [`App`](crate::App) and [`AppHandle`](crate::AppHandle) targets.
|
||||
App,
|
||||
|
||||
/// [`Window`](crate::Window) target.
|
||||
Window {
|
||||
/// window label.
|
||||
label: String,
|
||||
},
|
||||
|
||||
/// [`Webview`](crate::Webview) target.
|
||||
Webview {
|
||||
/// webview label.
|
||||
label: String,
|
||||
},
|
||||
|
||||
/// [`WebviewWindow`](crate::WebviewWindow) target.
|
||||
WebviewWindow {
|
||||
/// webview window label.
|
||||
label: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl EventTarget {
|
||||
/// [`Self::Any`] target.
|
||||
pub fn any() -> Self {
|
||||
Self::Any
|
||||
}
|
||||
|
||||
/// [`Self::App`] target.
|
||||
pub fn app() -> Self {
|
||||
Self::App
|
||||
}
|
||||
|
||||
/// [`Self::AnyLabel`] target.
|
||||
pub fn labeled(label: impl Into<String>) -> Self {
|
||||
Self::AnyLabel {
|
||||
label: label.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Self::Window`] target.
|
||||
pub fn window(label: impl Into<String>) -> Self {
|
||||
Self::Window {
|
||||
label: label.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Self::Webview`] target.
|
||||
pub fn webview(label: impl Into<String>) -> Self {
|
||||
Self::Webview {
|
||||
label: label.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Self::WebviewWindow`] target.
|
||||
pub fn webview_window(label: impl Into<String>) -> Self {
|
||||
Self::WebviewWindow {
|
||||
label: label.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AsRef<str>> From<T> for EventTarget {
|
||||
fn from(value: T) -> Self {
|
||||
Self::AnyLabel {
|
||||
label: value.as_ref().to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for EventTarget {
|
||||
type Err = Infallible;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(Self::AnyLabel {
|
||||
label: s.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Serialized emit arguments.
|
||||
#[derive(Clone)]
|
||||
pub struct EmitArgs {
|
||||
/// event name.
|
||||
event: EventName,
|
||||
/// Serialized payload.
|
||||
payload: String,
|
||||
}
|
||||
|
||||
impl EmitArgs {
|
||||
pub fn new<S: Serialize>(event: EventName<&str>, payload: &S) -> crate::Result<Self> {
|
||||
#[cfg(feature = "tracing")]
|
||||
let _span = tracing::debug_span!("window::emit::serialize").entered();
|
||||
Ok(EmitArgs {
|
||||
event: event.into_owned(),
|
||||
payload: serde_json::to_string(payload)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_str(event: EventName<&str>, payload: String) -> crate::Result<Self> {
|
||||
#[cfg(feature = "tracing")]
|
||||
let _span = tracing::debug_span!("window::emit::json").entered();
|
||||
Ok(EmitArgs {
|
||||
event: event.into_owned(),
|
||||
payload,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// An event that was emitted.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Event {
|
||||
id: EventId,
|
||||
data: String,
|
||||
}
|
||||
|
||||
impl Event {
|
||||
fn new(id: EventId, data: String) -> Self {
|
||||
Self { id, data }
|
||||
}
|
||||
|
||||
/// The [`EventId`] of the handler that was triggered.
|
||||
pub fn id(&self) -> EventId {
|
||||
self.id
|
||||
}
|
||||
|
||||
/// The event payload.
|
||||
pub fn payload(&self) -> &str {
|
||||
&self.data
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn listen_js_script(
|
||||
listeners_object_name: &str,
|
||||
serialized_target: &str,
|
||||
event: EventName<&str>,
|
||||
event_id: EventId,
|
||||
handler: CallbackFn,
|
||||
) -> String {
|
||||
let handler_id = handler.0;
|
||||
format!(
|
||||
"(function () {{
|
||||
if (window['{listeners_object_name}'] === void 0) {{
|
||||
Object.defineProperty(window, '{listeners_object_name}', {{ value: Object.create(null) }});
|
||||
}}
|
||||
if (window['{listeners_object_name}']['{event}'] === void 0) {{
|
||||
Object.defineProperty(window['{listeners_object_name}'], '{event}', {{ value: Object.create(null) }});
|
||||
}}
|
||||
const eventListeners = window['{listeners_object_name}']['{event}']
|
||||
const listener = {{
|
||||
target: {serialized_target},
|
||||
handlerId: {handler_id}
|
||||
}};
|
||||
Object.defineProperty(eventListeners, '{event_id}', {{ value: listener, configurable: true }});
|
||||
}})()
|
||||
",
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn emit_js_script(
|
||||
event_emit_function_name: &str,
|
||||
emit_args: &EmitArgs,
|
||||
serialized_ids: &str,
|
||||
) -> crate::Result<String> {
|
||||
Ok(format!(
|
||||
"(function () {{ const fn = window['{}']; fn && fn({{event: '{}', payload: {}}}, {ids}) }})()",
|
||||
event_emit_function_name,
|
||||
emit_args.event,
|
||||
emit_args.payload,
|
||||
ids = serialized_ids,
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn unlisten_js_script(
|
||||
listeners_object_name: &str,
|
||||
event_arg: &str,
|
||||
event_id_arg: &str,
|
||||
) -> String {
|
||||
format!(
|
||||
"(function () {{
|
||||
const listeners = (window['{listeners_object_name}'] || {{}})[{event_arg}]
|
||||
if (listeners) {{
|
||||
const listener = listeners[{event_id_arg}]
|
||||
if (listener) {{
|
||||
window.__TAURI_INTERNALS__.unregisterCallback(listener.handlerId)
|
||||
}}
|
||||
}}
|
||||
}})()
|
||||
",
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn event_initialization_script(function_name: &str, listeners: &str) -> String {
|
||||
format!(
|
||||
"Object.defineProperty(window, '{function_name}', {{
|
||||
value: function (eventData, ids) {{
|
||||
const listeners = (window['{listeners}'] && window['{listeners}'][eventData.event]) || []
|
||||
for (const id of ids) {{
|
||||
const listener = listeners[id]
|
||||
if (listener) {{
|
||||
eventData.id = id
|
||||
window.__TAURI_INTERNALS__.runCallback(listener.handlerId, eventData)
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
}});
|
||||
"
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn test_illegal_event_name() {
|
||||
let s = EventName::new("some\r illegal event name")
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert_eq!(
|
||||
"only alphanumeric, '-', '/', ':', '_' permitted for event names: \"some\\r illegal event name\"",
|
||||
s
|
||||
);
|
||||
}
|
||||
}
|
||||
82
crates/tauri/src/event/plugin.rs
Normal file
82
crates/tauri/src/event/plugin.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
use serde_json::Value as JsonValue;
|
||||
use serialize_to_javascript::{DefaultTemplate, Template, default_template};
|
||||
|
||||
use crate::plugin::{Builder, TauriPlugin};
|
||||
use crate::{AppHandle, Emitter, Manager, Webview};
|
||||
use crate::{EventId, Result, Runtime, command, ipc::CallbackFn};
|
||||
|
||||
use super::EventName;
|
||||
use super::EventTarget;
|
||||
|
||||
#[command(root = "crate")]
|
||||
async fn listen<R: Runtime>(
|
||||
webview: Webview<R>,
|
||||
event: EventName,
|
||||
target: EventTarget,
|
||||
handler: CallbackFn,
|
||||
) -> Result<EventId> {
|
||||
webview.listen_js(event.as_str_event(), target, handler)
|
||||
}
|
||||
|
||||
#[command(root = "crate")]
|
||||
async fn unlisten<R: Runtime>(
|
||||
webview: Webview<R>,
|
||||
event: EventName,
|
||||
event_id: EventId,
|
||||
) -> Result<()> {
|
||||
webview.unlisten_js(event.as_str_event(), event_id)
|
||||
}
|
||||
|
||||
#[command(root = "crate")]
|
||||
async fn emit<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
event: EventName,
|
||||
payload: Option<JsonValue>,
|
||||
) -> Result<()> {
|
||||
app.emit(event.as_str(), payload)
|
||||
}
|
||||
|
||||
#[command(root = "crate")]
|
||||
async fn emit_to<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
target: EventTarget,
|
||||
event: EventName,
|
||||
payload: Option<JsonValue>,
|
||||
) -> Result<()> {
|
||||
app.emit_to(target, event.as_str(), payload)
|
||||
}
|
||||
|
||||
/// Initializes the event plugin.
|
||||
pub(crate) fn init<R: Runtime, M: Manager<R>>(manager: &M) -> TauriPlugin<R> {
|
||||
let listeners = manager.manager().listeners();
|
||||
|
||||
#[derive(Template)]
|
||||
#[default_template("./init.js")]
|
||||
struct InitJavascript {
|
||||
#[raw]
|
||||
unregister_listener_function: String,
|
||||
}
|
||||
|
||||
let init_script = InitJavascript {
|
||||
unregister_listener_function: format!(
|
||||
"(event, eventId) => {}",
|
||||
crate::event::unlisten_js_script(listeners.listeners_object_name(), "event", "eventId")
|
||||
),
|
||||
};
|
||||
|
||||
Builder::new("event")
|
||||
.invoke_handler(crate::generate_handler![
|
||||
#![plugin(event)]
|
||||
listen, unlisten, emit, emit_to
|
||||
])
|
||||
.js_init_script(
|
||||
init_script
|
||||
.render_default(&Default::default())
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
)
|
||||
.build()
|
||||
}
|
||||
527
crates/tauri/src/image/mod.rs
Normal file
527
crates/tauri/src/image/mod.rs
Normal file
@@ -0,0 +1,527 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! Image types used by this crate and also referenced by the JavaScript API layer.
|
||||
|
||||
pub(crate) mod plugin;
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg(windows)]
|
||||
use windows::{
|
||||
Win32::{
|
||||
Foundation::{E_FAIL, ERROR_INVALID_PARAMETER, ERROR_NOT_SUPPORTED, WIN32_ERROR},
|
||||
Graphics::Gdi::{
|
||||
BI_RGB, BITMAPINFO, BITMAPINFOHEADER, CreateCompatibleDC, DIB_RGB_COLORS, DeleteDC,
|
||||
GetDIBits, HBITMAP,
|
||||
},
|
||||
System::LibraryLoader::GetModuleHandleW,
|
||||
UI::WindowsAndMessaging::{
|
||||
GetIconInfo, GetSystemMetrics, HICON, ICONINFO, IMAGE_ICON, LR_DEFAULTCOLOR, LoadImageW,
|
||||
SM_CXICON, SM_CYICON,
|
||||
},
|
||||
},
|
||||
core::{Owned, PCWSTR},
|
||||
};
|
||||
|
||||
use crate::{Resource, ResourceId, ResourceTable};
|
||||
|
||||
/// Identifies an icon resource embedded in the executable.
|
||||
#[cfg(windows)]
|
||||
#[cfg_attr(docsrs, doc(cfg(windows)))]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum IconResource<'a> {
|
||||
/// An integer resource identifier (`MAKEINTRESOURCE`).
|
||||
Id(u16),
|
||||
/// A string resource name.
|
||||
Name(&'a str),
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
impl From<u16> for IconResource<'_> {
|
||||
fn from(id: u16) -> Self {
|
||||
Self::Id(id)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
impl<'a> From<&'a str> for IconResource<'a> {
|
||||
fn from(name: &'a str) -> Self {
|
||||
Self::Name(name)
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads the default window icon from the application icon resource, reporting failures.
|
||||
///
|
||||
/// Used by [`crate::generate_context!`]; not public API.
|
||||
#[cfg(windows)]
|
||||
#[doc(hidden)]
|
||||
pub fn default_window_icon_from_app_icon_resource() -> Option<Image<'static>> {
|
||||
// the window icon is drawn in the title bar and, as a fallback for the taskbar icon,
|
||||
// at the system's large icon size (32x32 at 96 DPI, scaled with the system DPI),
|
||||
// so pick the entry Windows would use for the taskbar instead of a larger one it has to shrink
|
||||
// `GetSystemMetrics` returns 0 on failure
|
||||
let metric = |index| match unsafe { GetSystemMetrics(index) } {
|
||||
n if n > 0 => n as u32,
|
||||
_ => 32,
|
||||
};
|
||||
let (width, height) = (metric(SM_CXICON), metric(SM_CYICON));
|
||||
match Image::from_icon_resource(
|
||||
crate::utils::platform::WINDOWS_APP_ICON_RESOURCE_ID,
|
||||
width,
|
||||
height,
|
||||
) {
|
||||
Ok(icon) => Some(icon),
|
||||
Err(e) => {
|
||||
// a logger is usually not installed yet when `generate_context!` runs
|
||||
#[cfg(debug_assertions)]
|
||||
eprintln!("failed to load the default window icon from the application icon resource: {e}");
|
||||
log::warn!("failed to load the default window icon from the application icon resource: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
const BYTES_PER_PIXEL: usize = 4;
|
||||
|
||||
/// Reads `hbm` as a top-down 32bpp BGRA bitmap of the given dimensions.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// `hbm` must be a valid bitmap handle and `width` and `height` must be positive.
|
||||
#[cfg(windows)]
|
||||
unsafe fn read_bgra(hbm: HBITMAP, width: i32, height: i32) -> crate::Result<Vec<u8>> {
|
||||
let image_bytes = (width as usize)
|
||||
.checked_mul(height as usize)
|
||||
.and_then(|n| n.checked_mul(BYTES_PER_PIXEL))
|
||||
.ok_or_else(|| resource_error(ERROR_INVALID_PARAMETER, "image size overflows usize"))?;
|
||||
let mut bgra = vec![0u8; image_bytes];
|
||||
|
||||
let mut bitmap_info = BITMAPINFO::default();
|
||||
bitmap_info.bmiHeader.biSize = std::mem::size_of::<BITMAPINFOHEADER>() as _;
|
||||
bitmap_info.bmiHeader.biWidth = width;
|
||||
// negative value for top-down
|
||||
bitmap_info.bmiHeader.biHeight = -height;
|
||||
bitmap_info.bmiHeader.biBitCount = (BYTES_PER_PIXEL * 8) as u16;
|
||||
bitmap_info.bmiHeader.biPlanes = 1;
|
||||
bitmap_info.bmiHeader.biCompression = BI_RGB.0;
|
||||
|
||||
unsafe {
|
||||
let hdc = CreateCompatibleDC(None);
|
||||
let scan_lines = GetDIBits(
|
||||
hdc,
|
||||
hbm,
|
||||
0,
|
||||
height as u32,
|
||||
Some(bgra.as_mut_ptr() as _),
|
||||
&mut bitmap_info,
|
||||
DIB_RGB_COLORS,
|
||||
);
|
||||
// capture the error before `DeleteDC` can overwrite it
|
||||
let error = (scan_lines != height).then(|| {
|
||||
last_error_or(&format!(
|
||||
"GetDIBits copied {scan_lines} of {height} scan lines"
|
||||
))
|
||||
});
|
||||
let _ = DeleteDC(hdc);
|
||||
if let Some(error) = error {
|
||||
return Err(crate::Error::ImageFromResource(error));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(bgra)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn resource_error(code: WIN32_ERROR, message: &str) -> crate::Error {
|
||||
crate::Error::ImageFromResource(windows::core::Error::new(code.to_hresult(), message))
|
||||
}
|
||||
|
||||
/// Returns the calling thread's last error, or a generic `E_FAIL` with `message`
|
||||
/// when no error code was set (GDI functions do not always set one).
|
||||
#[cfg(windows)]
|
||||
fn last_error_or(message: &str) -> windows::core::Error {
|
||||
let error = windows::core::Error::from_thread();
|
||||
if error.code().is_ok() {
|
||||
windows::core::Error::new(E_FAIL, message)
|
||||
} else {
|
||||
error
|
||||
}
|
||||
}
|
||||
|
||||
/// An RGBA Image in row-major order from top to bottom.
|
||||
#[derive(Clone)]
|
||||
pub struct Image<'a> {
|
||||
rgba: Cow<'a, [u8]>,
|
||||
width: u32,
|
||||
height: u32,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Image<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Image")
|
||||
.field(
|
||||
"rgba",
|
||||
// Reduces the debug size compared to the derived default, as the default
|
||||
// would format the raw bytes as numbers `[0, 0, 0, 0]` for 1 pixel.
|
||||
// The custom format doesn't grow as much with larger images:
|
||||
// `Image { rgba: Cow::Borrowed([u8; 4096]), width: 32, height: 32 }`
|
||||
&format_args!(
|
||||
"Cow::{}([u8; {}])",
|
||||
match &self.rgba {
|
||||
Cow::Borrowed(_) => "Borrowed",
|
||||
Cow::Owned(_) => "Owned",
|
||||
},
|
||||
self.rgba.len()
|
||||
),
|
||||
)
|
||||
.field("width", &self.width)
|
||||
.field("height", &self.height)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Resource for Image<'static> {}
|
||||
|
||||
impl Image<'static> {
|
||||
/// Creates a new Image using RGBA data, in row-major order from top to bottom, and with specified width and height.
|
||||
///
|
||||
/// Similar to [`Self::new`] but avoids cloning the rgba data to get an owned Image.
|
||||
pub const fn new_owned(rgba: Vec<u8>, width: u32, height: u32) -> Self {
|
||||
Self {
|
||||
rgba: Cow::Owned(rgba),
|
||||
width,
|
||||
height,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Image<'a> {
|
||||
/// Creates a new Image using RGBA data, in row-major order from top to bottom, and with specified width and height.
|
||||
pub const fn new(rgba: &'a [u8], width: u32, height: u32) -> Self {
|
||||
Self {
|
||||
rgba: Cow::Borrowed(rgba),
|
||||
width,
|
||||
height,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new image using the provided bytes.
|
||||
///
|
||||
/// Only `ico` and `png` are supported (based on activated feature flag).
|
||||
#[cfg(any(feature = "image-ico", feature = "image-png"))]
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "image-ico", feature = "image-png"))))]
|
||||
pub fn from_bytes(bytes: &[u8]) -> crate::Result<Self> {
|
||||
let img = image::load_from_memory(bytes)?;
|
||||
let (width, height) = (img.width(), img.height());
|
||||
Ok(Self {
|
||||
rgba: Cow::Owned(img.into_rgba8().into_raw()),
|
||||
width,
|
||||
height,
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a new image using the provided path.
|
||||
///
|
||||
/// Only `ico` and `png` are supported (based on activated feature flag).
|
||||
#[cfg(any(feature = "image-ico", feature = "image-png"))]
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "image-ico", feature = "image-png"))))]
|
||||
pub fn from_path<P: AsRef<std::path::Path>>(path: P) -> crate::Result<Self> {
|
||||
let bytes = std::fs::read(path)?;
|
||||
Self::from_bytes(&bytes)
|
||||
}
|
||||
|
||||
/// Creates a new image from the application icon embedded in the executable of the current process.
|
||||
///
|
||||
/// The application icon is the one `tauri-build` embeds with the
|
||||
/// [`WINDOWS_APP_ICON_RESOURCE_ID`](crate::utils::platform::WINDOWS_APP_ICON_RESOURCE_ID) id,
|
||||
/// this could change in the future.
|
||||
#[cfg(windows)]
|
||||
#[cfg_attr(docsrs, doc(cfg(windows)))]
|
||||
pub fn from_app_icon_resource(size: u32) -> crate::Result<Self> {
|
||||
Image::from_icon_resource(
|
||||
crate::utils::platform::WINDOWS_APP_ICON_RESOURCE_ID,
|
||||
size,
|
||||
size,
|
||||
)
|
||||
}
|
||||
|
||||
/// Create a new image from an icon resource embedded in the executable of the current process.
|
||||
///
|
||||
/// Resources are looked up in the process executable (`GetModuleHandleW(NULL)`),
|
||||
/// not in the DLL containing this code when tauri is built as a library.
|
||||
///
|
||||
/// **Note**: This might take ~2ms for [`LoadImageW`] to load the image for the first time.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// The resource can be identified by its integer id or by its name, see [`IconResource`].
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use tauri::image::Image;
|
||||
/// # fn main() -> tauri::Result<()> {
|
||||
/// let icon = Image::from_icon_resource(1, 32, 32)?;
|
||||
/// let icon = Image::from_icon_resource("icon", 32, 32)?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[cfg(windows)]
|
||||
#[cfg_attr(docsrs, doc(cfg(windows)))]
|
||||
pub fn from_icon_resource<'r>(
|
||||
resource: impl Into<IconResource<'r>>,
|
||||
width: u32,
|
||||
height: u32,
|
||||
) -> crate::Result<Self> {
|
||||
let (width_i32, height_i32) = match (i32::try_from(width), i32::try_from(height)) {
|
||||
(Ok(w), Ok(h)) if w > 0 && h > 0 => (w, h),
|
||||
_ => {
|
||||
return Err(resource_error(
|
||||
ERROR_INVALID_PARAMETER,
|
||||
"width and height must be between 1 and i32::MAX",
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// keeps the wide string alive for the `LoadImageW` call
|
||||
let name: Vec<u16>;
|
||||
let resource_id = match resource.into() {
|
||||
// MAKEINTRESOURCE
|
||||
IconResource::Id(id) => PCWSTR(id as usize as *const u16),
|
||||
IconResource::Name(n) => {
|
||||
name = n.encode_utf16().chain(std::iter::once(0)).collect();
|
||||
PCWSTR(name.as_ptr())
|
||||
}
|
||||
};
|
||||
|
||||
let hicon = unsafe {
|
||||
Owned::new(HICON(
|
||||
LoadImageW(
|
||||
Some(
|
||||
GetModuleHandleW(PCWSTR::null())
|
||||
.map_err(crate::Error::ImageFromResource)?
|
||||
.into(),
|
||||
),
|
||||
resource_id,
|
||||
IMAGE_ICON,
|
||||
width_i32,
|
||||
height_i32,
|
||||
LR_DEFAULTCOLOR,
|
||||
)
|
||||
.map_err(crate::Error::ImageFromResource)?
|
||||
.0,
|
||||
))
|
||||
};
|
||||
|
||||
let mut icon_info = ICONINFO::default();
|
||||
unsafe { GetIconInfo(*hicon, &mut icon_info).map_err(crate::Error::ImageFromResource)? };
|
||||
let hbm_mask = unsafe { Owned::new(icon_info.hbmMask) };
|
||||
let hbm_color = unsafe { Owned::new(icon_info.hbmColor) };
|
||||
|
||||
// monochrome icons only have a mask bitmap (AND mask stacked on top of the XOR mask)
|
||||
if hbm_color.is_invalid() {
|
||||
return Err(resource_error(
|
||||
ERROR_NOT_SUPPORTED,
|
||||
"monochrome icons are not supported",
|
||||
));
|
||||
}
|
||||
|
||||
let mut bgra = unsafe { read_bgra(*hbm_color, width_i32, height_i32)? };
|
||||
|
||||
// Color bitmaps without an alpha channel (e.g. 24bpp icons) read back with alpha = 0 on every pixel,
|
||||
// so recover the alpha channel from the AND mask: a set bit means the pixel is transparent.
|
||||
if bgra
|
||||
.as_chunks::<BYTES_PER_PIXEL>()
|
||||
.0
|
||||
.iter()
|
||||
.all(|px| px[3] == 0)
|
||||
{
|
||||
let mask = unsafe { read_bgra(*hbm_mask, width_i32, height_i32)? };
|
||||
for (px, mask) in bgra
|
||||
.as_chunks_mut::<BYTES_PER_PIXEL>()
|
||||
.0
|
||||
.iter_mut()
|
||||
.zip(mask.as_chunks::<BYTES_PER_PIXEL>().0)
|
||||
{
|
||||
// the 1bpp mask expands to black (clear bit) or white (set bit)
|
||||
px[3] = if mask[0] == 0 { 0xFF } else { 0 };
|
||||
}
|
||||
}
|
||||
|
||||
let rgba = {
|
||||
for px in bgra.as_chunks_mut::<BYTES_PER_PIXEL>().0 {
|
||||
// Swap Blue and Red channels
|
||||
px.swap(0, 2);
|
||||
}
|
||||
bgra
|
||||
};
|
||||
|
||||
Ok(Image::new_owned(rgba, width, height))
|
||||
}
|
||||
|
||||
/// Returns the RGBA data for this image, in row-major order from top to bottom.
|
||||
pub fn rgba(&'a self) -> &'a [u8] {
|
||||
&self.rgba
|
||||
}
|
||||
|
||||
/// Returns the width of this image.
|
||||
pub fn width(&self) -> u32 {
|
||||
self.width
|
||||
}
|
||||
|
||||
/// Returns the height of this image.
|
||||
pub fn height(&self) -> u32 {
|
||||
self.height
|
||||
}
|
||||
|
||||
/// Convert into a 'static owned [`Image`].
|
||||
/// This will allocate.
|
||||
pub fn to_owned(self) -> Image<'static> {
|
||||
Image {
|
||||
rgba: match self.rgba {
|
||||
Cow::Owned(v) => Cow::Owned(v),
|
||||
Cow::Borrowed(v) => Cow::Owned(v.to_vec()),
|
||||
},
|
||||
height: self.height,
|
||||
width: self.width,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Image<'a>> for crate::runtime::Icon<'a> {
|
||||
fn from(img: Image<'a>) -> Self {
|
||||
Self {
|
||||
rgba: img.rgba,
|
||||
width: img.width,
|
||||
height: img.height,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(desktop)]
|
||||
impl TryFrom<Image<'_>> for muda::Icon {
|
||||
type Error = crate::Error;
|
||||
|
||||
fn try_from(img: Image<'_>) -> Result<Self, Self::Error> {
|
||||
muda::Icon::from_rgba(img.rgba.into_owned(), img.width, img.height).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(desktop, feature = "tray-icon"))]
|
||||
impl TryFrom<Image<'_>> for tray_icon::Icon {
|
||||
type Error = crate::Error;
|
||||
|
||||
fn try_from(img: Image<'_>) -> Result<Self, Self::Error> {
|
||||
tray_icon::Icon::from_rgba(img.rgba.into_owned(), img.width, img.height).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
/// An image type that accepts file paths, raw bytes, previously loaded images and image objects.
|
||||
///
|
||||
/// This type is meant to be used along the [transformImage](https://v2.tauri.app/reference/javascript/api/namespaceimage/#transformimage) API.
|
||||
///
|
||||
/// # Stability
|
||||
///
|
||||
/// The stability of the variants are not guaranteed, and matching against them is not recommended.
|
||||
/// Use [`JsImage::into_img`] instead.
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(untagged)]
|
||||
#[non_exhaustive]
|
||||
pub enum JsImage {
|
||||
/// A reference to an image in the filesystem. This requires `image-ico` or `image-png` cargo features.
|
||||
#[non_exhaustive]
|
||||
Path(std::path::PathBuf),
|
||||
/// ICO or PNG image in raw bytes. This requires `image-ico` or `image-png` cargo features.
|
||||
#[non_exhaustive]
|
||||
Bytes(Vec<u8>),
|
||||
/// An image that was previously loaded with the API and is stored in the resource table.
|
||||
#[non_exhaustive]
|
||||
Resource(ResourceId),
|
||||
/// Raw RGBA definition of an image.
|
||||
#[non_exhaustive]
|
||||
Rgba {
|
||||
/// Image bytes.
|
||||
rgba: Vec<u8>,
|
||||
/// Image width.
|
||||
width: u32,
|
||||
/// Image height.
|
||||
height: u32,
|
||||
},
|
||||
}
|
||||
|
||||
impl JsImage {
|
||||
/// Converts this intermediate image format into an actual [`Image`].
|
||||
///
|
||||
/// This will retrieve the image from the passed [`ResourceTable`] if it is [`JsImage::Resource`]
|
||||
/// and will return an error if it doesn't exist in the passed [`ResourceTable`] so make sure
|
||||
/// the passed [`ResourceTable`] is the same one used to store the image, usually this should be
|
||||
/// the webview resources table.
|
||||
pub fn into_img(self, resources_table: &ResourceTable) -> crate::Result<Arc<Image<'_>>> {
|
||||
match self {
|
||||
Self::Resource(rid) => resources_table.get::<Image<'static>>(rid),
|
||||
#[cfg(any(feature = "image-ico", feature = "image-png"))]
|
||||
Self::Path(path) => Image::from_path(path).map(Arc::new),
|
||||
|
||||
#[cfg(any(feature = "image-ico", feature = "image-png"))]
|
||||
Self::Bytes(bytes) => Image::from_bytes(&bytes).map(Arc::new),
|
||||
|
||||
Self::Rgba {
|
||||
rgba,
|
||||
width,
|
||||
height,
|
||||
} => Ok(Arc::new(Image::new_owned(rgba, width, height))),
|
||||
|
||||
#[cfg(not(any(feature = "image-ico", feature = "image-png")))]
|
||||
_ => Err(
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!(
|
||||
"expected RGBA image data, found {}",
|
||||
match self {
|
||||
JsImage::Path(_) => "a file path",
|
||||
JsImage::Bytes(_) => "raw bytes",
|
||||
_ => unreachable!(),
|
||||
}
|
||||
),
|
||||
)
|
||||
.into(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, windows))]
|
||||
mod tests {
|
||||
use super::{IconResource, Image, default_window_icon_from_app_icon_resource};
|
||||
|
||||
/// The test executable has no icon resources, so every lookup must fail with an error
|
||||
/// (instead of panicking or returning a stretched placeholder).
|
||||
#[test]
|
||||
fn from_icon_resource_missing_resource_is_an_error() {
|
||||
for resource in [
|
||||
IconResource::Id(u16::MAX),
|
||||
IconResource::Name("tauri-image-test-missing-icon"),
|
||||
] {
|
||||
let error = Image::from_icon_resource(resource, 32, 32).unwrap_err();
|
||||
assert!(
|
||||
matches!(error, crate::Error::ImageFromResource(_)),
|
||||
"{resource:?}: {error:?}"
|
||||
);
|
||||
}
|
||||
|
||||
assert!(default_window_icon_from_app_icon_resource().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_icon_resource_rejects_invalid_sizes() {
|
||||
for (width, height) in [(0, 32), (32, 0), (u32::MAX, 32), (32, i32::MAX as u32 + 1)] {
|
||||
let error = Image::from_icon_resource(1, width, height).unwrap_err();
|
||||
assert!(
|
||||
matches!(error, crate::Error::ImageFromResource(_)),
|
||||
"{width}x{height}: {error:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
88
crates/tauri/src/image/plugin.rs
Normal file
88
crates/tauri/src/image/plugin.rs
Normal file
@@ -0,0 +1,88 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::Manager;
|
||||
use crate::plugin::{Builder, TauriPlugin};
|
||||
use crate::{ResourceId, Runtime, Webview, command, image::Image};
|
||||
|
||||
#[command(root = "crate")]
|
||||
fn new<R: Runtime>(
|
||||
webview: Webview<R>,
|
||||
rgba: Vec<u8>,
|
||||
width: u32,
|
||||
height: u32,
|
||||
) -> crate::Result<ResourceId> {
|
||||
let image = Image::new_owned(rgba, width, height);
|
||||
let mut resources_table = webview.resources_table();
|
||||
let rid = resources_table.add(image);
|
||||
Ok(rid)
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "image-ico", feature = "image-png"))]
|
||||
#[command(root = "crate")]
|
||||
fn from_bytes<R: Runtime>(webview: Webview<R>, bytes: Vec<u8>) -> crate::Result<ResourceId> {
|
||||
let image = Image::from_bytes(&bytes)?.to_owned();
|
||||
let mut resources_table = webview.resources_table();
|
||||
let rid = resources_table.add(image);
|
||||
Ok(rid)
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "image-ico", feature = "image-png")))]
|
||||
#[command(root = "crate")]
|
||||
fn from_bytes() -> std::result::Result<(), &'static str> {
|
||||
Err("from_bytes is only supported if the `image-ico` or `image-png` Cargo features are enabled")
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "image-ico", feature = "image-png"))]
|
||||
#[command(root = "crate")]
|
||||
fn from_path<R: Runtime>(
|
||||
webview: Webview<R>,
|
||||
path: std::path::PathBuf,
|
||||
) -> crate::Result<ResourceId> {
|
||||
let image = Image::from_path(path)?.to_owned();
|
||||
let mut resources_table = webview.resources_table();
|
||||
let rid = resources_table.add(image);
|
||||
Ok(rid)
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "image-ico", feature = "image-png")))]
|
||||
#[command(root = "crate")]
|
||||
fn from_path() -> std::result::Result<(), &'static str> {
|
||||
Err("from_path is only supported if the `image-ico` or `image-png` Cargo features are enabled")
|
||||
}
|
||||
|
||||
#[command(root = "crate")]
|
||||
fn rgba<R: Runtime>(webview: Webview<R>, rid: ResourceId) -> crate::Result<crate::ipc::Response> {
|
||||
let resources_table = webview.resources_table();
|
||||
let image = resources_table.get::<Image<'_>>(rid)?;
|
||||
Ok(crate::ipc::Response::new(image.rgba().to_owned()))
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Size {
|
||||
width: u32,
|
||||
height: u32,
|
||||
}
|
||||
|
||||
#[command(root = "crate")]
|
||||
fn size<R: Runtime>(webview: Webview<R>, rid: ResourceId) -> crate::Result<Size> {
|
||||
let resources_table = webview.resources_table();
|
||||
let image = resources_table.get::<Image<'_>>(rid)?;
|
||||
Ok(Size {
|
||||
width: image.width(),
|
||||
height: image.height(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Initializes the plugin.
|
||||
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
Builder::new("image")
|
||||
.invoke_handler(crate::generate_handler![
|
||||
#![plugin(image)]
|
||||
new, from_bytes, from_path, rgba, size
|
||||
])
|
||||
.build()
|
||||
}
|
||||
49
crates/tauri/src/ios.rs
Normal file
49
crates/tauri/src/ios.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use swift_rs::{SRString, SwiftArg, swift};
|
||||
|
||||
use std::{
|
||||
ffi::c_void,
|
||||
os::raw::{c_char, c_int, c_ulonglong},
|
||||
};
|
||||
|
||||
type PluginMessageCallbackFn = unsafe extern "C" fn(c_int, c_int, *const c_char);
|
||||
pub struct PluginMessageCallback(pub PluginMessageCallbackFn);
|
||||
|
||||
impl<'a> SwiftArg<'a> for PluginMessageCallback {
|
||||
type ArgType = PluginMessageCallbackFn;
|
||||
|
||||
unsafe fn as_arg(&'a self) -> Self::ArgType {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
type ChannelSendDataCallbackFn = unsafe extern "C" fn(c_ulonglong, *const c_char);
|
||||
pub struct ChannelSendDataCallback(pub ChannelSendDataCallbackFn);
|
||||
|
||||
impl<'a> SwiftArg<'a> for ChannelSendDataCallback {
|
||||
type ArgType = ChannelSendDataCallbackFn;
|
||||
|
||||
unsafe fn as_arg(&'a self) -> Self::ArgType {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
swift!(pub fn run_plugin_command(
|
||||
id: i32,
|
||||
name: &SRString,
|
||||
method: &SRString,
|
||||
data: &SRString,
|
||||
callback: PluginMessageCallback,
|
||||
send_channel_data_callback: ChannelSendDataCallback
|
||||
));
|
||||
swift!(pub fn register_plugin(
|
||||
name: &SRString,
|
||||
plugin: *const c_void,
|
||||
config: &SRString,
|
||||
webview: *const c_void
|
||||
));
|
||||
swift!(pub fn on_webview_created(webview: *const c_void, controller: *const c_void));
|
||||
swift!(pub fn log_stdout());
|
||||
1485
crates/tauri/src/ipc/authority.rs
Normal file
1485
crates/tauri/src/ipc/authority.rs
Normal file
File diff suppressed because it is too large
Load Diff
170
crates/tauri/src/ipc/capability_builder.rs
Normal file
170
crates/tauri/src/ipc/capability_builder.rs
Normal file
@@ -0,0 +1,170 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use serde::Serialize;
|
||||
use tauri_utils::{
|
||||
acl::{
|
||||
Scopes,
|
||||
capability::{Capability, CapabilityFile, PermissionEntry},
|
||||
},
|
||||
platform::Target,
|
||||
};
|
||||
|
||||
/// A capability that can be added at runtime.
|
||||
pub trait RuntimeCapability {
|
||||
/// Creates the capability file.
|
||||
fn build(self) -> CapabilityFile;
|
||||
}
|
||||
|
||||
impl<T: AsRef<str>> RuntimeCapability for T {
|
||||
fn build(self) -> CapabilityFile {
|
||||
self.as_ref().parse().expect("invalid capability")
|
||||
}
|
||||
}
|
||||
|
||||
/// A builder for a [`Capability`].
|
||||
pub struct CapabilityBuilder(Capability);
|
||||
|
||||
impl CapabilityBuilder {
|
||||
/// Creates a new capability builder with a unique identifier.
|
||||
pub fn new(identifier: impl Into<String>) -> Self {
|
||||
Self(Capability {
|
||||
identifier: identifier.into(),
|
||||
description: "".into(),
|
||||
remote: None,
|
||||
local: true,
|
||||
windows: Vec::new(),
|
||||
webviews: Vec::new(),
|
||||
permissions: Vec::new(),
|
||||
platforms: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Allows this capability to be used by a remote URL.
|
||||
pub fn remote(mut self, url: String) -> Self {
|
||||
self
|
||||
.0
|
||||
.remote
|
||||
.get_or_insert_with(Default::default)
|
||||
.urls
|
||||
.push(url);
|
||||
self
|
||||
}
|
||||
|
||||
/// Whether this capability is applied on local app URLs or not. Defaults to `true`.
|
||||
pub fn local(mut self, local: bool) -> Self {
|
||||
self.0.local = local;
|
||||
self
|
||||
}
|
||||
|
||||
/// Link this capability to the given window label.
|
||||
pub fn window(mut self, window: impl Into<String>) -> Self {
|
||||
self.0.windows.push(window.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Link this capability to the a list of window labels.
|
||||
pub fn windows(mut self, windows: impl IntoIterator<Item = impl Into<String>>) -> Self {
|
||||
self.0.windows.extend(windows.into_iter().map(|w| w.into()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Link this capability to the given webview label.
|
||||
pub fn webview(mut self, webview: impl Into<String>) -> Self {
|
||||
self.0.webviews.push(webview.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Link this capability to the a list of window labels.
|
||||
pub fn webviews(mut self, webviews: impl IntoIterator<Item = impl Into<String>>) -> Self {
|
||||
self
|
||||
.0
|
||||
.webviews
|
||||
.extend(webviews.into_iter().map(|w| w.into()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a new permission to this capability.
|
||||
pub fn permission(mut self, permission: impl Into<String>) -> Self {
|
||||
let permission = permission.into();
|
||||
self.0.permissions.push(PermissionEntry::PermissionRef(
|
||||
permission
|
||||
.clone()
|
||||
.try_into()
|
||||
.unwrap_or_else(|_| panic!("invalid permission identifier '{permission}'")),
|
||||
));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a new scoped permission to this capability.
|
||||
pub fn permission_scoped<T: Serialize>(
|
||||
mut self,
|
||||
permission: impl Into<String>,
|
||||
allowed: Vec<T>,
|
||||
denied: Vec<T>,
|
||||
) -> Self {
|
||||
let permission = permission.into();
|
||||
let identifier = permission
|
||||
.clone()
|
||||
.try_into()
|
||||
.unwrap_or_else(|_| panic!("invalid permission identifier '{permission}'"));
|
||||
|
||||
let allowed_scope = allowed
|
||||
.into_iter()
|
||||
.map(|a| {
|
||||
serde_json::to_value(a)
|
||||
.expect("failed to serialize scope")
|
||||
.into()
|
||||
})
|
||||
.collect();
|
||||
let denied_scope = denied
|
||||
.into_iter()
|
||||
.map(|a| {
|
||||
serde_json::to_value(a)
|
||||
.expect("failed to serialize scope")
|
||||
.into()
|
||||
})
|
||||
.collect();
|
||||
let scope = Scopes {
|
||||
allow: Some(allowed_scope),
|
||||
deny: Some(denied_scope),
|
||||
};
|
||||
|
||||
self
|
||||
.0
|
||||
.permissions
|
||||
.push(PermissionEntry::ExtendedPermission { identifier, scope });
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a target platform for this capability.
|
||||
///
|
||||
/// By default all platforms are applied.
|
||||
pub fn platform(mut self, platform: Target) -> Self {
|
||||
self
|
||||
.0
|
||||
.platforms
|
||||
.get_or_insert_with(Default::default)
|
||||
.push(platform);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds target platforms for this capability.
|
||||
///
|
||||
/// By default all platforms are applied.
|
||||
pub fn platforms(mut self, platforms: impl IntoIterator<Item = Target>) -> Self {
|
||||
self
|
||||
.0
|
||||
.platforms
|
||||
.get_or_insert_with(Default::default)
|
||||
.extend(platforms);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RuntimeCapability for CapabilityBuilder {
|
||||
fn build(self) -> CapabilityFile {
|
||||
CapabilityFile::Capability(self.0)
|
||||
}
|
||||
}
|
||||
530
crates/tauri/src/ipc/channel.rs
Normal file
530
crates/tauri/src/ipc/channel.rs
Normal file
@@ -0,0 +1,530 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
str::FromStr,
|
||||
sync::{
|
||||
Arc, Mutex,
|
||||
atomic::{AtomicU32, AtomicUsize, Ordering},
|
||||
},
|
||||
};
|
||||
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
use crate::{
|
||||
Manager, Runtime, State, Webview, command,
|
||||
ipc::{CommandArg, CommandItem},
|
||||
plugin::{Builder as PluginBuilder, TauriPlugin},
|
||||
};
|
||||
|
||||
use super::{
|
||||
CallbackFn, InvokeError, InvokeResponseBody, IpcResponse, Request, Response,
|
||||
format_callback::format_raw_js,
|
||||
};
|
||||
|
||||
pub const IPC_PAYLOAD_PREFIX: &str = "__CHANNEL__:";
|
||||
// TODO: Change this to `channel` in v3
|
||||
pub const CHANNEL_PLUGIN_NAME: &str = "__TAURI_CHANNEL__";
|
||||
// TODO: Change this to `plugin:channel|fetch` in v3
|
||||
pub const FETCH_CHANNEL_DATA_COMMAND: &str = "plugin:__TAURI_CHANNEL__|fetch";
|
||||
const CHANNEL_ID_HEADER_NAME: &str = "Tauri-Channel-Id";
|
||||
|
||||
/// Maximum size a JSON we should send directly without going through the fetch process
|
||||
// 8192 byte JSON payload runs roughly 2x faster through eval than through fetch on WebView2 v135
|
||||
const MAX_JSON_DIRECT_EXECUTE_THRESHOLD: usize = 8192;
|
||||
// 1024 byte payload runs roughly 30% faster through eval than through fetch on macOS
|
||||
const MAX_RAW_DIRECT_EXECUTE_THRESHOLD: usize = 1024;
|
||||
|
||||
static CHANNEL_COUNTER: AtomicU32 = AtomicU32::new(0);
|
||||
|
||||
/// Maps channel ids to pending data that must be sent to the JavaScript side via the IPC.
|
||||
///
|
||||
/// Scoped per webview: each webview has its own id sequence and lookups only
|
||||
/// ever touch the calling webview's entries.
|
||||
#[derive(Default, Clone)]
|
||||
pub struct ChannelDataIpcQueue(Arc<Mutex<HashMap<String, WebviewChannelDataQueue>>>);
|
||||
|
||||
/// Pending channel data of one webview.
|
||||
#[derive(Default)]
|
||||
struct WebviewChannelDataQueue {
|
||||
next_id: u32,
|
||||
entries: HashMap<u32, InvokeResponseBody>,
|
||||
}
|
||||
|
||||
impl ChannelDataIpcQueue {
|
||||
/// Stores the body for the given webview and returns its id.
|
||||
///
|
||||
/// Ids are sequential per webview and only address that webview's entries.
|
||||
fn insert(&self, webview_label: &str, body: InvokeResponseBody) -> u32 {
|
||||
let mut cache = self.0.lock().unwrap();
|
||||
let queue = cache.entry(webview_label.to_string()).or_default();
|
||||
let data_id = loop {
|
||||
let candidate = queue.next_id;
|
||||
queue.next_id = queue.next_id.wrapping_add(1);
|
||||
if !queue.entries.contains_key(&candidate) {
|
||||
break candidate;
|
||||
}
|
||||
};
|
||||
queue.entries.insert(data_id, body);
|
||||
data_id
|
||||
}
|
||||
|
||||
/// Removes and returns the entry with the given id from the given webview's queue.
|
||||
fn remove(&self, webview_label: &str, data_id: u32) -> Option<InvokeResponseBody> {
|
||||
self
|
||||
.0
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get_mut(webview_label)?
|
||||
.entries
|
||||
.remove(&data_id)
|
||||
}
|
||||
|
||||
/// Drops all entries of the given webview.
|
||||
pub(crate) fn remove_webview_entries(&self, webview_label: &str) {
|
||||
self.0.lock().unwrap().remove(webview_label);
|
||||
}
|
||||
}
|
||||
|
||||
/// An IPC channel, used to stream values from Rust to the frontend.
|
||||
///
|
||||
/// A command can only resolve once, so a channel is how you push an arbitrary number of messages
|
||||
/// to the JavaScript side after the command returned: download progress, log lines, streamed
|
||||
/// responses and so on. Each message is a `TSend` value, which must implement [`IpcResponse`]
|
||||
/// (automatically implemented for every [`serde::Serialize`] type) and is delivered to the
|
||||
/// `onmessage` handler of the matching `Channel` on the JavaScript side.
|
||||
///
|
||||
/// The usual flow is to create the channel on the frontend and pass it as a command argument,
|
||||
/// since [`Channel`] implements [`CommandArg`]:
|
||||
///
|
||||
/// ```javascript
|
||||
/// import { Channel, invoke } from '@tauri-apps/api/core'
|
||||
///
|
||||
/// const onProgress = new Channel()
|
||||
/// onProgress.onmessage = (message) => console.log(message)
|
||||
/// await invoke('download', { url, onProgress })
|
||||
/// ```
|
||||
///
|
||||
/// A channel can also be created on the Rust side with [`Channel::new`], or resolved from an id
|
||||
/// the frontend sent inside a bigger payload with [`JavaScriptChannelId::channel_on`].
|
||||
///
|
||||
/// Channels are cheap to clone (every clone refers to the same JavaScript callback) and can be
|
||||
/// stored in the app state or moved to another thread to send messages later. When the last clone
|
||||
/// of a channel created from the frontend is dropped, the JavaScript side is notified that no more
|
||||
/// messages will arrive and the callback is unregistered.
|
||||
///
|
||||
/// # Ordering
|
||||
///
|
||||
/// The channel automatically orders the messages: every message carries the index it was sent with,
|
||||
/// and the JavaScript side buffers out-of-order messages until the missing ones arrive, so the
|
||||
/// `onmessage` handler always observes messages in the same order [`Channel::send`] was called.
|
||||
/// See [`Builder::channel_interceptor`](crate::Builder::channel_interceptor) if you need to
|
||||
/// intercept or replace this delivery mechanism.
|
||||
///
|
||||
/// # Raw payloads
|
||||
///
|
||||
/// Any [`serde::Serialize`] value is sent as JSON. To stream binary data without the JSON overhead,
|
||||
/// send an [`InvokeResponseBody::Raw`] through a `Channel<InvokeResponseBody>` (the default type
|
||||
/// parameter) or a [`Response`]: the payload is then received in JavaScript as an `ArrayBuffer`.
|
||||
/// Note that a `Channel<Vec<u8>>` does *not* do this - `Vec<u8>` is `Serialize`,
|
||||
/// so it is sent as a JSON array of numbers.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Streaming progress to the frontend from a command:
|
||||
///
|
||||
/// ```rust
|
||||
/// use tauri::ipc::Channel;
|
||||
///
|
||||
/// #[derive(Clone, serde::Serialize)]
|
||||
/// #[serde(rename_all = "camelCase")]
|
||||
/// struct DownloadProgress {
|
||||
/// downloaded: usize,
|
||||
/// content_length: usize,
|
||||
/// }
|
||||
///
|
||||
/// #[tauri::command]
|
||||
/// fn download(url: String, on_progress: Channel<DownloadProgress>) -> tauri::Result<()> {
|
||||
/// let content_length = 1000;
|
||||
/// for downloaded in (0..=content_length).step_by(100) {
|
||||
/// on_progress.send(DownloadProgress { downloaded, content_length })?;
|
||||
/// }
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Streaming binary chunks, received as `ArrayBuffer`s in JavaScript:
|
||||
///
|
||||
/// ```rust
|
||||
/// use tauri::ipc::{Channel, InvokeResponseBody};
|
||||
///
|
||||
/// #[tauri::command]
|
||||
/// fn read_file(on_chunk: Channel<InvokeResponseBody>) -> tauri::Result<()> {
|
||||
/// for chunk in [vec![0u8; 16], vec![1u8; 16]] {
|
||||
/// on_chunk.send(InvokeResponseBody::Raw(chunk))?;
|
||||
/// }
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
pub struct Channel<TSend = InvokeResponseBody> {
|
||||
inner: Arc<ChannelInner>,
|
||||
phantom: std::marker::PhantomData<TSend>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "specta")]
|
||||
const _: () = {
|
||||
#[derive(specta::Type)]
|
||||
#[specta(remote = super::Channel)]
|
||||
#[allow(dead_code, non_camel_case_types)]
|
||||
struct TAURI_CHANNEL<TSend>(std::marker::PhantomData<TSend>);
|
||||
};
|
||||
|
||||
impl<TSend> Clone for Channel<TSend> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
inner: self.inner.clone(),
|
||||
phantom: self.phantom,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type OnDropFn = Option<Box<dyn Fn() + Send + Sync + 'static>>;
|
||||
type OnMessageFn = Box<dyn Fn(InvokeResponseBody) -> crate::Result<()> + Send + Sync>;
|
||||
|
||||
struct ChannelInner {
|
||||
id: u32,
|
||||
on_message: OnMessageFn,
|
||||
on_drop: OnDropFn,
|
||||
}
|
||||
|
||||
impl Drop for ChannelInner {
|
||||
fn drop(&mut self) {
|
||||
if let Some(on_drop) = &self.on_drop {
|
||||
on_drop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<TSend> Serialize for Channel<TSend> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&format!("{IPC_PAYLOAD_PREFIX}{}", self.inner.id))
|
||||
}
|
||||
}
|
||||
|
||||
/// The ID of a channel that was defined on the JavaScript layer.
|
||||
///
|
||||
/// Useful when expecting [`Channel`] as part of a JSON object instead of a top-level command argument.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use tauri::{ipc::JavaScriptChannelId, Runtime, Webview};
|
||||
///
|
||||
/// #[derive(serde::Deserialize)]
|
||||
/// #[serde(rename_all = "camelCase")]
|
||||
/// struct Button {
|
||||
/// label: String,
|
||||
/// on_click: JavaScriptChannelId,
|
||||
/// }
|
||||
///
|
||||
/// #[tauri::command]
|
||||
/// fn add_button<R: Runtime>(webview: Webview<R>, button: Button) {
|
||||
/// let channel = button.on_click.channel_on(webview);
|
||||
/// channel.send("clicked").unwrap();
|
||||
/// }
|
||||
/// ```
|
||||
pub struct JavaScriptChannelId(CallbackFn);
|
||||
|
||||
impl FromStr for JavaScriptChannelId {
|
||||
type Err = &'static str;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
s.strip_prefix(IPC_PAYLOAD_PREFIX)
|
||||
.ok_or("invalid channel string")
|
||||
.and_then(|id| id.parse().map_err(|_| "invalid channel ID"))
|
||||
.map(|id| Self(CallbackFn(id)))
|
||||
}
|
||||
}
|
||||
|
||||
impl JavaScriptChannelId {
|
||||
/// Gets a [`Channel`] for this channel ID on the given [`Webview`].
|
||||
pub fn channel_on<R: Runtime, TSend>(&self, webview: Webview<R>) -> Channel<TSend> {
|
||||
let callback_fn = self.0;
|
||||
let callback_id = callback_fn.0;
|
||||
|
||||
let counter = Arc::new(AtomicUsize::new(0));
|
||||
let counter_clone = counter.clone();
|
||||
let webview_clone = webview.clone();
|
||||
|
||||
Channel::new_with_id(
|
||||
callback_id,
|
||||
Box::new(move |body| {
|
||||
let current_index = counter.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
if let Some(interceptor) = &webview.manager.channel_interceptor {
|
||||
if interceptor(&webview, callback_fn, current_index, &body) {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
match body {
|
||||
// Don't go through the fetch process if the payload is small
|
||||
InvokeResponseBody::Json(json_string)
|
||||
if json_string.len() < MAX_JSON_DIRECT_EXECUTE_THRESHOLD =>
|
||||
{
|
||||
webview.eval(format_raw_js(
|
||||
callback_id,
|
||||
format!("{{ message: {json_string}, index: {current_index} }}"),
|
||||
))?;
|
||||
}
|
||||
InvokeResponseBody::Raw(bytes) if bytes.len() < MAX_RAW_DIRECT_EXECUTE_THRESHOLD => {
|
||||
let bytes_as_json_array = serde_json::to_string(&bytes)?;
|
||||
webview.eval(format_raw_js(callback_id, format!("{{ message: new Uint8Array({bytes_as_json_array}).buffer, index: {current_index} }}")))?;
|
||||
}
|
||||
// use the fetch API to speed up larger response payloads
|
||||
_ => {
|
||||
let data_id = webview
|
||||
.state::<ChannelDataIpcQueue>()
|
||||
.insert(webview.label(), body);
|
||||
|
||||
webview.eval(format!(
|
||||
"window.__TAURI_INTERNALS__.invoke('{FETCH_CHANNEL_DATA_COMMAND}', null, {{ headers: {{ '{CHANNEL_ID_HEADER_NAME}': '{data_id}' }} }}).then((response) => window.__TAURI_INTERNALS__.runCallback({callback_id}, {{ message: response, index: {current_index} }})).catch(console.error)",
|
||||
))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}),
|
||||
Some(Box::new(move || {
|
||||
let current_index = counter_clone.load(Ordering::Relaxed);
|
||||
let _ = webview_clone.eval(format_raw_js(
|
||||
callback_id,
|
||||
format!("{{ end: true, index: {current_index} }}"),
|
||||
));
|
||||
})),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for JavaScriptChannelId {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let value: String = Deserialize::deserialize(deserializer)?;
|
||||
Self::from_str(&value).map_err(|_| {
|
||||
serde::de::Error::custom(format!(
|
||||
"invalid channel value `{value}`, expected a string in the `{IPC_PAYLOAD_PREFIX}ID` format"
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<TSend> Channel<TSend> {
|
||||
/// Creates a new channel with the given message handler.
|
||||
///
|
||||
/// This does not involve the frontend: the closure is called with the body of every message sent
|
||||
/// through [`Channel::send`], and it is up to you to forward it. Use it to create a channel that
|
||||
/// a plugin or a mobile command expects, or to consume channel messages in Rust.
|
||||
///
|
||||
/// To push messages to a channel that was created by the frontend, receive the [`Channel`] as a
|
||||
/// command argument (see [`CommandArg`]) or deserialize a [`JavaScriptChannelId`] and call
|
||||
/// [`JavaScriptChannelId::channel_on`] with the target [`Webview`], which wires the messages
|
||||
/// through the IPC for you.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use tauri::ipc::{Channel, InvokeResponseBody};
|
||||
///
|
||||
/// let channel = Channel::new(|message: InvokeResponseBody| {
|
||||
/// match message {
|
||||
/// InvokeResponseBody::Json(json) => println!("channel message: {json}"),
|
||||
/// InvokeResponseBody::Raw(bytes) => println!("channel message: {} bytes", bytes.len()),
|
||||
/// }
|
||||
/// Ok(())
|
||||
/// });
|
||||
///
|
||||
/// channel.send("hello").unwrap();
|
||||
/// ```
|
||||
pub fn new<F: Fn(InvokeResponseBody) -> crate::Result<()> + Send + Sync + 'static>(
|
||||
on_message: F,
|
||||
) -> Self {
|
||||
Self::new_with_id(
|
||||
CHANNEL_COUNTER.fetch_add(1, Ordering::Relaxed),
|
||||
Box::new(on_message),
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
fn new_with_id(id: u32, on_message: OnMessageFn, on_drop: OnDropFn) -> Self {
|
||||
#[allow(clippy::let_and_return)]
|
||||
let channel = Self {
|
||||
inner: Arc::new(ChannelInner {
|
||||
id,
|
||||
on_message,
|
||||
on_drop,
|
||||
}),
|
||||
phantom: Default::default(),
|
||||
};
|
||||
|
||||
#[cfg(mobile)]
|
||||
crate::plugin::mobile::register_channel(Channel {
|
||||
inner: channel.inner.clone(),
|
||||
phantom: Default::default(),
|
||||
});
|
||||
|
||||
channel
|
||||
}
|
||||
|
||||
// This is used from the IPC handler
|
||||
pub(crate) fn from_callback_fn<R: Runtime>(webview: Webview<R>, callback: CallbackFn) -> Self {
|
||||
let callback_id = callback.0;
|
||||
Channel::new_with_id(
|
||||
callback_id,
|
||||
Box::new(move |body| {
|
||||
match body {
|
||||
// Don't go through the fetch process if the payload is small
|
||||
InvokeResponseBody::Json(json_string)
|
||||
if json_string.len() < MAX_JSON_DIRECT_EXECUTE_THRESHOLD =>
|
||||
{
|
||||
webview.eval(format_raw_js(callback_id, json_string))?;
|
||||
}
|
||||
InvokeResponseBody::Raw(bytes) if bytes.len() < MAX_RAW_DIRECT_EXECUTE_THRESHOLD => {
|
||||
let bytes_as_json_array = serde_json::to_string(&bytes)?;
|
||||
webview.eval(format_raw_js(
|
||||
callback_id,
|
||||
format!("new Uint8Array({bytes_as_json_array}).buffer"),
|
||||
))?;
|
||||
}
|
||||
// use the fetch API to speed up larger response payloads
|
||||
_ => {
|
||||
let data_id = webview
|
||||
.state::<ChannelDataIpcQueue>()
|
||||
.insert(webview.label(), body);
|
||||
|
||||
webview.eval(format!(
|
||||
"window.__TAURI_INTERNALS__.invoke('{FETCH_CHANNEL_DATA_COMMAND}', null, {{ headers: {{ '{CHANNEL_ID_HEADER_NAME}': '{data_id}' }} }}).then((response) => window.__TAURI_INTERNALS__.runCallback({callback_id}, response)).catch(console.error)",
|
||||
))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}),
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
/// The channel identifier.
|
||||
pub fn id(&self) -> u32 {
|
||||
self.inner.id
|
||||
}
|
||||
|
||||
/// Sends the given data through the channel.
|
||||
pub fn send(&self, data: TSend) -> crate::Result<()>
|
||||
where
|
||||
TSend: IpcResponse,
|
||||
{
|
||||
(self.inner.on_message)(data.body()?)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, R: Runtime, TSend> CommandArg<'de, R> for Channel<TSend> {
|
||||
/// Grabs the [`Webview`] from the [`CommandItem`] and returns the associated [`Channel`].
|
||||
fn from_command(command: CommandItem<'de, R>) -> Result<Self, InvokeError> {
|
||||
let name = command.name;
|
||||
let arg = command.key;
|
||||
let webview = command.message.webview();
|
||||
let value: String =
|
||||
Deserialize::deserialize(command).map_err(|e| crate::Error::InvalidArgs(name, arg, e))?;
|
||||
JavaScriptChannelId::from_str(&value)
|
||||
.map(|id| id.channel_on(webview))
|
||||
.map_err(|_| {
|
||||
InvokeError::from(format!(
|
||||
"invalid channel value `{value}`, expected a string in the `{IPC_PAYLOAD_PREFIX}ID` format"
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[command(root = "crate")]
|
||||
fn fetch<R: Runtime>(
|
||||
webview: Webview<R>,
|
||||
request: Request<'_>,
|
||||
cache: State<'_, ChannelDataIpcQueue>,
|
||||
) -> Result<Response, &'static str> {
|
||||
if let Some(id) = request
|
||||
.headers()
|
||||
.get(CHANNEL_ID_HEADER_NAME)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|id| id.parse().ok())
|
||||
{
|
||||
if let Some(data) = cache.remove(webview.label(), id) {
|
||||
Ok(Response::new(data))
|
||||
} else {
|
||||
Err("data not found")
|
||||
}
|
||||
} else {
|
||||
Err("missing channel id header")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn plugin<R: Runtime>() -> TauriPlugin<R> {
|
||||
PluginBuilder::new(CHANNEL_PLUGIN_NAME)
|
||||
.invoke_handler(crate::generate_handler![
|
||||
#![plugin(__TAURI_CHANNEL__)]
|
||||
fetch
|
||||
])
|
||||
.build()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn json_body(s: &str) -> InvokeResponseBody {
|
||||
InvokeResponseBody::Json(s.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn queue_entries_are_scoped_to_the_owning_webview() {
|
||||
let queue = ChannelDataIpcQueue::default();
|
||||
let id = queue.insert("main", json_body("{}"));
|
||||
|
||||
// foreign webviews cannot fetch the entry
|
||||
assert!(queue.remove("settings", id).is_none());
|
||||
// the owner can, once
|
||||
assert!(queue.remove("main", id).is_some());
|
||||
assert!(queue.remove("main", id).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ids_are_scoped_per_webview() {
|
||||
let queue = ChannelDataIpcQueue::default();
|
||||
let a = queue.insert("a", json_body("1"));
|
||||
let b = queue.insert("b", json_body("2"));
|
||||
|
||||
// each webview has its own id sequence, so the same id exists twice
|
||||
// without the entries being visible across webviews
|
||||
assert_eq!(a, b);
|
||||
assert!(queue.remove("b", a).is_some());
|
||||
assert!(queue.remove("a", a).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn purge_removes_only_the_closed_webview_entries() {
|
||||
let queue = ChannelDataIpcQueue::default();
|
||||
let a = queue.insert("a", json_body("1"));
|
||||
let b = queue.insert("b", json_body("2"));
|
||||
|
||||
queue.remove_webview_entries("a");
|
||||
|
||||
assert!(queue.remove("a", a).is_none());
|
||||
assert!(queue.remove("b", b).is_some());
|
||||
}
|
||||
}
|
||||
332
crates/tauri/src/ipc/command.rs
Normal file
332
crates/tauri/src/ipc/command.rs
Normal file
@@ -0,0 +1,332 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! The Tauri custom commands types and traits.
|
||||
//!
|
||||
//! You usually don't need to create these items yourself. These are created from [command](../attr.command.html)
|
||||
//! attribute macro along the way and used by [`crate::generate_handler`] macro.
|
||||
|
||||
use crate::{
|
||||
Runtime,
|
||||
ipc::{InvokeBody, InvokeError, InvokeMessage},
|
||||
};
|
||||
use serde::{
|
||||
Deserialize, Deserializer,
|
||||
de::{Error, Visitor},
|
||||
};
|
||||
|
||||
use tauri_utils::acl::resolved::ResolvedCommand;
|
||||
|
||||
/// Represents a custom command.
|
||||
pub struct CommandItem<'a, R: Runtime> {
|
||||
/// Name of the plugin if this command targets one.
|
||||
pub plugin: Option<&'static str>,
|
||||
|
||||
/// The name of the command, e.g. `handler` on `#[command] fn handler(value: u64)`
|
||||
pub name: &'static str,
|
||||
|
||||
/// The key of the command item, e.g. `value` on `#[command] fn handler(value: u64)`
|
||||
pub key: &'static str,
|
||||
|
||||
/// The [`InvokeMessage`] that was passed to this command.
|
||||
pub message: &'a InvokeMessage<R>,
|
||||
|
||||
/// The resolved ACL for this command.
|
||||
pub acl: &'a Option<Vec<ResolvedCommand>>,
|
||||
}
|
||||
|
||||
/// Trait implemented by command arguments to derive a value from a [`CommandItem`].
|
||||
///
|
||||
/// # Command Arguments
|
||||
///
|
||||
/// A command argument is any type that represents an item parsable from a [`CommandItem`]. Most
|
||||
/// implementations will use the data stored in [`InvokeMessage`] since [`CommandItem`] is mostly a
|
||||
/// wrapper around it.
|
||||
///
|
||||
/// # Provided Implementations
|
||||
///
|
||||
/// Tauri implements [`CommandArg`] automatically for a number of types.
|
||||
/// * [`crate::AppHandle`]
|
||||
/// * [`crate::Window`]
|
||||
/// * [`crate::Webview`]
|
||||
/// * [`crate::WebviewWindow`]
|
||||
/// * [`crate::State`]
|
||||
/// * [`crate::ipc::Channel`]
|
||||
/// * [`crate::ipc::Request`]
|
||||
/// * Gives access to the raw request body and headers instead of a deserialized argument.
|
||||
/// * [`crate::ipc::CommandScope`] and [`crate::ipc::GlobalScope`]
|
||||
/// * The scope defined by the capabilities that allowed the command, see the
|
||||
/// [scope documentation](https://v2.tauri.app/security/scope/).
|
||||
/// * `T where T: serde::Deserialize`
|
||||
/// * Any type that implements `Deserialize` can automatically be used as a [`CommandArg`].
|
||||
///
|
||||
/// Note that the types above are resolved from the command message and not from the command
|
||||
/// arguments sent by the frontend, so they do not need to be provided on the JavaScript side.
|
||||
pub trait CommandArg<'de, R: Runtime>: Sized {
|
||||
/// Derives an instance of `Self` from the [`CommandItem`].
|
||||
///
|
||||
/// If the derivation fails, the corresponding message will be rejected using [`InvokeMessage#reject`].
|
||||
fn from_command(command: CommandItem<'de, R>) -> Result<Self, InvokeError>;
|
||||
}
|
||||
|
||||
/// Automatically implement [`CommandArg`] for any type that can be deserialized.
|
||||
impl<'de, D: Deserialize<'de>, R: Runtime> CommandArg<'de, R> for D {
|
||||
fn from_command(command: CommandItem<'de, R>) -> Result<D, InvokeError> {
|
||||
let name = command.name;
|
||||
let arg = command.key;
|
||||
#[cfg(feature = "tracing")]
|
||||
let _span = tracing::trace_span!("ipc::request::deserialize_arg", arg = arg).entered();
|
||||
Self::deserialize(command).map_err(|e| crate::Error::InvalidArgs(name, arg, e).into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Pass the result of [`serde_json::Value::get`] into [`serde_json::Value`]'s deserializer.
|
||||
///
|
||||
/// Returns an error if the [`CommandItem`]'s key does not exist in the value.
|
||||
macro_rules! pass {
|
||||
($fn:ident, $($arg:ident: $argt:ty),+) => {
|
||||
fn $fn<V: Visitor<'de>>(self, $($arg: $argt),*) -> Result<V::Value, Self::Error> {
|
||||
self.deserialize_json()?.$fn($($arg),*)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, R: Runtime> CommandItem<'a, R> {
|
||||
fn deserialize_json(self) -> serde_json::Result<&'a serde_json::Value> {
|
||||
if self.key.is_empty() {
|
||||
return Err(serde_json::Error::custom(format!(
|
||||
"command {} has an argument with no name with a non-optional value",
|
||||
self.name
|
||||
)));
|
||||
}
|
||||
|
||||
match &self.message.payload {
|
||||
InvokeBody::Raw(_body) => Err(serde_json::Error::custom(format!(
|
||||
"command {} expected a value for key {} but the IPC call used a bytes payload",
|
||||
self.name, self.key
|
||||
))),
|
||||
InvokeBody::Json(v) => match v.get(self.key) {
|
||||
Some(value) => Ok(value),
|
||||
None => Err(serde_json::Error::custom(format!(
|
||||
"command {} missing required key {}",
|
||||
self.name, self.key
|
||||
))),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`Deserializer`] wrapper around [`CommandItem`].
|
||||
///
|
||||
/// If the key doesn't exist, an error will be returned if the deserialized type is not expecting
|
||||
/// an optional item. If the key does exist, the value will be called with
|
||||
/// [`Value`](serde_json::Value)'s [`Deserializer`] implementation.
|
||||
impl<'de, R: Runtime> Deserializer<'de> for CommandItem<'de, R> {
|
||||
type Error = serde_json::Error;
|
||||
|
||||
pass!(deserialize_any, visitor: V);
|
||||
pass!(deserialize_bool, visitor: V);
|
||||
pass!(deserialize_i8, visitor: V);
|
||||
pass!(deserialize_i16, visitor: V);
|
||||
pass!(deserialize_i32, visitor: V);
|
||||
pass!(deserialize_i64, visitor: V);
|
||||
pass!(deserialize_u8, visitor: V);
|
||||
pass!(deserialize_u16, visitor: V);
|
||||
pass!(deserialize_u32, visitor: V);
|
||||
pass!(deserialize_u64, visitor: V);
|
||||
pass!(deserialize_f32, visitor: V);
|
||||
pass!(deserialize_f64, visitor: V);
|
||||
pass!(deserialize_char, visitor: V);
|
||||
pass!(deserialize_str, visitor: V);
|
||||
pass!(deserialize_string, visitor: V);
|
||||
pass!(deserialize_bytes, visitor: V);
|
||||
pass!(deserialize_byte_buf, visitor: V);
|
||||
|
||||
fn deserialize_option<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> {
|
||||
match &self.message.payload {
|
||||
InvokeBody::Raw(_body) => Err(serde_json::Error::custom(format!(
|
||||
"command {} expected a value for key {} but the IPC call used a bytes payload",
|
||||
self.name, self.key
|
||||
))),
|
||||
InvokeBody::Json(v) => match v.get(self.key) {
|
||||
Some(value) => value.deserialize_option(visitor),
|
||||
None => visitor.visit_none(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pass!(deserialize_unit, visitor: V);
|
||||
pass!(deserialize_unit_struct, name: &'static str, visitor: V);
|
||||
pass!(deserialize_newtype_struct, name: &'static str, visitor: V);
|
||||
pass!(deserialize_seq, visitor: V);
|
||||
pass!(deserialize_tuple, len: usize, visitor: V);
|
||||
|
||||
pass!(
|
||||
deserialize_tuple_struct,
|
||||
name: &'static str,
|
||||
len: usize,
|
||||
visitor: V
|
||||
);
|
||||
|
||||
pass!(deserialize_map, visitor: V);
|
||||
|
||||
pass!(
|
||||
deserialize_struct,
|
||||
name: &'static str,
|
||||
fields: &'static [&'static str],
|
||||
visitor: V
|
||||
);
|
||||
|
||||
pass!(
|
||||
deserialize_enum,
|
||||
name: &'static str,
|
||||
fields: &'static [&'static str],
|
||||
visitor: V
|
||||
);
|
||||
|
||||
pass!(deserialize_identifier, visitor: V);
|
||||
pass!(deserialize_ignored_any, visitor: V);
|
||||
}
|
||||
|
||||
/// [Autoref-based stable specialization](https://github.com/dtolnay/case-studies/blob/master/autoref-specialization/README.md)
|
||||
///
|
||||
/// Nothing in this module is considered stable.
|
||||
#[doc(hidden)]
|
||||
pub mod private {
|
||||
use crate::{
|
||||
Runtime,
|
||||
ipc::{InvokeError, InvokeResolver, InvokeResponseBody, IpcResponse},
|
||||
};
|
||||
use std::future::Future;
|
||||
#[cfg(feature = "tracing")]
|
||||
pub use tracing;
|
||||
|
||||
// ===== impl IpcResponse =====
|
||||
|
||||
pub struct ResponseTag;
|
||||
|
||||
pub trait ResponseKind {
|
||||
#[inline(always)]
|
||||
fn blocking_kind(&self) -> ResponseTag {
|
||||
ResponseTag
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn async_kind(&self) -> ResponseTag {
|
||||
ResponseTag
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: IpcResponse> ResponseKind for &T {}
|
||||
|
||||
impl ResponseTag {
|
||||
#[inline(always)]
|
||||
pub fn block<R, T>(self, value: T, resolver: InvokeResolver<R>)
|
||||
where
|
||||
R: Runtime,
|
||||
T: IpcResponse,
|
||||
{
|
||||
resolver.respond(Ok(value))
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub async fn future<T>(self, value: T) -> Result<InvokeResponseBody, InvokeError>
|
||||
where
|
||||
T: IpcResponse,
|
||||
{
|
||||
Ok(value.body()?)
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Result<impl Serialize, impl Into<InvokeError>> =====
|
||||
|
||||
pub struct ResultTag;
|
||||
|
||||
pub trait ResultKind {
|
||||
#[inline(always)]
|
||||
fn blocking_kind(&self) -> ResultTag {
|
||||
ResultTag
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn async_kind(&self) -> ResultTag {
|
||||
ResultTag
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: IpcResponse, E: Into<InvokeError>> ResultKind for Result<T, E> {}
|
||||
|
||||
impl ResultTag {
|
||||
#[inline(always)]
|
||||
pub fn block<R, T, E>(self, value: Result<T, E>, resolver: InvokeResolver<R>)
|
||||
where
|
||||
R: Runtime,
|
||||
T: IpcResponse,
|
||||
E: Into<InvokeError>,
|
||||
{
|
||||
resolver.respond(value.map_err(Into::into))
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub async fn future<T, E>(self, value: Result<T, E>) -> Result<InvokeResponseBody, InvokeError>
|
||||
where
|
||||
T: IpcResponse,
|
||||
E: Into<InvokeError>,
|
||||
{
|
||||
Ok(value.map_err(Into::into)?.body()?)
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Future<Output = impl IpcResponse> =====
|
||||
|
||||
pub struct FutureTag;
|
||||
|
||||
pub trait FutureKind {
|
||||
#[inline(always)]
|
||||
fn async_kind(&self) -> FutureTag {
|
||||
FutureTag
|
||||
}
|
||||
}
|
||||
impl<T: IpcResponse, F: Future<Output = T>> FutureKind for &F {}
|
||||
|
||||
impl FutureTag {
|
||||
#[inline(always)]
|
||||
pub async fn future<T, F>(self, value: F) -> Result<InvokeResponseBody, InvokeError>
|
||||
where
|
||||
T: IpcResponse,
|
||||
F: Future<Output = T> + Send + 'static,
|
||||
{
|
||||
Ok(value.await.body()?)
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Future<Output = Result<impl Serialize, impl Into<InvokeError>>> =====
|
||||
|
||||
pub struct ResultFutureTag;
|
||||
|
||||
pub trait ResultFutureKind {
|
||||
#[inline(always)]
|
||||
fn async_kind(&self) -> ResultFutureTag {
|
||||
ResultFutureTag
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: IpcResponse, E: Into<InvokeError>, F: Future<Output = Result<T, E>>> ResultFutureKind
|
||||
for F
|
||||
{
|
||||
}
|
||||
|
||||
impl ResultFutureTag {
|
||||
#[inline(always)]
|
||||
pub async fn future<T, E, F>(self, value: F) -> Result<InvokeResponseBody, InvokeError>
|
||||
where
|
||||
T: IpcResponse,
|
||||
E: Into<InvokeError>,
|
||||
F: Future<Output = Result<T, E>> + Send,
|
||||
{
|
||||
let response = value.await.map_err(Into::into)?;
|
||||
Ok(response.body()?)
|
||||
}
|
||||
}
|
||||
}
|
||||
344
crates/tauri/src/ipc/format_callback.rs
Normal file
344
crates/tauri/src/ipc/format_callback.rs
Normal file
@@ -0,0 +1,344 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use serde::Serialize;
|
||||
use serde_json::value::RawValue;
|
||||
use serialize_to_javascript::Serialized;
|
||||
|
||||
use super::CallbackFn;
|
||||
|
||||
/// The information about this is quite limited. On Chrome/Edge and Firefox, [the maximum string size is approximately 1 GB](https://stackoverflow.com/a/34958490).
|
||||
///
|
||||
/// [From MDN:](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/length#description)
|
||||
///
|
||||
/// ECMAScript 2016 (ed. 7) established a maximum length of 2^53 - 1 elements. Previously, no maximum length was specified.
|
||||
///
|
||||
/// In Firefox, strings have a maximum length of 2\*\*30 - 2 (~1GB). In versions prior to Firefox 65, the maximum length was 2\*\*28 - 1 (~256MB).
|
||||
const MAX_JSON_STR_LEN: usize = usize::pow(2, 30) - 2;
|
||||
|
||||
/// Minimum size JSON needs to be in order to convert it to JSON.parse with [`format_json`].
|
||||
// TODO: this number should be benchmarked and checked for optimal range, I set 10 KiB arbitrarily
|
||||
// we don't want to lose the gained object parsing time to extra allocations preparing it
|
||||
const MIN_JSON_PARSE_LEN: usize = 10_240;
|
||||
|
||||
/// Transforms & escapes a JSON value.
|
||||
///
|
||||
/// If it's an object or array, JSON.parse('{json}') is used, with the '{json}' string properly escaped.
|
||||
/// The return value of this function can be safely used on [`eval`](crate::Window#method.eval) calls.
|
||||
///
|
||||
/// Single quotes chosen because double quotes are already used in JSON. With single quotes, we only
|
||||
/// need to escape strings that include backslashes or single quotes. If we used double quotes, then
|
||||
/// there would be no cases that a string doesn't need escaping.
|
||||
///
|
||||
/// The function takes a closure to handle the escaped string in order to avoid unnecessary allocations.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The ability to safely escape JSON into a JSON.parse('{json}') relies entirely on 2 things.
|
||||
///
|
||||
/// 1. `serde_json`'s ability to correctly escape and format json into a string.
|
||||
/// 2. JavaScript engines not accepting anything except another unescaped, literal single quote
|
||||
/// character to end a string that was opened with it.
|
||||
fn serialize_js_with<F: FnOnce(&str) -> String>(
|
||||
json_string: String,
|
||||
options: serialize_to_javascript::Options,
|
||||
cb: F,
|
||||
) -> crate::Result<String> {
|
||||
// get a raw &str representation of a serialized json value.
|
||||
|
||||
let raw = RawValue::from_string(json_string)?;
|
||||
|
||||
// from here we know json.len() > 1 because an empty string is not a valid json value.
|
||||
let json = raw.get();
|
||||
let first = json.as_bytes()[0];
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
if first == b'"' {
|
||||
assert!(
|
||||
json.len() < MAX_JSON_STR_LEN,
|
||||
"passing a string larger than the max JavaScript literal string size"
|
||||
)
|
||||
}
|
||||
|
||||
let return_val = if json.len() > MIN_JSON_PARSE_LEN && (first == b'{' || first == b'[') {
|
||||
let serialized = Serialized::new(&raw, &options).into_string();
|
||||
// only use JSON.parse('{arg}') for arrays and objects less than the limit
|
||||
// smaller literals do not benefit from being parsed from json
|
||||
if serialized.len() < MAX_JSON_STR_LEN {
|
||||
cb(&serialized)
|
||||
} else {
|
||||
cb(json)
|
||||
}
|
||||
} else {
|
||||
cb(json)
|
||||
};
|
||||
|
||||
Ok(return_val)
|
||||
}
|
||||
|
||||
/// Formats a function name and a serializable argument to be evaluated as callback.
|
||||
///
|
||||
/// See [`format_raw`] for more information.
|
||||
pub fn format<T: Serialize>(function_name: CallbackFn, arg: &T) -> crate::Result<String> {
|
||||
format_raw(function_name, serde_json::to_string(arg)?)
|
||||
}
|
||||
|
||||
/// Formats a function name and a raw JSON string argument to be evaluated as callback.
|
||||
///
|
||||
/// This will serialize primitive JSON types (e.g. booleans, strings, numbers, etc.) as JavaScript literals,
|
||||
/// but will serialize arrays and objects whose serialized JSON string is smaller than 1 GB and larger
|
||||
/// than 10 KiB with `JSON.parse('...')`.
|
||||
/// See [json-parse-benchmark](https://github.com/GoogleChromeLabs/json-parse-benchmark).
|
||||
pub fn format_raw(function_name: CallbackFn, json_string: String) -> crate::Result<String> {
|
||||
let callback_id = function_name.0;
|
||||
serialize_js_with(json_string, Default::default(), |arg| {
|
||||
format_raw_js(callback_id, arg)
|
||||
})
|
||||
}
|
||||
|
||||
/// Formats a function name and a JavaScript string argument to be evaluated as callback.
|
||||
pub fn format_raw_js(callback_id: u32, js: impl AsRef<str>) -> String {
|
||||
fn format_inner(callback_id: u32, js: &str) -> String {
|
||||
format!("window.__TAURI_INTERNALS__.runCallback({callback_id}, {js})")
|
||||
}
|
||||
format_inner(callback_id, js.as_ref())
|
||||
}
|
||||
|
||||
/// Formats a serializable Result type to its Promise response.
|
||||
///
|
||||
/// See [`format_result_raw`] for more information.
|
||||
pub fn format_result<T: Serialize, E: Serialize>(
|
||||
result: Result<T, E>,
|
||||
success_callback: CallbackFn,
|
||||
error_callback: CallbackFn,
|
||||
) -> crate::Result<String> {
|
||||
match result {
|
||||
Ok(res) => format(success_callback, &res),
|
||||
Err(err) => format(error_callback, &err),
|
||||
}
|
||||
}
|
||||
|
||||
/// Formats a Result type of raw JSON strings to its Promise response.
|
||||
/// Useful for Promises handling.
|
||||
/// If the Result `is_ok()`, the callback will be the `success_callback` function name and the argument will be the Ok value.
|
||||
/// If the Result `is_err()`, the callback will be the `error_callback` function name and the argument will be the Err value.
|
||||
///
|
||||
/// * `result` the Result to check
|
||||
/// * `success_callback` the function name of the Ok callback. Usually the `resolve` of the JS Promise.
|
||||
/// * `error_callback` the function name of the Err callback. Usually the `reject` of the JS Promise.
|
||||
///
|
||||
/// Note that the callback strings are automatically generated by the `invoke` helper.
|
||||
pub fn format_result_raw(
|
||||
raw_result: Result<String, String>,
|
||||
success_callback: CallbackFn,
|
||||
error_callback: CallbackFn,
|
||||
) -> crate::Result<String> {
|
||||
match raw_result {
|
||||
Ok(res) => format_raw(success_callback, res),
|
||||
Err(err) => format_raw(error_callback, err),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use quickcheck::{Arbitrary, Gen};
|
||||
use quickcheck_macros::quickcheck;
|
||||
|
||||
impl Arbitrary for CallbackFn {
|
||||
fn arbitrary(g: &mut Gen) -> CallbackFn {
|
||||
CallbackFn(u32::arbitrary(g))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct JsonStr(String);
|
||||
|
||||
impl Arbitrary for JsonStr {
|
||||
fn arbitrary(g: &mut Gen) -> Self {
|
||||
if bool::arbitrary(g) {
|
||||
Self(format!(
|
||||
"{{ {}: {} }}",
|
||||
serde_json::to_string(&String::arbitrary(g)).unwrap(),
|
||||
serde_json::to_string(&String::arbitrary(g)).unwrap()
|
||||
))
|
||||
} else {
|
||||
Self(serde_json::to_string(&String::arbitrary(g)).unwrap())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_js<T: Serialize>(value: &T) -> crate::Result<String> {
|
||||
serialize_js_with(serde_json::to_string(value)?, Default::default(), |v| {
|
||||
v.into()
|
||||
})
|
||||
}
|
||||
|
||||
fn serialize_js_raw(value: impl Into<String>) -> crate::Result<String> {
|
||||
serialize_js_with(value.into(), Default::default(), |v| v.into())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize_js() {
|
||||
assert_eq!(serialize_js(&()).unwrap(), "null");
|
||||
assert_eq!(serialize_js(&5i32).unwrap(), "5");
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct JsonObj {
|
||||
value: String,
|
||||
}
|
||||
|
||||
let raw_str = "T".repeat(MIN_JSON_PARSE_LEN);
|
||||
assert_eq!(serialize_js(&raw_str).unwrap(), format!("\"{raw_str}\""));
|
||||
|
||||
assert_eq!(
|
||||
serialize_js(&JsonObj {
|
||||
value: raw_str.clone()
|
||||
})
|
||||
.unwrap(),
|
||||
format!("JSON.parse('{{\"value\":\"{raw_str}\"}}')")
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
serialize_js(&JsonObj {
|
||||
value: format!("\"{raw_str}\"")
|
||||
})
|
||||
.unwrap(),
|
||||
format!("JSON.parse('{{\"value\":\"\\\\\"{raw_str}\\\\\"\"}}')")
|
||||
);
|
||||
|
||||
let dangerous_json = RawValue::from_string(
|
||||
r#"{"test":"don\\🚀🐱👤\\'t forget to escape me!🚀🐱👤","te🚀🐱👤st2":"don't forget to escape me!","test3":"\\🚀🐱👤\\\\'''\\\\🚀🐱👤\\\\🚀🐱👤\\'''''"}"#.into()
|
||||
).unwrap();
|
||||
|
||||
let definitely_escaped_dangerous_json = format!(
|
||||
"JSON.parse('{}')",
|
||||
dangerous_json
|
||||
.get()
|
||||
.replace('\\', "\\\\")
|
||||
.replace('\'', "\\'")
|
||||
);
|
||||
let escape_single_quoted_json_test =
|
||||
serialize_to_javascript::Serialized::new(&dangerous_json, &Default::default()).into_string();
|
||||
|
||||
let result = r#"JSON.parse('{"test":"don\\\\🚀🐱👤\\\\\'t forget to escape me!🚀🐱👤","te🚀🐱👤st2":"don\'t forget to escape me!","test3":"\\\\🚀🐱👤\\\\\\\\\'\'\'\\\\\\\\🚀🐱👤\\\\\\\\🚀🐱👤\\\\\'\'\'\'\'"}')"#;
|
||||
assert_eq!(definitely_escaped_dangerous_json, result);
|
||||
assert_eq!(escape_single_quoted_json_test, result);
|
||||
}
|
||||
|
||||
// check arbitrary strings in the format callback function
|
||||
#[quickcheck]
|
||||
fn qc_formatting(f: CallbackFn, a: String) -> bool {
|
||||
// call format callback
|
||||
let fc = format(f, &a).unwrap();
|
||||
fc.contains(&format!(
|
||||
"window.__TAURI_INTERNALS__.runCallback({}, JSON.parse('{}'))",
|
||||
f.0,
|
||||
serde_json::Value::String(a.clone()),
|
||||
)) || fc.contains(&format!(
|
||||
r#"window.__TAURI_INTERNALS__.runCallback({}, {})"#,
|
||||
f.0,
|
||||
serde_json::Value::String(a),
|
||||
))
|
||||
}
|
||||
|
||||
// check arbitrary strings in format_result
|
||||
#[quickcheck]
|
||||
fn qc_format_res(result: Result<String, String>, c: CallbackFn, ec: CallbackFn) -> bool {
|
||||
let resp = format_result(result.clone(), c, ec).expect("failed to format callback result");
|
||||
let (function, value) = match result {
|
||||
Ok(v) => (c, v),
|
||||
Err(e) => (ec, e),
|
||||
};
|
||||
|
||||
resp.contains(&format!(
|
||||
r#"window.__TAURI_INTERNALS__.runCallback({}, {})"#,
|
||||
function.0,
|
||||
serde_json::Value::String(value),
|
||||
))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize_js_raw() {
|
||||
assert_eq!(serialize_js_raw("null").unwrap(), "null");
|
||||
assert_eq!(serialize_js_raw("5").unwrap(), "5");
|
||||
assert_eq!(
|
||||
serialize_js_raw("{ \"x\": [1, 2, 3] }").unwrap(),
|
||||
"{ \"x\": [1, 2, 3] }"
|
||||
);
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct JsonObj {
|
||||
value: String,
|
||||
}
|
||||
|
||||
let raw_str = "T".repeat(MIN_JSON_PARSE_LEN);
|
||||
assert_eq!(
|
||||
serialize_js_raw(format!("\"{raw_str}\"")).unwrap(),
|
||||
format!("\"{raw_str}\"")
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
serialize_js_raw(format!("{{\"value\":\"{raw_str}\"}}")).unwrap(),
|
||||
format!("JSON.parse('{{\"value\":\"{raw_str}\"}}')")
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
serialize_js(&JsonObj {
|
||||
value: format!("\"{raw_str}\"")
|
||||
})
|
||||
.unwrap(),
|
||||
format!("JSON.parse('{{\"value\":\"\\\\\"{raw_str}\\\\\"\"}}')")
|
||||
);
|
||||
|
||||
let dangerous_json = RawValue::from_string(
|
||||
r#"{"test":"don\\🚀🐱👤\\'t forget to escape me!🚀🐱👤","te🚀🐱👤st2":"don't forget to escape me!","test3":"\\🚀🐱👤\\\\'''\\\\🚀🐱👤\\\\🚀🐱👤\\'''''"}"#.into()
|
||||
).unwrap();
|
||||
|
||||
let definitely_escaped_dangerous_json = format!(
|
||||
"JSON.parse('{}')",
|
||||
dangerous_json
|
||||
.get()
|
||||
.replace('\\', "\\\\")
|
||||
.replace('\'', "\\'")
|
||||
);
|
||||
let escape_single_quoted_json_test =
|
||||
serialize_to_javascript::Serialized::new(&dangerous_json, &Default::default()).into_string();
|
||||
|
||||
let result = r#"JSON.parse('{"test":"don\\\\🚀🐱👤\\\\\'t forget to escape me!🚀🐱👤","te🚀🐱👤st2":"don\'t forget to escape me!","test3":"\\\\🚀🐱👤\\\\\\\\\'\'\'\\\\\\\\🚀🐱👤\\\\\\\\🚀🐱👤\\\\\'\'\'\'\'"}')"#;
|
||||
assert_eq!(definitely_escaped_dangerous_json, result);
|
||||
assert_eq!(escape_single_quoted_json_test, result);
|
||||
}
|
||||
|
||||
// check arbitrary strings in the format callback function
|
||||
#[quickcheck]
|
||||
fn qc_formatting_raw(f: CallbackFn, a: JsonStr) -> bool {
|
||||
let a = a.0;
|
||||
// call format callback
|
||||
let fc = format_raw(f, a.clone()).unwrap();
|
||||
fc.contains(&format!(
|
||||
r#"window.__TAURI_INTERNALS__.runCallback({}, JSON.parse('{}'))"#,
|
||||
f.0, a
|
||||
)) || fc.contains(&format!(
|
||||
r#"window.__TAURI_INTERNALS__.runCallback({}, {})"#,
|
||||
f.0, a
|
||||
))
|
||||
}
|
||||
|
||||
// check arbitrary strings in format_result
|
||||
#[quickcheck]
|
||||
fn qc_format_raw_res(result: Result<JsonStr, JsonStr>, c: CallbackFn, ec: CallbackFn) -> bool {
|
||||
let result = result.map(|v| v.0).map_err(|e| e.0);
|
||||
let resp = format_result_raw(result.clone(), c, ec).expect("failed to format callback result");
|
||||
let (function, value) = match result {
|
||||
Ok(v) => (c, v),
|
||||
Err(e) => (ec, e),
|
||||
};
|
||||
|
||||
resp.contains(&format!(
|
||||
r#"window.__TAURI_INTERNALS__.runCallback({}, {})"#,
|
||||
function.0, value
|
||||
))
|
||||
}
|
||||
}
|
||||
606
crates/tauri/src/ipc/mod.rs
Normal file
606
crates/tauri/src/ipc/mod.rs
Normal file
@@ -0,0 +1,606 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! Types and functions related to Inter Procedure Call(IPC).
|
||||
//!
|
||||
//! This module includes utilities to send messages to the JS layer of the webview.
|
||||
|
||||
use std::{
|
||||
future::Future,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use http::HeaderMap;
|
||||
use serde::{
|
||||
Deserialize, Serialize,
|
||||
de::{DeserializeOwned, IntoDeserializer},
|
||||
};
|
||||
use serde_json::Value as JsonValue;
|
||||
pub use serialize_to_javascript::Options as SerializeOptions;
|
||||
use tauri_macros::default_runtime;
|
||||
use tauri_utils::acl::resolved::ResolvedCommand;
|
||||
|
||||
use crate::{Runtime, StateManager, webview::Webview};
|
||||
|
||||
mod authority;
|
||||
#[cfg(feature = "dynamic-acl")]
|
||||
mod capability_builder;
|
||||
pub(crate) mod channel;
|
||||
mod command;
|
||||
pub(crate) mod format_callback;
|
||||
pub(crate) mod protocol;
|
||||
|
||||
pub use authority::{
|
||||
CommandScope, GlobalScope, Origin, RuntimeAuthority, ScopeObject, ScopeObjectMatch, ScopeValue,
|
||||
};
|
||||
#[cfg(feature = "dynamic-acl")]
|
||||
pub use capability_builder::{CapabilityBuilder, RuntimeCapability};
|
||||
pub use channel::{Channel, JavaScriptChannelId};
|
||||
pub use command::{CommandArg, CommandItem, private};
|
||||
|
||||
/// A closure that is run every time Tauri receives a message it doesn't explicitly handle.
|
||||
pub type InvokeHandler<R> = dyn Fn(Invoke<R>) -> bool + Send + Sync + 'static;
|
||||
|
||||
/// A closure that is responsible for respond a JS message.
|
||||
pub type InvokeResponder<R> =
|
||||
dyn Fn(&Webview<R>, &str, &InvokeResponse, CallbackFn, CallbackFn) + Send + Sync + 'static;
|
||||
/// Similar to [`InvokeResponder`] but taking owned arguments.
|
||||
pub type OwnedInvokeResponder<R> =
|
||||
dyn FnOnce(Webview<R>, String, InvokeResponse, CallbackFn, CallbackFn) + Send + 'static;
|
||||
|
||||
/// Possible values of an IPC payload.
|
||||
///
|
||||
/// ### Android
|
||||
/// On Android, [InvokeBody::Raw] is not supported. The enum will always contain [InvokeBody::Json].
|
||||
/// When targeting Android Devices, consider passing raw bytes as a base64 [[std::string::String]], which is still more efficient than passing them as a number array in [InvokeBody::Json]
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub enum InvokeBody {
|
||||
/// Json payload.
|
||||
Json(JsonValue),
|
||||
/// Bytes payload.
|
||||
Raw(Vec<u8>),
|
||||
}
|
||||
|
||||
impl Default for InvokeBody {
|
||||
fn default() -> Self {
|
||||
Self::Json(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<JsonValue> for InvokeBody {
|
||||
fn from(value: JsonValue) -> Self {
|
||||
Self::Json(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<u8>> for InvokeBody {
|
||||
fn from(value: Vec<u8>) -> Self {
|
||||
Self::Raw(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl InvokeBody {
|
||||
#[cfg(mobile)]
|
||||
pub(crate) fn into_json(self) -> JsonValue {
|
||||
match self {
|
||||
Self::Json(v) => v,
|
||||
Self::Raw(v) => {
|
||||
JsonValue::Array(v.into_iter().map(|n| JsonValue::Number(n.into())).collect())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Possible values of an IPC response.
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub enum InvokeResponseBody {
|
||||
/// Json payload.
|
||||
Json(String),
|
||||
/// Bytes payload.
|
||||
Raw(Vec<u8>),
|
||||
}
|
||||
|
||||
impl From<String> for InvokeResponseBody {
|
||||
fn from(value: String) -> Self {
|
||||
Self::Json(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<u8>> for InvokeResponseBody {
|
||||
fn from(value: Vec<u8>) -> Self {
|
||||
Self::Raw(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<InvokeBody> for InvokeResponseBody {
|
||||
fn from(value: InvokeBody) -> Self {
|
||||
match value {
|
||||
InvokeBody::Json(v) => Self::Json(serde_json::to_string(&v).unwrap()),
|
||||
InvokeBody::Raw(v) => Self::Raw(v),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IpcResponse for InvokeResponseBody {
|
||||
fn body(self) -> crate::Result<InvokeResponseBody> {
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl InvokeResponseBody {
|
||||
/// Attempts to deserialize the response.
|
||||
pub fn deserialize<T: DeserializeOwned>(self) -> serde_json::Result<T> {
|
||||
match self {
|
||||
Self::Json(v) => serde_json::from_str(&v),
|
||||
Self::Raw(v) => T::deserialize(v.into_deserializer()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The IPC request.
|
||||
///
|
||||
/// Includes the `body` and `headers` parameters of a Tauri command invocation.
|
||||
/// This allows commands to accept raw bytes - on all platforms except Android.
|
||||
#[derive(Debug)]
|
||||
pub struct Request<'a> {
|
||||
body: &'a InvokeBody,
|
||||
headers: &'a HeaderMap,
|
||||
}
|
||||
|
||||
impl Request<'_> {
|
||||
/// The request body.
|
||||
pub fn body(&self) -> &InvokeBody {
|
||||
self.body
|
||||
}
|
||||
|
||||
/// The request headers.
|
||||
pub fn headers(&self) -> &HeaderMap {
|
||||
self.headers
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, R: Runtime> CommandArg<'a, R> for Request<'a> {
|
||||
/// Returns the invoke [`Request`].
|
||||
fn from_command(command: CommandItem<'a, R>) -> Result<Self, InvokeError> {
|
||||
Ok(Self {
|
||||
body: command.message.payload(),
|
||||
headers: command.message.headers(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Marks a type as a response to an IPC call.
|
||||
pub trait IpcResponse {
|
||||
/// Resolve the IPC response body.
|
||||
fn body(self) -> crate::Result<InvokeResponseBody>;
|
||||
}
|
||||
|
||||
impl<T: Serialize> IpcResponse for T {
|
||||
fn body(self) -> crate::Result<InvokeResponseBody> {
|
||||
serde_json::to_string(&self)
|
||||
.map(Into::into)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
/// The IPC response.
|
||||
pub struct Response {
|
||||
body: InvokeResponseBody,
|
||||
}
|
||||
|
||||
impl IpcResponse for Response {
|
||||
fn body(self) -> crate::Result<InvokeResponseBody> {
|
||||
Ok(self.body)
|
||||
}
|
||||
}
|
||||
|
||||
impl Response {
|
||||
/// Defines a response with the given body.
|
||||
pub fn new(body: impl Into<InvokeResponseBody>) -> Self {
|
||||
Self { body: body.into() }
|
||||
}
|
||||
}
|
||||
|
||||
/// The message and resolver given to a custom command.
|
||||
///
|
||||
/// This struct is used internally by macros and is explicitly **NOT** stable.
|
||||
#[default_runtime(crate::Wry, wry)]
|
||||
pub struct Invoke<R: Runtime> {
|
||||
/// The message passed.
|
||||
pub message: InvokeMessage<R>,
|
||||
|
||||
/// The resolver of the message.
|
||||
pub resolver: InvokeResolver<R>,
|
||||
|
||||
/// Resolved ACL for this IPC invoke.
|
||||
pub acl: Option<Vec<ResolvedCommand>>,
|
||||
}
|
||||
|
||||
/// Error response from an [`InvokeMessage`].
|
||||
#[derive(Debug)]
|
||||
pub struct InvokeError(pub serde_json::Value);
|
||||
|
||||
impl InvokeError {
|
||||
/// Create an [`InvokeError`] as a string of the [`std::error::Error`] message.
|
||||
#[inline(always)]
|
||||
pub fn from_error<E: std::error::Error>(error: E) -> Self {
|
||||
Self(serde_json::Value::String(error.to_string()))
|
||||
}
|
||||
|
||||
/// Create an [`InvokeError`] as a string of the [`anyhow::Error`] message.
|
||||
#[inline(always)]
|
||||
pub fn from_anyhow(error: anyhow::Error) -> Self {
|
||||
Self(serde_json::Value::String(format!("{error:#}")))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Serialize> From<T> for InvokeError {
|
||||
#[inline]
|
||||
fn from(value: T) -> Self {
|
||||
serde_json::to_value(value)
|
||||
.map(Self)
|
||||
.unwrap_or_else(Self::from_error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::Error> for InvokeError {
|
||||
#[inline(always)]
|
||||
fn from(error: crate::Error) -> Self {
|
||||
Self(serde_json::Value::String(error.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Response from a [`InvokeMessage`] passed to the [`InvokeResolver`].
|
||||
#[derive(Debug)]
|
||||
pub enum InvokeResponse {
|
||||
/// Resolve the promise.
|
||||
Ok(InvokeResponseBody),
|
||||
/// Reject the promise.
|
||||
Err(InvokeError),
|
||||
}
|
||||
|
||||
impl<T: IpcResponse, E: Into<InvokeError>> From<Result<T, E>> for InvokeResponse {
|
||||
#[inline]
|
||||
fn from(result: Result<T, E>) -> Self {
|
||||
match result {
|
||||
Ok(ok) => match ok.body() {
|
||||
Ok(value) => Self::Ok(value),
|
||||
Err(err) => Self::Err(InvokeError::from_error(err)),
|
||||
},
|
||||
Err(err) => Self::Err(err.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<InvokeError> for InvokeResponse {
|
||||
fn from(error: InvokeError) -> Self {
|
||||
Self::Err(error)
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolver of a invoke message.
|
||||
#[default_runtime(crate::Wry, wry)]
|
||||
pub struct InvokeResolver<R: Runtime> {
|
||||
webview: Webview<R>,
|
||||
responder: Arc<Mutex<Option<Box<OwnedInvokeResponder<R>>>>>,
|
||||
cmd: String,
|
||||
pub(crate) callback: CallbackFn,
|
||||
pub(crate) error: CallbackFn,
|
||||
}
|
||||
|
||||
impl<R: Runtime> Clone for InvokeResolver<R> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
webview: self.webview.clone(),
|
||||
responder: self.responder.clone(),
|
||||
cmd: self.cmd.clone(),
|
||||
callback: self.callback,
|
||||
error: self.error,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> InvokeResolver<R> {
|
||||
pub(crate) fn new(
|
||||
webview: Webview<R>,
|
||||
responder: Arc<Mutex<Option<Box<OwnedInvokeResponder<R>>>>>,
|
||||
cmd: String,
|
||||
callback: CallbackFn,
|
||||
error: CallbackFn,
|
||||
) -> Self {
|
||||
Self {
|
||||
webview,
|
||||
responder,
|
||||
cmd,
|
||||
callback,
|
||||
error,
|
||||
}
|
||||
}
|
||||
|
||||
/// Reply to the invoke promise with an async task.
|
||||
pub fn respond_async<T, F>(self, task: F)
|
||||
where
|
||||
T: IpcResponse,
|
||||
F: Future<Output = Result<T, InvokeError>> + Send + 'static,
|
||||
{
|
||||
crate::async_runtime::spawn(async move {
|
||||
Self::return_task(
|
||||
self.webview,
|
||||
self.responder,
|
||||
task,
|
||||
self.cmd,
|
||||
self.callback,
|
||||
self.error,
|
||||
)
|
||||
.await;
|
||||
});
|
||||
}
|
||||
|
||||
/// Reply to the invoke promise with an async task which is already serialized.
|
||||
pub fn respond_async_serialized<F>(self, task: F)
|
||||
where
|
||||
F: Future<Output = Result<InvokeResponseBody, InvokeError>> + Send + 'static,
|
||||
{
|
||||
// Dynamic dispatch the call in dev for a faster compile time
|
||||
// TODO: Revisit this and see if we can do this for the release build as well if the performance hit is not a problem
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
self.respond_async_serialized_dyn(Box::pin(task))
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
self.respond_async_serialized_inner(task)
|
||||
}
|
||||
}
|
||||
|
||||
/// Dynamic dispatch the [`Self::respond_async_serialized`] call
|
||||
#[cfg(debug_assertions)]
|
||||
fn respond_async_serialized_dyn(
|
||||
self,
|
||||
task: std::pin::Pin<
|
||||
Box<dyn Future<Output = Result<InvokeResponseBody, InvokeError>> + Send + 'static>,
|
||||
>,
|
||||
) {
|
||||
self.respond_async_serialized_inner(task)
|
||||
}
|
||||
|
||||
/// Reply to the invoke promise with an async task which is already serialized.
|
||||
fn respond_async_serialized_inner<F>(self, task: F)
|
||||
where
|
||||
F: Future<Output = Result<InvokeResponseBody, InvokeError>> + Send + 'static,
|
||||
{
|
||||
crate::async_runtime::spawn(async move {
|
||||
let response = match task.await {
|
||||
Ok(ok) => InvokeResponse::Ok(ok),
|
||||
Err(err) => InvokeResponse::Err(err),
|
||||
};
|
||||
Self::return_result(
|
||||
self.webview,
|
||||
self.responder,
|
||||
response,
|
||||
self.cmd,
|
||||
self.callback,
|
||||
self.error,
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
/// Reply to the invoke promise with a serializable value.
|
||||
pub fn respond<T: IpcResponse>(self, value: Result<T, InvokeError>) {
|
||||
Self::return_result(
|
||||
self.webview,
|
||||
self.responder,
|
||||
value.into(),
|
||||
self.cmd,
|
||||
self.callback,
|
||||
self.error,
|
||||
)
|
||||
}
|
||||
|
||||
/// Resolve the invoke promise with a value.
|
||||
pub fn resolve<T: IpcResponse>(self, value: T) {
|
||||
self.respond(Ok(value))
|
||||
}
|
||||
|
||||
/// Reject the invoke promise with a value.
|
||||
pub fn reject<T: Serialize>(self, value: T) {
|
||||
Self::return_result(
|
||||
self.webview,
|
||||
self.responder,
|
||||
Result::<(), _>::Err(value).into(),
|
||||
self.cmd,
|
||||
self.callback,
|
||||
self.error,
|
||||
)
|
||||
}
|
||||
|
||||
/// Reject the invoke promise with an [`InvokeError`].
|
||||
pub fn invoke_error(self, error: InvokeError) {
|
||||
Self::return_result(
|
||||
self.webview,
|
||||
self.responder,
|
||||
error.into(),
|
||||
self.cmd,
|
||||
self.callback,
|
||||
self.error,
|
||||
)
|
||||
}
|
||||
|
||||
/// Asynchronously executes the given task
|
||||
/// and evaluates its Result to the JS promise described by the `success_callback` and `error_callback` function names.
|
||||
///
|
||||
/// If the Result `is_ok()`, the callback will be the `success_callback` function name and the argument will be the Ok value.
|
||||
/// If the Result `is_err()`, the callback will be the `error_callback` function name and the argument will be the Err value.
|
||||
pub async fn return_task<T, F>(
|
||||
webview: Webview<R>,
|
||||
responder: Arc<Mutex<Option<Box<OwnedInvokeResponder<R>>>>>,
|
||||
task: F,
|
||||
cmd: String,
|
||||
success_callback: CallbackFn,
|
||||
error_callback: CallbackFn,
|
||||
) where
|
||||
T: IpcResponse,
|
||||
F: Future<Output = Result<T, InvokeError>> + Send + 'static,
|
||||
{
|
||||
let result = task.await;
|
||||
Self::return_closure(
|
||||
webview,
|
||||
responder,
|
||||
|| result,
|
||||
cmd,
|
||||
success_callback,
|
||||
error_callback,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn return_closure<T: IpcResponse, F: FnOnce() -> Result<T, InvokeError>>(
|
||||
webview: Webview<R>,
|
||||
responder: Arc<Mutex<Option<Box<OwnedInvokeResponder<R>>>>>,
|
||||
f: F,
|
||||
cmd: String,
|
||||
success_callback: CallbackFn,
|
||||
error_callback: CallbackFn,
|
||||
) {
|
||||
Self::return_result(
|
||||
webview,
|
||||
responder,
|
||||
f().into(),
|
||||
cmd,
|
||||
success_callback,
|
||||
error_callback,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn return_result(
|
||||
webview: Webview<R>,
|
||||
responder: Arc<Mutex<Option<Box<OwnedInvokeResponder<R>>>>>,
|
||||
response: InvokeResponse,
|
||||
cmd: String,
|
||||
success_callback: CallbackFn,
|
||||
error_callback: CallbackFn,
|
||||
) {
|
||||
(responder.lock().unwrap().take().expect("resolver consumed"))(
|
||||
webview,
|
||||
cmd,
|
||||
response,
|
||||
success_callback,
|
||||
error_callback,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// An invoke message.
|
||||
#[default_runtime(crate::Wry, wry)]
|
||||
#[derive(Debug)]
|
||||
pub struct InvokeMessage<R: Runtime> {
|
||||
/// The webview that received the invoke message.
|
||||
pub(crate) webview: Webview<R>,
|
||||
/// The IPC command.
|
||||
pub(crate) command: String,
|
||||
/// The JSON argument passed on the invoke message.
|
||||
pub(crate) payload: InvokeBody,
|
||||
/// The request headers.
|
||||
pub(crate) headers: HeaderMap,
|
||||
}
|
||||
|
||||
impl<R: Runtime> Clone for InvokeMessage<R> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
webview: self.webview.clone(),
|
||||
command: self.command.clone(),
|
||||
payload: self.payload.clone(),
|
||||
headers: self.headers.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> InvokeMessage<R> {
|
||||
/// Create an new [`InvokeMessage`] from a payload send by a webview.
|
||||
pub(crate) fn new(
|
||||
webview: Webview<R>,
|
||||
command: String,
|
||||
payload: InvokeBody,
|
||||
headers: HeaderMap,
|
||||
) -> Self {
|
||||
Self {
|
||||
webview,
|
||||
command,
|
||||
payload,
|
||||
headers,
|
||||
}
|
||||
}
|
||||
|
||||
/// The invoke command.
|
||||
#[inline(always)]
|
||||
pub fn command(&self) -> &str {
|
||||
&self.command
|
||||
}
|
||||
|
||||
/// The webview that received the invoke.
|
||||
#[inline(always)]
|
||||
pub fn webview(&self) -> Webview<R> {
|
||||
self.webview.clone()
|
||||
}
|
||||
|
||||
/// A reference to webview that received the invoke.
|
||||
#[inline(always)]
|
||||
pub fn webview_ref(&self) -> &Webview<R> {
|
||||
&self.webview
|
||||
}
|
||||
|
||||
/// A reference to the payload the invoke received.
|
||||
#[inline(always)]
|
||||
pub fn payload(&self) -> &InvokeBody {
|
||||
&self.payload
|
||||
}
|
||||
|
||||
// TODO: make private or remove in v3
|
||||
/// The state manager associated with the application
|
||||
#[deprecated(note = "Use `Manager::state` to access the state: `self.webview_ref().state()`")]
|
||||
#[inline(always)]
|
||||
pub fn state(&self) -> Arc<StateManager> {
|
||||
self.webview.manager.state.clone()
|
||||
}
|
||||
|
||||
// TODO: make private or remove in v3
|
||||
/// A reference to the state manager associated with application.
|
||||
#[deprecated(note = "Use `Manager::state` to access the state: `self.webview_ref().state()`")]
|
||||
#[inline(always)]
|
||||
pub fn state_ref(&self) -> &StateManager {
|
||||
&self.webview.manager.state
|
||||
}
|
||||
|
||||
/// The request headers.
|
||||
#[inline(always)]
|
||||
pub fn headers(&self) -> &HeaderMap {
|
||||
&self.headers
|
||||
}
|
||||
}
|
||||
|
||||
/// The `Callback` type is the return value of the `transformCallback` JavaScript function.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)]
|
||||
pub struct CallbackFn(pub u32);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn deserialize_invoke_response_body() {
|
||||
let json = InvokeResponseBody::Json("[1, 123, 1231]".to_string());
|
||||
assert_eq!(json.deserialize::<Vec<u16>>().unwrap(), vec![1, 123, 1231]);
|
||||
|
||||
let json = InvokeResponseBody::Json("\"string value\"".to_string());
|
||||
assert_eq!(json.deserialize::<String>().unwrap(), "string value");
|
||||
|
||||
let json = InvokeResponseBody::Json("\"string value\"".to_string());
|
||||
assert!(json.deserialize::<Vec<u16>>().is_err());
|
||||
|
||||
let values = vec![1, 2, 3, 4, 5, 6, 1];
|
||||
let raw = InvokeResponseBody::Raw(values.clone());
|
||||
assert_eq!(raw.deserialize::<Vec<u8>>().unwrap(), values);
|
||||
}
|
||||
}
|
||||
753
crates/tauri/src/ipc/protocol.rs
Normal file
753
crates/tauri/src/ipc/protocol.rs
Normal file
@@ -0,0 +1,753 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
|
||||
use crate::{
|
||||
Runtime,
|
||||
ipc::InvokeResponseBody,
|
||||
manager::AppManager,
|
||||
webview::{InvokeRequest, UriSchemeProtocolHandler},
|
||||
};
|
||||
use http::{
|
||||
HeaderValue, Method, Request, StatusCode,
|
||||
header::{
|
||||
ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_EXPOSE_HEADERS,
|
||||
CONTENT_TYPE,
|
||||
},
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
use super::{CallbackFn, InvokeResponse};
|
||||
|
||||
const TAURI_CALLBACK_HEADER_NAME: &str = "Tauri-Callback";
|
||||
const TAURI_ERROR_HEADER_NAME: &str = "Tauri-Error";
|
||||
const TAURI_INVOKE_KEY_HEADER_NAME: &str = "Tauri-Invoke-Key";
|
||||
|
||||
const TAURI_RESPONSE_HEADER_NAME: &str = "Tauri-Response";
|
||||
const TAURI_RESPONSE_HEADER_ERROR: &str = "error";
|
||||
const TAURI_RESPONSE_HEADER_OK: &str = "ok";
|
||||
|
||||
pub fn message_handler<R: Runtime>(
|
||||
manager: Arc<AppManager<R>>,
|
||||
) -> crate::runtime::webview::WebviewIpcHandler<crate::EventLoopMessage, R> {
|
||||
Box::new(move |webview, request| handle_ipc_message(request, &manager, &webview.label))
|
||||
}
|
||||
|
||||
pub fn get<R: Runtime>(manager: Arc<AppManager<R>>) -> UriSchemeProtocolHandler {
|
||||
Box::new(move |label, request, responder| {
|
||||
#[cfg(feature = "tracing")]
|
||||
let span = tracing::trace_span!(
|
||||
"ipc::request",
|
||||
kind = "custom-protocol",
|
||||
request = tracing::field::Empty
|
||||
)
|
||||
.entered();
|
||||
|
||||
let respond = move |mut response: http::Response<Cow<'static, [u8]>>| {
|
||||
response
|
||||
.headers_mut()
|
||||
.insert(ACCESS_CONTROL_ALLOW_ORIGIN, HeaderValue::from_static("*"));
|
||||
response.headers_mut().insert(
|
||||
ACCESS_CONTROL_EXPOSE_HEADERS,
|
||||
HeaderValue::from_static(TAURI_RESPONSE_HEADER_NAME),
|
||||
);
|
||||
responder.respond(response);
|
||||
};
|
||||
|
||||
match *request.method() {
|
||||
Method::POST => {
|
||||
if let Some(webview) = manager.get_webview(label) {
|
||||
match parse_invoke_request(&manager, request) {
|
||||
Ok(request) => {
|
||||
#[cfg(feature = "tracing")]
|
||||
span.record(
|
||||
"request",
|
||||
match &request.body {
|
||||
super::InvokeBody::Json(j) => serde_json::to_string(j).unwrap(),
|
||||
super::InvokeBody::Raw(b) => serde_json::to_string(b).unwrap(),
|
||||
},
|
||||
);
|
||||
#[cfg(feature = "tracing")]
|
||||
let request_span = tracing::trace_span!("ipc::request::handle", cmd = request.cmd);
|
||||
|
||||
webview.on_message(
|
||||
request,
|
||||
Box::new(move |_webview, _cmd, response, _callback, _error| {
|
||||
#[cfg(feature = "tracing")]
|
||||
let _respond_span = tracing::trace_span!(
|
||||
parent: &request_span,
|
||||
"ipc::request::respond"
|
||||
)
|
||||
.entered();
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
let response_span = match &response {
|
||||
InvokeResponse::Ok(InvokeResponseBody::Json(v)) => tracing::trace_span!(
|
||||
"ipc::request::response",
|
||||
response = v,
|
||||
mime_type = tracing::field::Empty
|
||||
)
|
||||
.entered(),
|
||||
InvokeResponse::Ok(InvokeResponseBody::Raw(v)) => tracing::trace_span!(
|
||||
"ipc::request::response",
|
||||
response = format!("{v:?}"),
|
||||
mime_type = tracing::field::Empty
|
||||
)
|
||||
.entered(),
|
||||
InvokeResponse::Err(e) => tracing::trace_span!(
|
||||
"ipc::request::response",
|
||||
error = format!("{e:?}"),
|
||||
mime_type = tracing::field::Empty
|
||||
)
|
||||
.entered(),
|
||||
};
|
||||
|
||||
let response_header = match &response {
|
||||
InvokeResponse::Ok(_) => TAURI_RESPONSE_HEADER_OK,
|
||||
InvokeResponse::Err(_) => TAURI_RESPONSE_HEADER_ERROR,
|
||||
};
|
||||
|
||||
let (mut response, mime_type) = match response {
|
||||
InvokeResponse::Ok(InvokeResponseBody::Json(v)) => (
|
||||
http::Response::new(v.into_bytes().into()),
|
||||
mime::APPLICATION_JSON,
|
||||
),
|
||||
InvokeResponse::Ok(InvokeResponseBody::Raw(v)) => (
|
||||
http::Response::new(v.into()),
|
||||
mime::APPLICATION_OCTET_STREAM,
|
||||
),
|
||||
InvokeResponse::Err(e) => (
|
||||
http::Response::new(serde_json::to_vec(&e.0).unwrap().into()),
|
||||
mime::APPLICATION_JSON,
|
||||
),
|
||||
};
|
||||
|
||||
response
|
||||
.headers_mut()
|
||||
.insert(TAURI_RESPONSE_HEADER_NAME, response_header.parse().unwrap());
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
response_span.record("mime_type", mime_type.essence_str());
|
||||
|
||||
response.headers_mut().insert(
|
||||
CONTENT_TYPE,
|
||||
HeaderValue::from_str(mime_type.essence_str()).unwrap(),
|
||||
);
|
||||
|
||||
respond(response);
|
||||
}),
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
respond(
|
||||
http::Response::builder()
|
||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
.header(CONTENT_TYPE, mime::TEXT_PLAIN.essence_str())
|
||||
.body(e.into_bytes().into())
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
respond(
|
||||
http::Response::builder()
|
||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
.header(CONTENT_TYPE, mime::TEXT_PLAIN.essence_str())
|
||||
.body("failed to acquire webview reference".as_bytes().into())
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Method::OPTIONS => {
|
||||
let mut r = http::Response::new(Vec::new().into());
|
||||
r.headers_mut()
|
||||
.insert(ACCESS_CONTROL_ALLOW_HEADERS, HeaderValue::from_static("*"));
|
||||
|
||||
respond(r);
|
||||
}
|
||||
|
||||
_ => {
|
||||
let mut r = http::Response::new("only POST and OPTIONS are allowed".as_bytes().into());
|
||||
*r.status_mut() = StatusCode::METHOD_NOT_ALLOWED;
|
||||
r.headers_mut().insert(
|
||||
CONTENT_TYPE,
|
||||
HeaderValue::from_str(mime::TEXT_PLAIN.essence_str()).unwrap(),
|
||||
);
|
||||
respond(r);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_ipc_message<R: Runtime>(request: Request<String>, manager: &AppManager<R>, label: &str) {
|
||||
let Some(webview) = manager.get_webview(label) else {
|
||||
return;
|
||||
};
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
let _span = tracing::trace_span!(
|
||||
"ipc::request",
|
||||
kind = "post-message",
|
||||
uri = request.uri().to_string(),
|
||||
request = request.body()
|
||||
)
|
||||
.entered();
|
||||
|
||||
use serde::{Deserialize, Deserializer};
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct HeaderMap(http::HeaderMap);
|
||||
|
||||
impl<'de> Deserialize<'de> for HeaderMap {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let map = std::collections::HashMap::<String, String>::deserialize(deserializer)?;
|
||||
let mut headers = http::HeaderMap::default();
|
||||
for (key, value) in map {
|
||||
if let (Ok(key), Ok(value)) = (
|
||||
http::header::HeaderName::from_bytes(key.as_bytes()),
|
||||
http::HeaderValue::from_str(&value),
|
||||
) {
|
||||
headers.insert(key, value);
|
||||
} else {
|
||||
return Err(serde::de::Error::custom(format!(
|
||||
"invalid header `{key}` `{value}`"
|
||||
)));
|
||||
}
|
||||
}
|
||||
Ok(Self(headers))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct RequestOptions {
|
||||
#[serde(default)]
|
||||
headers: HeaderMap,
|
||||
#[serde(default)]
|
||||
custom_protocol_ipc_blocked: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Message {
|
||||
cmd: String,
|
||||
callback: CallbackFn,
|
||||
error: CallbackFn,
|
||||
payload: serde_json::Value,
|
||||
options: Option<RequestOptions>,
|
||||
#[serde(rename = "__TAURI_INVOKE_KEY__")]
|
||||
invoke_key: String,
|
||||
}
|
||||
|
||||
#[allow(unused_mut)]
|
||||
let mut invoke_message: Option<crate::Result<Message>> = None;
|
||||
|
||||
#[cfg(feature = "isolation")]
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
struct IsolationMessage<'a> {
|
||||
cmd: String,
|
||||
callback: CallbackFn,
|
||||
error: CallbackFn,
|
||||
payload: crate::utils::pattern::isolation::RawIsolationPayload<'a>,
|
||||
options: Option<RequestOptions>,
|
||||
#[serde(rename = "__TAURI_INVOKE_KEY__")]
|
||||
invoke_key: String,
|
||||
}
|
||||
|
||||
if let crate::Pattern::Isolation { crypto_keys, .. } = &*manager.pattern {
|
||||
#[cfg(feature = "tracing")]
|
||||
let _span = tracing::trace_span!("ipc::request::decrypt_isolation_payload").entered();
|
||||
|
||||
invoke_message.replace(
|
||||
serde_json::from_str::<IsolationMessage<'_>>(request.body())
|
||||
.map_err(Into::into)
|
||||
.and_then(|message| {
|
||||
let is_raw =
|
||||
message.payload.content_type() == &mime::APPLICATION_OCTET_STREAM.to_string();
|
||||
let payload = crypto_keys.decrypt(message.payload)?;
|
||||
Ok(Message {
|
||||
cmd: message.cmd,
|
||||
callback: message.callback,
|
||||
error: message.error,
|
||||
payload: if is_raw {
|
||||
payload.into()
|
||||
} else {
|
||||
serde_json::from_slice(&payload)?
|
||||
},
|
||||
options: message.options,
|
||||
invoke_key: message.invoke_key,
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let message = invoke_message.unwrap_or_else(|| {
|
||||
#[cfg(feature = "tracing")]
|
||||
let _span = tracing::trace_span!("ipc::request::deserialize").entered();
|
||||
serde_json::from_str::<Message>(request.body()).map_err(Into::into)
|
||||
});
|
||||
|
||||
match message {
|
||||
Ok(message) => {
|
||||
let options = message.options.unwrap_or_default();
|
||||
|
||||
let request = InvokeRequest {
|
||||
cmd: message.cmd,
|
||||
callback: message.callback,
|
||||
error: message.error,
|
||||
url: Url::parse(&request.uri().to_string()).expect("invalid IPC request URL"),
|
||||
body: message.payload.into(),
|
||||
headers: options.headers.0,
|
||||
invoke_key: message.invoke_key,
|
||||
};
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
let request_span = tracing::trace_span!("ipc::request::handle", cmd = request.cmd);
|
||||
|
||||
webview.on_message(
|
||||
request,
|
||||
Box::new(move |webview, cmd, response, callback, error| {
|
||||
use crate::ipc::Channel;
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
let _respond_span = tracing::trace_span!(
|
||||
parent: &request_span,
|
||||
"ipc::request::respond"
|
||||
)
|
||||
.entered();
|
||||
|
||||
fn responder_eval<R: Runtime>(
|
||||
webview: &crate::Webview<R>,
|
||||
js: crate::Result<String>,
|
||||
error: CallbackFn,
|
||||
) {
|
||||
let eval_js = js.unwrap_or_else(|e| {
|
||||
crate::ipc::format_callback::format(error, &e.to_string())
|
||||
.expect("unable to serialize response error string to json")
|
||||
});
|
||||
let _ = webview.eval(eval_js);
|
||||
}
|
||||
|
||||
let can_use_channel_for_response = cmd != crate::ipc::channel::FETCH_CHANNEL_DATA_COMMAND
|
||||
&& !options.custom_protocol_ipc_blocked;
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
let mime_type = match &response {
|
||||
InvokeResponse::Ok(InvokeResponseBody::Json(_)) => mime::APPLICATION_JSON,
|
||||
InvokeResponse::Ok(InvokeResponseBody::Raw(_)) => mime::APPLICATION_OCTET_STREAM,
|
||||
InvokeResponse::Err(_) => mime::APPLICATION_JSON,
|
||||
};
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
let _response_span = match &response {
|
||||
InvokeResponse::Ok(InvokeResponseBody::Json(v)) => tracing::trace_span!(
|
||||
"ipc::request::response",
|
||||
response = v,
|
||||
mime_type = mime_type.essence_str()
|
||||
)
|
||||
.entered(),
|
||||
InvokeResponse::Ok(InvokeResponseBody::Raw(v)) => tracing::trace_span!(
|
||||
"ipc::request::response",
|
||||
response = format!("{v:?}"),
|
||||
mime_type = mime_type.essence_str()
|
||||
)
|
||||
.entered(),
|
||||
InvokeResponse::Err(e) => tracing::trace_span!(
|
||||
"ipc::request::response",
|
||||
response = format!("{e:?}"),
|
||||
mime_type = mime_type.essence_str()
|
||||
)
|
||||
.entered(),
|
||||
};
|
||||
|
||||
match response {
|
||||
InvokeResponse::Ok(InvokeResponseBody::Json(v)) => {
|
||||
if !(cfg!(target_os = "macos") || cfg!(target_os = "ios"))
|
||||
&& (v.starts_with('{') || v.starts_with('['))
|
||||
&& can_use_channel_for_response
|
||||
{
|
||||
let _ =
|
||||
Channel::from_callback_fn(webview, callback).send(InvokeResponseBody::Json(v));
|
||||
} else {
|
||||
responder_eval(
|
||||
&webview,
|
||||
crate::ipc::format_callback::format_result_raw(
|
||||
Result::<_, String>::Ok(v),
|
||||
callback,
|
||||
error,
|
||||
),
|
||||
error,
|
||||
)
|
||||
}
|
||||
}
|
||||
InvokeResponse::Ok(InvokeResponseBody::Raw(v)) => {
|
||||
if cfg!(target_os = "macos")
|
||||
|| cfg!(target_os = "ios")
|
||||
|| !can_use_channel_for_response
|
||||
{
|
||||
responder_eval(
|
||||
&webview,
|
||||
crate::ipc::format_callback::format_result(
|
||||
Result::<_, ()>::Ok(v),
|
||||
callback,
|
||||
error,
|
||||
),
|
||||
error,
|
||||
);
|
||||
} else {
|
||||
let _ =
|
||||
Channel::from_callback_fn(webview, callback).send(InvokeResponseBody::Raw(v));
|
||||
}
|
||||
}
|
||||
InvokeResponse::Err(e) => responder_eval(
|
||||
&webview,
|
||||
crate::ipc::format_callback::format_result(
|
||||
Result::<(), _>::Err(&e.0),
|
||||
callback,
|
||||
error,
|
||||
),
|
||||
error,
|
||||
),
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::trace!("ipc.request.error {e}");
|
||||
|
||||
let _ = webview.eval(format!(
|
||||
r#"console.error({})"#,
|
||||
serde_json::Value::String(e.to_string())
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_invoke_request<R: Runtime>(
|
||||
#[allow(unused_variables)] manager: &AppManager<R>,
|
||||
request: http::Request<Vec<u8>>,
|
||||
) -> std::result::Result<InvokeRequest, String> {
|
||||
#[allow(unused_mut)]
|
||||
let (parts, mut body) = request.into_parts();
|
||||
|
||||
// skip leading `/`
|
||||
let cmd = percent_encoding::percent_decode(&parts.uri.path().as_bytes()[1..])
|
||||
.decode_utf8_lossy()
|
||||
.to_string();
|
||||
|
||||
// on Android we cannot read the request body
|
||||
// so we must ignore it because some commands use the IPC for faster response
|
||||
let has_payload = !body.is_empty();
|
||||
|
||||
#[allow(unused_mut)]
|
||||
let mut content_type = parts
|
||||
.headers
|
||||
.get(http::header::CONTENT_TYPE)
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.map(|mime| mime.parse())
|
||||
.unwrap_or(Ok(mime::APPLICATION_OCTET_STREAM))
|
||||
.map_err(|_| "unknown content type")?;
|
||||
|
||||
#[cfg(feature = "isolation")]
|
||||
if let crate::Pattern::Isolation { crypto_keys, .. } = &*manager.pattern {
|
||||
// if the platform does not support request body, we ignore it
|
||||
if has_payload {
|
||||
#[cfg(feature = "tracing")]
|
||||
let _span = tracing::trace_span!("ipc::request::decrypt_isolation_payload").entered();
|
||||
|
||||
(body, content_type) = crate::utils::pattern::isolation::RawIsolationPayload::try_from(&body)
|
||||
.and_then(|raw| {
|
||||
let content_type = raw
|
||||
.content_type()
|
||||
.parse()
|
||||
.unwrap_or(mime::APPLICATION_OCTET_STREAM);
|
||||
Ok((crypto_keys.decrypt(raw)?, content_type))
|
||||
})
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
}
|
||||
|
||||
let invoke_key = parts
|
||||
.headers
|
||||
.get(TAURI_INVOKE_KEY_HEADER_NAME)
|
||||
.ok_or("missing Tauri-Invoke-Key header")?
|
||||
.to_str()
|
||||
.map_err(|_| "Tauri invoke key header value must be a string")?
|
||||
.to_owned();
|
||||
|
||||
let url = Url::parse(
|
||||
parts
|
||||
.headers
|
||||
.get("Origin")
|
||||
.ok_or("missing Origin header")?
|
||||
.to_str()
|
||||
.map_err(|_| "Origin header value must be a string")?,
|
||||
)
|
||||
.map_err(|_| "Origin header is not a valid URL")?;
|
||||
|
||||
let callback = CallbackFn(
|
||||
parts
|
||||
.headers
|
||||
.get(TAURI_CALLBACK_HEADER_NAME)
|
||||
.ok_or("missing Tauri-Callback header")?
|
||||
.to_str()
|
||||
.map_err(|_| "Tauri callback header value must be a string")?
|
||||
.parse()
|
||||
.map_err(|_| "Tauri callback header value must be a numeric string")?,
|
||||
);
|
||||
let error = CallbackFn(
|
||||
parts
|
||||
.headers
|
||||
.get(TAURI_ERROR_HEADER_NAME)
|
||||
.ok_or("missing Tauri-Error header")?
|
||||
.to_str()
|
||||
.map_err(|_| "Tauri error header value must be a string")?
|
||||
.parse()
|
||||
.map_err(|_| "Tauri error header value must be a numeric string")?,
|
||||
);
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
let span = tracing::trace_span!("ipc::request::deserialize").entered();
|
||||
|
||||
let body = if content_type == mime::APPLICATION_OCTET_STREAM {
|
||||
body.into()
|
||||
} else if content_type == mime::APPLICATION_JSON {
|
||||
// if the platform does not support request body, we ignore it
|
||||
if has_payload {
|
||||
serde_json::from_slice::<serde_json::Value>(&body)
|
||||
.map_err(|e| e.to_string())?
|
||||
.into()
|
||||
} else {
|
||||
serde_json::Value::Object(Default::default()).into()
|
||||
}
|
||||
} else {
|
||||
return Err(format!("content type {content_type} is not implemented"));
|
||||
};
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
drop(span);
|
||||
|
||||
let payload = InvokeRequest {
|
||||
cmd,
|
||||
callback,
|
||||
error,
|
||||
url,
|
||||
body,
|
||||
headers: parts.headers,
|
||||
invoke_key,
|
||||
};
|
||||
|
||||
Ok(payload)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::str::FromStr;
|
||||
|
||||
use super::*;
|
||||
use crate::{StateManager, Wry, ipc::InvokeBody, manager::AppManager, plugin::PluginStore};
|
||||
use http::header::*;
|
||||
use serde_json::json;
|
||||
use tauri_macros::generate_context;
|
||||
|
||||
#[test]
|
||||
fn parse_invoke_request() {
|
||||
let context = generate_context!("test/fixture/src-tauri/tauri.conf.json", crate, test = true);
|
||||
let manager: AppManager<Wry> = AppManager::with_handlers(
|
||||
context,
|
||||
PluginStore::default(),
|
||||
Box::new(|_| false),
|
||||
None,
|
||||
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||
None,
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
StateManager::new(),
|
||||
Default::default(),
|
||||
#[cfg(all(desktop, feature = "tray-icon"))]
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
"".into(),
|
||||
None,
|
||||
crate::generate_invoke_key().unwrap(),
|
||||
);
|
||||
|
||||
let cmd = "write_something";
|
||||
let url = "tauri://localhost";
|
||||
let invoke_key = "1234ahdsjkl123";
|
||||
let callback = 12378123;
|
||||
let error = 6243;
|
||||
let mut headers = HeaderMap::from_iter(vec![
|
||||
(
|
||||
CONTENT_TYPE,
|
||||
HeaderValue::from_str(mime::APPLICATION_OCTET_STREAM.as_ref()).unwrap(),
|
||||
),
|
||||
(
|
||||
HeaderName::from_str(TAURI_INVOKE_KEY_HEADER_NAME).unwrap(),
|
||||
HeaderValue::from_str(invoke_key).unwrap(),
|
||||
),
|
||||
(
|
||||
HeaderName::from_str(TAURI_CALLBACK_HEADER_NAME).unwrap(),
|
||||
HeaderValue::from_str(&callback.to_string()).unwrap(),
|
||||
),
|
||||
(
|
||||
HeaderName::from_str(TAURI_ERROR_HEADER_NAME).unwrap(),
|
||||
HeaderValue::from_str(&error.to_string()).unwrap(),
|
||||
),
|
||||
(ORIGIN, HeaderValue::from_str("tauri://localhost").unwrap()),
|
||||
]);
|
||||
|
||||
let mut request = Request::builder().uri(format!("ipc://localhost/{cmd}"));
|
||||
*request.headers_mut().unwrap() = headers.clone();
|
||||
|
||||
let body = vec![123, 31, 45];
|
||||
let request = request.body(body.clone()).unwrap();
|
||||
let invoke_request = super::parse_invoke_request(&manager, request).unwrap();
|
||||
|
||||
assert_eq!(invoke_request.cmd, cmd);
|
||||
assert_eq!(invoke_request.callback.0, callback);
|
||||
assert_eq!(invoke_request.error.0, error);
|
||||
assert_eq!(invoke_request.invoke_key, invoke_key);
|
||||
assert_eq!(invoke_request.url, url.parse().unwrap());
|
||||
assert_eq!(invoke_request.headers, headers);
|
||||
assert_eq!(invoke_request.body, InvokeBody::Raw(body));
|
||||
|
||||
let body = json!({
|
||||
"key": 1,
|
||||
"anotherKey": "asda",
|
||||
});
|
||||
|
||||
headers.insert(
|
||||
CONTENT_TYPE,
|
||||
HeaderValue::from_str(mime::APPLICATION_JSON.as_ref()).unwrap(),
|
||||
);
|
||||
|
||||
let mut request = Request::builder().uri(format!("ipc://localhost/{cmd}"));
|
||||
*request.headers_mut().unwrap() = headers.clone();
|
||||
|
||||
let request = request.body(serde_json::to_vec(&body).unwrap()).unwrap();
|
||||
let invoke_request = super::parse_invoke_request(&manager, request).unwrap();
|
||||
|
||||
assert_eq!(invoke_request.headers, headers);
|
||||
assert_eq!(invoke_request.body, InvokeBody::Json(body));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "isolation")]
|
||||
fn parse_invoke_request_isolation() {
|
||||
let context = generate_context!(
|
||||
"test/fixture/isolation/src-tauri/tauri.conf.json",
|
||||
crate,
|
||||
test = false
|
||||
);
|
||||
|
||||
let crate::pattern::Pattern::Isolation { crypto_keys, .. } = &context.pattern else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
let mut nonce = [0u8; 12];
|
||||
getrandom::fill(&mut nonce).unwrap();
|
||||
|
||||
let body_raw = vec![1, 41, 65, 12, 78];
|
||||
let body_bytes = crypto_keys.aes_gcm().encrypt(&nonce, &body_raw).unwrap();
|
||||
let isolation_payload_raw = json!({
|
||||
"nonce": nonce,
|
||||
"payload": body_bytes,
|
||||
"contentType": mime::APPLICATION_OCTET_STREAM.to_string(),
|
||||
});
|
||||
|
||||
let body_json = json!({
|
||||
"key": 1,
|
||||
"anotherKey": "string"
|
||||
});
|
||||
let body_bytes = crypto_keys
|
||||
.aes_gcm()
|
||||
.encrypt(&nonce, &serde_json::to_vec(&body_json).unwrap())
|
||||
.unwrap();
|
||||
let isolation_payload_json = json!({
|
||||
"nonce": nonce,
|
||||
"payload": body_bytes,
|
||||
"contentType": mime::APPLICATION_JSON.to_string(),
|
||||
});
|
||||
|
||||
let manager: AppManager<Wry> = AppManager::with_handlers(
|
||||
context,
|
||||
PluginStore::default(),
|
||||
Box::new(|_| false),
|
||||
None,
|
||||
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||
None,
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
StateManager::new(),
|
||||
Default::default(),
|
||||
#[cfg(all(desktop, feature = "tray-icon"))]
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
"".into(),
|
||||
None,
|
||||
crate::generate_invoke_key().unwrap(),
|
||||
);
|
||||
|
||||
let cmd = "write_something";
|
||||
let url = "tauri://localhost";
|
||||
let invoke_key = "1234ahdsjkl123";
|
||||
let callback = 12378123;
|
||||
let error = 6243;
|
||||
|
||||
let headers = HeaderMap::from_iter(vec![
|
||||
(
|
||||
CONTENT_TYPE,
|
||||
HeaderValue::from_str(mime::APPLICATION_JSON.as_ref()).unwrap(),
|
||||
),
|
||||
(
|
||||
HeaderName::from_str(TAURI_INVOKE_KEY_HEADER_NAME).unwrap(),
|
||||
HeaderValue::from_str(invoke_key).unwrap(),
|
||||
),
|
||||
(
|
||||
HeaderName::from_str(TAURI_CALLBACK_HEADER_NAME).unwrap(),
|
||||
HeaderValue::from_str(&callback.to_string()).unwrap(),
|
||||
),
|
||||
(
|
||||
HeaderName::from_str(TAURI_ERROR_HEADER_NAME).unwrap(),
|
||||
HeaderValue::from_str(&error.to_string()).unwrap(),
|
||||
),
|
||||
(ORIGIN, HeaderValue::from_str("tauri://localhost").unwrap()),
|
||||
]);
|
||||
|
||||
let mut request = Request::builder().uri(format!("ipc://localhost/{cmd}"));
|
||||
*request.headers_mut().unwrap() = headers.clone();
|
||||
let body = serde_json::to_vec(&isolation_payload_raw).unwrap();
|
||||
let request = request.body(body).unwrap();
|
||||
let invoke_request = super::parse_invoke_request(&manager, request).unwrap();
|
||||
|
||||
assert_eq!(invoke_request.cmd, cmd);
|
||||
assert_eq!(invoke_request.callback.0, callback);
|
||||
assert_eq!(invoke_request.error.0, error);
|
||||
assert_eq!(invoke_request.invoke_key, invoke_key);
|
||||
assert_eq!(invoke_request.url, url.parse().unwrap());
|
||||
assert_eq!(invoke_request.headers, headers);
|
||||
assert_eq!(invoke_request.body, InvokeBody::Raw(body_raw));
|
||||
|
||||
let mut request = Request::builder().uri(format!("ipc://localhost/{cmd}"));
|
||||
*request.headers_mut().unwrap() = headers.clone();
|
||||
let body = serde_json::to_vec(&isolation_payload_json).unwrap();
|
||||
let request = request.body(body).unwrap();
|
||||
let invoke_request = super::parse_invoke_request(&manager, request).unwrap();
|
||||
|
||||
assert_eq!(invoke_request.headers, headers);
|
||||
assert_eq!(invoke_request.body, InvokeBody::Json(body_json));
|
||||
}
|
||||
}
|
||||
1267
crates/tauri/src/lib.rs
Normal file
1267
crates/tauri/src/lib.rs
Normal file
File diff suppressed because it is too large
Load Diff
100
crates/tauri/src/manager/menu.rs
Normal file
100
crates/tauri/src/manager/menu.rs
Normal file
@@ -0,0 +1,100 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Arc, Mutex, MutexGuard},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
AppHandle, Runtime, Window,
|
||||
menu::{Menu, MenuEvent, MenuId},
|
||||
};
|
||||
|
||||
pub struct MenuManager<R: Runtime> {
|
||||
/// A set containing a reference to the active menus, including
|
||||
/// the app-wide menu and the window-specific menus
|
||||
///
|
||||
/// This should be mainly used to access [`Menu::haccel`]
|
||||
/// to setup the accelerator handling in the event loop
|
||||
pub menus: Arc<Mutex<HashMap<MenuId, Menu<R>>>>,
|
||||
/// The menu set to all windows.
|
||||
pub menu: Mutex<Option<Menu<R>>>,
|
||||
/// Menu event listeners to all windows.
|
||||
pub global_event_listeners: Mutex<Vec<crate::app::GlobalMenuEventListener<AppHandle<R>>>>,
|
||||
/// Menu event listeners to specific windows.
|
||||
pub event_listeners: Mutex<HashMap<String, crate::app::GlobalMenuEventListener<Window<R>>>>,
|
||||
}
|
||||
|
||||
impl<R: Runtime> MenuManager<R> {
|
||||
/// App-wide menu.
|
||||
pub fn menu_lock(&self) -> MutexGuard<'_, Option<Menu<R>>> {
|
||||
self.menu.lock().expect("poisoned menu mutex")
|
||||
}
|
||||
|
||||
pub fn menus_stash_lock(&self) -> MutexGuard<'_, HashMap<MenuId, Menu<R>>> {
|
||||
self.menus.lock().expect("poisoned menu mutex")
|
||||
}
|
||||
|
||||
pub fn is_menu_in_use<I: PartialEq<MenuId>>(&self, id: &I) -> bool {
|
||||
self
|
||||
.menu_lock()
|
||||
.as_ref()
|
||||
.map(|m| id.eq(m.id()))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn insert_menu_into_stash(&self, menu: &Menu<R>) {
|
||||
self
|
||||
.menus_stash_lock()
|
||||
.insert(menu.id().clone(), menu.clone());
|
||||
}
|
||||
|
||||
pub(crate) fn prepare_window_menu_creation_handler(
|
||||
&self,
|
||||
window_menu: Option<&crate::window::WindowMenu<R>>,
|
||||
#[allow(unused)] theme: Option<tauri_utils::Theme>,
|
||||
) -> Option<impl Fn(tauri_runtime::window::RawWindow<'_>) + use<R>> {
|
||||
let window_menu = window_menu?;
|
||||
|
||||
self
|
||||
.menus_stash_lock()
|
||||
.insert(window_menu.menu.id().clone(), window_menu.menu.clone());
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
return None;
|
||||
|
||||
#[cfg_attr(target_os = "macos", allow(unused_variables, unreachable_code))]
|
||||
{
|
||||
let menu = window_menu.menu.clone();
|
||||
Some(move |raw: tauri_runtime::window::RawWindow<'_>| {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let theme = theme
|
||||
.map(crate::menu::map_to_menu_theme)
|
||||
.unwrap_or(muda::MenuTheme::Auto);
|
||||
let _ = unsafe { menu.inner().init_for_hwnd_with_theme(raw.hwnd as _, theme) };
|
||||
}
|
||||
#[cfg(any(
|
||||
target_os = "linux",
|
||||
target_os = "dragonfly",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "openbsd"
|
||||
))]
|
||||
let _ = menu
|
||||
.inner()
|
||||
.init_for_gtk_window(raw.gtk_window, raw.default_vbox);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_menu_event<F: Fn(&AppHandle<R>, MenuEvent) + Send + Sync + 'static>(&self, handler: F) {
|
||||
self
|
||||
.global_event_listeners
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(Box::new(handler));
|
||||
}
|
||||
}
|
||||
1040
crates/tauri/src/manager/mod.rs
Normal file
1040
crates/tauri/src/manager/mod.rs
Normal file
File diff suppressed because it is too large
Load Diff
92
crates/tauri/src/manager/tray.rs
Normal file
92
crates/tauri/src/manager/tray.rs
Normal file
@@ -0,0 +1,92 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
AppHandle, Manager, Resource, ResourceId, Runtime,
|
||||
app::GlobalTrayIconEventListener,
|
||||
image::Image,
|
||||
tray::{TrayIcon, TrayIconEvent, TrayIconId},
|
||||
};
|
||||
|
||||
pub struct TrayManager<R: Runtime> {
|
||||
/// icon passed in from [`tauri_utils::config::TrayIconConfig::icon_path`],
|
||||
/// used when creating tray icons from the configs.
|
||||
pub(crate) icon: Option<Image<'static>>,
|
||||
/// Tray icons
|
||||
pub(crate) icons: Mutex<Vec<(TrayIconId, ResourceId)>>,
|
||||
/// Global Tray icon event listeners.
|
||||
pub(crate) global_event_listeners: Mutex<Vec<GlobalTrayIconEventListener<AppHandle<R>>>>,
|
||||
/// Tray icon event listeners.
|
||||
pub(crate) event_listeners: Mutex<HashMap<TrayIconId, GlobalTrayIconEventListener<TrayIcon<R>>>>,
|
||||
}
|
||||
|
||||
impl<R: Runtime> fmt::Debug for TrayManager<R> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("TrayManager")
|
||||
.field("icon", &self.icon)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> TrayManager<R> {
|
||||
pub fn on_tray_icon_event<F: Fn(&AppHandle<R>, TrayIconEvent) + Send + Sync + 'static>(
|
||||
&self,
|
||||
handler: F,
|
||||
) {
|
||||
self
|
||||
.global_event_listeners
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(Box::new(handler));
|
||||
}
|
||||
|
||||
pub fn tray_by_id<'a, I>(&self, app: &AppHandle<R>, id: &'a I) -> Option<TrayIcon<R>>
|
||||
where
|
||||
I: ?Sized,
|
||||
TrayIconId: PartialEq<&'a I>,
|
||||
{
|
||||
let icons = self.icons.lock().unwrap();
|
||||
icons.iter().find_map(|(tray_icon_id, rid)| {
|
||||
if tray_icon_id == &id {
|
||||
let icon = app.resources_table().get::<TrayIcon<R>>(*rid).ok()?;
|
||||
Some(Arc::unwrap_or_clone(icon))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn tray_resource_by_id<'a, I>(&self, id: &'a I) -> Option<ResourceId>
|
||||
where
|
||||
I: ?Sized,
|
||||
TrayIconId: PartialEq<&'a I>,
|
||||
{
|
||||
let icons = self.icons.lock().unwrap();
|
||||
icons.iter().find_map(|(tray_icon_id, rid)| {
|
||||
if tray_icon_id == &id {
|
||||
Some(*rid)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn remove_tray_by_id<'a, I>(&self, app: &AppHandle<R>, id: &'a I) -> Option<TrayIcon<R>>
|
||||
where
|
||||
I: ?Sized,
|
||||
TrayIconId: PartialEq<&'a I>,
|
||||
{
|
||||
let rid = self.tray_resource_by_id(id)?;
|
||||
let icon = app.resources_table().take::<TrayIcon<R>>(rid).ok()?;
|
||||
let icon_to_return = icon.clone();
|
||||
icon.close();
|
||||
Some(Arc::unwrap_or_clone(icon_to_return))
|
||||
}
|
||||
}
|
||||
793
crates/tauri/src/manager/webview.rs
Normal file
793
crates/tauri/src/manager/webview.rs
Normal file
@@ -0,0 +1,793 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
collections::{HashMap, HashSet},
|
||||
fmt,
|
||||
fs::create_dir_all,
|
||||
sync::{Arc, Mutex, MutexGuard},
|
||||
};
|
||||
|
||||
use serde::Serialize;
|
||||
use serialize_to_javascript::{DefaultTemplate, Template, default_template};
|
||||
use tauri_runtime::{
|
||||
webview::{DetachedWebview, InitializationScript, PendingWebview},
|
||||
window::DragDropEvent,
|
||||
};
|
||||
use tauri_utils::config::WebviewUrl;
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
EventLoopMessage, EventTarget, Manager, Runtime, Scopes, UriSchemeContext, Webview, Window,
|
||||
app::{GlobalWebviewEventListener, OnPageLoad, UriSchemeResponder, WebviewEvent},
|
||||
ipc::InvokeHandler,
|
||||
pattern::PatternJavascript,
|
||||
sealed::ManagerBase,
|
||||
webview::PageLoadPayload,
|
||||
};
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||
use crate::app::OnWebContentProcessTerminate;
|
||||
|
||||
use super::{
|
||||
window::{DRAG_DROP_EVENT, DRAG_ENTER_EVENT, DRAG_LEAVE_EVENT, DRAG_OVER_EVENT, DragDropPayload},
|
||||
{AppManager, EmitPayload},
|
||||
};
|
||||
|
||||
// we need to proxy the dev server on mobile because we can't use `localhost`, so we use the local IP address
|
||||
// and we do not get a secure context without the custom protocol that proxies to the dev server
|
||||
// additionally, we need the custom protocol to inject the initialization scripts on Android
|
||||
// must also keep in sync with the `let mut response` assignment in prepare_uri_scheme_protocol
|
||||
pub(crate) const PROXY_DEV_SERVER: bool = cfg!(all(dev, mobile));
|
||||
|
||||
pub(crate) const PROCESS_IPC_MESSAGE_FN: &str =
|
||||
include_str!("../../scripts/process-ipc-message-fn.js");
|
||||
|
||||
#[cfg(feature = "isolation")]
|
||||
#[derive(Template)]
|
||||
#[default_template("../../scripts/isolation.js")]
|
||||
pub(crate) struct IsolationJavascript<'a> {
|
||||
pub(crate) isolation_src: &'a str,
|
||||
pub(crate) style: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[default_template("../../scripts/ipc.js")]
|
||||
pub(crate) struct IpcJavascript<'a> {
|
||||
pub(crate) isolation_origin: &'a str,
|
||||
}
|
||||
|
||||
/// Uses a custom URI scheme handler to resolve file requests
|
||||
pub struct UriSchemeProtocol<R: Runtime> {
|
||||
/// Handler for protocol
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub handler:
|
||||
Box<dyn Fn(UriSchemeContext<'_, R>, http::Request<Vec<u8>>, UriSchemeResponder) + Send + Sync>,
|
||||
}
|
||||
|
||||
pub struct WebviewManager<R: Runtime> {
|
||||
pub webviews: Mutex<HashMap<String, Webview<R>>>,
|
||||
/// The JS message handler.
|
||||
pub invoke_handler: Box<InvokeHandler<R>>,
|
||||
/// The page load hook, invoked when the webview performs a navigation.
|
||||
pub on_page_load: Option<Arc<OnPageLoad<R>>>,
|
||||
/// The permission request hook, invoked when the webview requests a permission.
|
||||
pub on_permission_request: Option<Arc<crate::webview::PermissionRequestHandler<R>>>,
|
||||
/// The web content process termination hook.
|
||||
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||
pub on_web_content_process_terminate: Option<Arc<OnWebContentProcessTerminate<R>>>,
|
||||
/// The webview protocols available to all webviews.
|
||||
pub uri_scheme_protocols: Mutex<HashMap<String, Arc<UriSchemeProtocol<R>>>>,
|
||||
/// Webview event listeners to all webviews.
|
||||
pub event_listeners: Arc<Vec<GlobalWebviewEventListener<R>>>,
|
||||
|
||||
/// The script that initializes the invoke system.
|
||||
pub invoke_initialization_script: String,
|
||||
|
||||
/// A runtime generated invoke key.
|
||||
pub(crate) invoke_key: String,
|
||||
}
|
||||
|
||||
impl<R: Runtime> fmt::Debug for WebviewManager<R> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("WebviewManager")
|
||||
.field(
|
||||
"invoke_initialization_script",
|
||||
&self.invoke_initialization_script,
|
||||
)
|
||||
.field("invoke_key", &self.invoke_key)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> WebviewManager<R> {
|
||||
pub(crate) fn register_uri_scheme_protocol<N: Into<String>>(
|
||||
&self,
|
||||
uri_scheme: N,
|
||||
protocol: Arc<UriSchemeProtocol<R>>,
|
||||
) {
|
||||
let uri_scheme = uri_scheme.into();
|
||||
self
|
||||
.uri_scheme_protocols
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(uri_scheme, protocol);
|
||||
}
|
||||
|
||||
/// Get a locked handle to the webviews.
|
||||
pub(crate) fn webviews_lock(&self) -> MutexGuard<'_, HashMap<String, Webview<R>>> {
|
||||
self.webviews.lock().expect("poisoned webview manager")
|
||||
}
|
||||
|
||||
fn prepare_pending_webview<M: Manager<R>>(
|
||||
&self,
|
||||
mut pending: PendingWebview<EventLoopMessage, R>,
|
||||
label: &str,
|
||||
window_label: &str,
|
||||
manager: &M,
|
||||
) -> crate::Result<PendingWebview<EventLoopMessage, R>> {
|
||||
let app_manager = manager.manager();
|
||||
|
||||
let plugin_init_scripts = app_manager
|
||||
.plugins
|
||||
.lock()
|
||||
.expect("poisoned plugin store")
|
||||
.initialization_script();
|
||||
|
||||
let pattern_init = PatternJavascript {
|
||||
pattern: (&*app_manager.pattern).into(),
|
||||
}
|
||||
.render_default(&Default::default())?;
|
||||
|
||||
let mut webview_attributes = pending.webview_attributes;
|
||||
|
||||
let use_https_scheme = webview_attributes.use_https_scheme;
|
||||
|
||||
let ipc_init = IpcJavascript {
|
||||
isolation_origin: &match &*app_manager.pattern {
|
||||
#[cfg(feature = "isolation")]
|
||||
crate::Pattern::Isolation { schema, .. } => {
|
||||
crate::pattern::format_real_schema(schema, use_https_scheme)
|
||||
}
|
||||
_ => "".to_owned(),
|
||||
},
|
||||
}
|
||||
.render_default(&Default::default())?;
|
||||
|
||||
let mut all_initialization_scripts: Vec<InitializationScript> = vec![];
|
||||
|
||||
fn main_frame_script(script: String) -> InitializationScript {
|
||||
InitializationScript {
|
||||
script,
|
||||
for_main_frame_only: true,
|
||||
}
|
||||
}
|
||||
|
||||
all_initialization_scripts.push(main_frame_script(
|
||||
r"
|
||||
Object.defineProperty(window, 'isTauri', {
|
||||
value: true,
|
||||
});
|
||||
|
||||
if (!window.__TAURI_INTERNALS__) {
|
||||
Object.defineProperty(window, '__TAURI_INTERNALS__', {
|
||||
value: {
|
||||
plugins: {}
|
||||
}
|
||||
})
|
||||
}
|
||||
"
|
||||
.to_owned(),
|
||||
));
|
||||
all_initialization_scripts.push(main_frame_script(self.invoke_initialization_script.clone()));
|
||||
all_initialization_scripts.push(main_frame_script(format!(
|
||||
r#"
|
||||
Object.defineProperty(window.__TAURI_INTERNALS__, 'metadata', {{
|
||||
value: {{
|
||||
currentWindow: {{ label: {current_window_label} }},
|
||||
currentWebview: {{ label: {current_webview_label} }}
|
||||
}}
|
||||
}})
|
||||
"#,
|
||||
current_window_label = serde_json::to_string(window_label)?,
|
||||
current_webview_label = serde_json::to_string(&label)?,
|
||||
)));
|
||||
all_initialization_scripts.push(main_frame_script(self.initialization_script(
|
||||
app_manager,
|
||||
&ipc_init.into_string(),
|
||||
&pattern_init.into_string(),
|
||||
use_https_scheme,
|
||||
)?));
|
||||
|
||||
all_initialization_scripts.extend(plugin_init_scripts);
|
||||
|
||||
#[cfg(feature = "isolation")]
|
||||
if let crate::Pattern::Isolation { schema, .. } = &*app_manager.pattern {
|
||||
all_initialization_scripts.push(main_frame_script(
|
||||
IsolationJavascript {
|
||||
isolation_src: &crate::pattern::format_real_schema(schema, use_https_scheme),
|
||||
style: tauri_utils::pattern::isolation::IFRAME_STYLE,
|
||||
}
|
||||
.render_default(&Default::default())?
|
||||
.into_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(plugin_global_api_scripts) = &*app_manager.plugin_global_api_scripts {
|
||||
for &script in plugin_global_api_scripts.iter() {
|
||||
all_initialization_scripts.push(main_frame_script(script.to_owned()));
|
||||
}
|
||||
}
|
||||
|
||||
// Prepend `all_initialization_scripts` to `webview_attributes.initialization_scripts`
|
||||
all_initialization_scripts.extend(webview_attributes.initialization_scripts);
|
||||
webview_attributes.initialization_scripts = all_initialization_scripts;
|
||||
|
||||
pending.webview_attributes = webview_attributes;
|
||||
|
||||
let mut registered_scheme_protocols = Vec::new();
|
||||
|
||||
for (uri_scheme, protocol) in &*self.uri_scheme_protocols.lock().unwrap() {
|
||||
registered_scheme_protocols.push(uri_scheme.clone());
|
||||
let protocol = protocol.clone();
|
||||
let app_handle = manager.app_handle().clone();
|
||||
|
||||
pending.register_uri_scheme_protocol(uri_scheme, move |webview_id, request, responder| {
|
||||
let context = UriSchemeContext {
|
||||
app_handle: &app_handle,
|
||||
webview_label: webview_id,
|
||||
};
|
||||
(protocol.handler)(context, request, UriSchemeResponder(responder))
|
||||
});
|
||||
}
|
||||
|
||||
let window_url = Url::parse(&pending.url).unwrap();
|
||||
let window_origin = if window_url.scheme() == "data" {
|
||||
"null".into()
|
||||
} else if (cfg!(windows) || cfg!(target_os = "android"))
|
||||
&& window_url.scheme() != "http"
|
||||
&& window_url.scheme() != "https"
|
||||
{
|
||||
crate::protocol::origin(window_url.scheme(), use_https_scheme)
|
||||
} else if let Some(host) = window_url.host() {
|
||||
format!(
|
||||
"{}://{}{}",
|
||||
window_url.scheme(),
|
||||
host,
|
||||
window_url
|
||||
.port()
|
||||
.map(|p| format!(":{p}"))
|
||||
.unwrap_or_default()
|
||||
)
|
||||
} else {
|
||||
"null".into()
|
||||
};
|
||||
|
||||
if !registered_scheme_protocols.contains(&"tauri".into()) {
|
||||
let web_resource_request_handler = pending.web_resource_request_handler.take();
|
||||
let protocol = crate::protocol::tauri::get(
|
||||
manager.manager_owned(),
|
||||
window_origin.clone(),
|
||||
web_resource_request_handler,
|
||||
);
|
||||
pending.register_uri_scheme_protocol("tauri", move |webview_id, request, responder| {
|
||||
protocol(webview_id, request, UriSchemeResponder(responder))
|
||||
});
|
||||
registered_scheme_protocols.push("tauri".into());
|
||||
}
|
||||
|
||||
if !registered_scheme_protocols.contains(&"ipc".into()) {
|
||||
let protocol = crate::ipc::protocol::get(manager.manager_owned());
|
||||
pending.register_uri_scheme_protocol("ipc", move |webview_id, request, responder| {
|
||||
protocol(webview_id, request, UriSchemeResponder(responder))
|
||||
});
|
||||
registered_scheme_protocols.push("ipc".into());
|
||||
}
|
||||
|
||||
let label = pending.label.clone();
|
||||
let app_manager_ = manager.manager_owned();
|
||||
let on_page_load_handler = pending.on_page_load_handler.take();
|
||||
pending
|
||||
.on_page_load_handler
|
||||
.replace(Box::new(move |url, event| {
|
||||
let payload = PageLoadPayload { url: &url, event };
|
||||
|
||||
if let Some(w) = app_manager_.get_webview(&label) {
|
||||
if let Some(on_page_load) = &app_manager_.webview.on_page_load {
|
||||
on_page_load(&w, &payload);
|
||||
}
|
||||
|
||||
app_manager_
|
||||
.plugins
|
||||
.lock()
|
||||
.unwrap()
|
||||
.on_page_load(&w, &payload);
|
||||
}
|
||||
|
||||
if let Some(handler) = &on_page_load_handler {
|
||||
handler(url, event);
|
||||
}
|
||||
}));
|
||||
|
||||
let permission_request_handler = pending.permission_request_handler.take();
|
||||
if permission_request_handler.is_some() || self.on_permission_request.is_some() {
|
||||
let label_ = pending.label.clone();
|
||||
let app_manager_ = manager.manager_owned();
|
||||
pending
|
||||
.permission_request_handler
|
||||
.replace(Box::new(move |kind| {
|
||||
if let Some(handler) = &permission_request_handler {
|
||||
let response = handler(kind);
|
||||
if response != crate::webview::PermissionResponse::Default {
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(w) = app_manager_.get_webview(&label_) {
|
||||
if let Some(on_permission_request) = &app_manager_.webview.on_permission_request {
|
||||
return on_permission_request(w, kind);
|
||||
}
|
||||
}
|
||||
|
||||
crate::webview::PermissionResponse::Default
|
||||
}));
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||
if pending.on_web_content_process_terminate_handler.is_none() {
|
||||
let app_manager_ = manager.manager_owned();
|
||||
if app_manager_
|
||||
.webview
|
||||
.on_web_content_process_terminate
|
||||
.is_some()
|
||||
{
|
||||
let label_ = pending.label.clone();
|
||||
pending
|
||||
.on_web_content_process_terminate_handler
|
||||
.replace(Box::new(move || {
|
||||
if let Some(w) = app_manager_.get_webview(&label_) {
|
||||
if let Some(on_web_content_process_terminate) =
|
||||
&app_manager_.webview.on_web_content_process_terminate
|
||||
{
|
||||
on_web_content_process_terminate(&w);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "protocol-asset")]
|
||||
if !registered_scheme_protocols.contains(&"asset".into()) {
|
||||
let asset_scope = app_manager
|
||||
.state
|
||||
.get::<crate::Scopes>()
|
||||
.asset_protocol
|
||||
.clone();
|
||||
let protocol = crate::protocol::asset::get(asset_scope, window_origin.clone());
|
||||
pending.register_uri_scheme_protocol("asset", move |webview_id, request, responder| {
|
||||
protocol(webview_id, request, UriSchemeResponder(responder))
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(feature = "isolation")]
|
||||
if let crate::Pattern::Isolation {
|
||||
assets,
|
||||
schema,
|
||||
key: _,
|
||||
crypto_keys,
|
||||
} = &*app_manager.pattern
|
||||
{
|
||||
let protocol = crate::protocol::isolation::get(
|
||||
manager.manager_owned(),
|
||||
schema,
|
||||
assets.clone(),
|
||||
*crypto_keys.aes_gcm().raw(),
|
||||
window_origin,
|
||||
use_https_scheme,
|
||||
);
|
||||
pending.register_uri_scheme_protocol(schema, move |webview_id, request, responder| {
|
||||
protocol(webview_id, request, UriSchemeResponder(responder))
|
||||
});
|
||||
}
|
||||
|
||||
Ok(pending)
|
||||
}
|
||||
|
||||
fn initialization_script(
|
||||
&self,
|
||||
app_manager: &AppManager<R>,
|
||||
ipc_script: &str,
|
||||
pattern_script: &str,
|
||||
use_https_scheme: bool,
|
||||
) -> crate::Result<String> {
|
||||
#[derive(Template)]
|
||||
#[default_template("../../scripts/init.js")]
|
||||
struct InitJavascript<'a> {
|
||||
#[raw]
|
||||
pattern_script: &'a str,
|
||||
#[raw]
|
||||
ipc_script: &'a str,
|
||||
#[raw]
|
||||
core_script: &'a str,
|
||||
#[raw]
|
||||
event_initialization_script: &'a str,
|
||||
#[raw]
|
||||
freeze_prototype: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[default_template("../../scripts/core.js")]
|
||||
struct CoreJavascript<'a> {
|
||||
os_name: &'a str,
|
||||
protocol_scheme: &'a str,
|
||||
invoke_key: &'a str,
|
||||
}
|
||||
|
||||
let freeze_prototype = if app_manager.config.app.security.freeze_prototype {
|
||||
include_str!("../../scripts/freeze_prototype.js")
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
InitJavascript {
|
||||
pattern_script,
|
||||
ipc_script,
|
||||
core_script: &CoreJavascript {
|
||||
os_name: std::env::consts::OS,
|
||||
protocol_scheme: if use_https_scheme { "https" } else { "http" },
|
||||
invoke_key: self.invoke_key(),
|
||||
}
|
||||
.render_default(&Default::default())?
|
||||
.into_string(),
|
||||
event_initialization_script: &crate::event::event_initialization_script(
|
||||
app_manager.listeners().function_name(),
|
||||
app_manager.listeners().listeners_object_name(),
|
||||
),
|
||||
freeze_prototype,
|
||||
}
|
||||
.render_default(&Default::default())
|
||||
.map(|s| s.into_string())
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn prepare_webview<M: Manager<R>>(
|
||||
&self,
|
||||
manager: &M,
|
||||
mut pending: PendingWebview<EventLoopMessage, R>,
|
||||
window_label: &str,
|
||||
) -> crate::Result<PendingWebview<EventLoopMessage, R>> {
|
||||
if self.webviews_lock().contains_key(&pending.label) {
|
||||
return Err(crate::Error::WebviewLabelAlreadyExists(pending.label));
|
||||
}
|
||||
|
||||
let app_manager = manager.manager();
|
||||
|
||||
#[allow(unused_mut)] // mut url only for the data-url parsing
|
||||
let mut url = match &pending.webview_attributes.url {
|
||||
WebviewUrl::App(path) => {
|
||||
let app_url = app_manager.get_app_url(pending.webview_attributes.use_https_scheme);
|
||||
let url = if PROXY_DEV_SERVER && is_local_network_url(&app_url) {
|
||||
Cow::Owned(Url::parse("tauri://localhost").unwrap())
|
||||
} else {
|
||||
app_url
|
||||
};
|
||||
// ignore "index.html" just to simplify the url
|
||||
if path.to_str() != Some("index.html") {
|
||||
url
|
||||
.join(&path.to_string_lossy())
|
||||
.map_err(crate::Error::InvalidUrl)
|
||||
// this will never fail
|
||||
.unwrap()
|
||||
} else {
|
||||
url.into_owned()
|
||||
}
|
||||
}
|
||||
WebviewUrl::External(url) => {
|
||||
let config_url = app_manager.get_app_url(pending.webview_attributes.use_https_scheme);
|
||||
let is_app_url = config_url.make_relative(url).is_some();
|
||||
let mut url = url.clone();
|
||||
if is_app_url && PROXY_DEV_SERVER && is_local_network_url(&url) {
|
||||
Url::parse("tauri://localhost").unwrap()
|
||||
} else {
|
||||
url
|
||||
}
|
||||
}
|
||||
|
||||
WebviewUrl::CustomProtocol(url) => url.clone(),
|
||||
_ => unimplemented!(),
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "webview-data-url"))]
|
||||
if url.scheme() == "data" {
|
||||
return Err(crate::Error::InvalidWebviewUrl(
|
||||
"data URLs are not supported without the `webview-data-url` feature.",
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(feature = "webview-data-url")]
|
||||
if let Some(csp) = app_manager.csp() {
|
||||
if url.scheme() == "data" {
|
||||
if let Ok(data_url) = data_url::DataUrl::process(url.as_str()) {
|
||||
let (body, _) = data_url.decode_to_vec().unwrap();
|
||||
let html = String::from_utf8_lossy(&body).into_owned();
|
||||
// naive way to check if it's an html
|
||||
if html.contains('<') && html.contains('>') {
|
||||
let document = tauri_utils::html2::parse_doc(html);
|
||||
tauri_utils::html2::inject_csp(&document, &csp.to_string());
|
||||
url.set_path(&format!("{},{}", mime::TEXT_HTML, document.html()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pending.url = url.to_string();
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
pending = pending.on_webview_created(move |ctx| {
|
||||
let plugin_manager = ctx
|
||||
.env
|
||||
.call_method(
|
||||
ctx.activity,
|
||||
"getPluginManager",
|
||||
"()Lapp/tauri/plugin/PluginManager;",
|
||||
&[],
|
||||
)?
|
||||
.l()?;
|
||||
|
||||
// tell the manager the webview is ready
|
||||
ctx.env.call_method(
|
||||
plugin_manager,
|
||||
"onWebViewCreated",
|
||||
"(Landroid/webkit/WebView;)V",
|
||||
&[ctx.webview.into()],
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
let label = pending.label.clone();
|
||||
pending = self.prepare_pending_webview(pending, &label, window_label, manager)?;
|
||||
|
||||
pending.ipc_handler = Some(crate::ipc::protocol::message_handler(
|
||||
manager.manager_owned(),
|
||||
));
|
||||
|
||||
// in `windows`, we need to force a data_directory
|
||||
// but we do respect user-specification
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
if pending.webview_attributes.data_directory.is_none() {
|
||||
let local_app_data = manager.path().resolve(
|
||||
&app_manager.config.identifier,
|
||||
crate::path::BaseDirectory::LocalData,
|
||||
);
|
||||
if let Ok(user_data_dir) = local_app_data {
|
||||
pending.webview_attributes.data_directory = Some(user_data_dir);
|
||||
}
|
||||
}
|
||||
|
||||
// make sure the directory is created and available to prevent a panic
|
||||
if let Some(user_data_dir) = &pending.webview_attributes.data_directory {
|
||||
if !user_data_dir.exists() {
|
||||
create_dir_all(user_data_dir)?;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(desktop, not(target_os = "windows")))]
|
||||
if pending.webview_attributes.zoom_hotkeys_enabled {
|
||||
#[derive(Template)]
|
||||
#[default_template("../webview/scripts/zoom-hotkey.js")]
|
||||
struct HotkeyZoom<'a> {
|
||||
os_name: &'a str,
|
||||
}
|
||||
|
||||
pending
|
||||
.webview_attributes
|
||||
.initialization_scripts
|
||||
.push(InitializationScript {
|
||||
script: HotkeyZoom {
|
||||
os_name: std::env::consts::OS,
|
||||
}
|
||||
.render_default(&Default::default())?
|
||||
.into_string(),
|
||||
for_main_frame_only: true,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "isolation")]
|
||||
let pattern = app_manager.pattern.clone();
|
||||
let navigation_handler = pending.navigation_handler.take();
|
||||
let app_manager = manager.manager_owned();
|
||||
let label = pending.label.clone();
|
||||
pending.navigation_handler = Some(Box::new(move |url| {
|
||||
// always allow navigation events for the isolation iframe and do not emit them for consumers
|
||||
#[cfg(feature = "isolation")]
|
||||
if let crate::Pattern::Isolation { schema, .. } = &*pattern {
|
||||
if url.scheme() == schema
|
||||
&& url.domain() == Some(crate::pattern::ISOLATION_IFRAME_SRC_DOMAIN)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if let Some(handler) = &navigation_handler {
|
||||
if !handler(url) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
let webview = app_manager.webview.webviews_lock().get(&label).cloned();
|
||||
if let Some(w) = webview {
|
||||
app_manager
|
||||
.plugins
|
||||
.lock()
|
||||
.expect("poisoned plugin store")
|
||||
.on_navigation(&w, url)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}));
|
||||
|
||||
Ok(pending)
|
||||
}
|
||||
|
||||
pub(crate) fn attach_webview(
|
||||
&self,
|
||||
window: Window<R>,
|
||||
webview: DetachedWebview<EventLoopMessage, R>,
|
||||
use_https_scheme: bool,
|
||||
) -> Webview<R> {
|
||||
let webview = Webview::new(window, webview, use_https_scheme);
|
||||
|
||||
let webview_event_listeners = self.event_listeners.clone();
|
||||
let webview_ = webview.clone();
|
||||
webview.on_webview_event(move |event| {
|
||||
let _ = on_webview_event(&webview_, event);
|
||||
for handler in webview_event_listeners.iter() {
|
||||
handler(&webview_, event);
|
||||
}
|
||||
});
|
||||
|
||||
// insert the webview into our manager
|
||||
{
|
||||
self
|
||||
.webviews_lock()
|
||||
.insert(webview.label().to_string(), webview.clone());
|
||||
}
|
||||
|
||||
// let plugins know that a new webview has been added to the manager
|
||||
let manager = webview.manager_owned();
|
||||
let webview_ = webview.clone();
|
||||
// run on main thread so the plugin store doesn't dead lock with the event loop handler in App
|
||||
let _ = webview.run_on_main_thread(move || {
|
||||
manager
|
||||
.plugins
|
||||
.lock()
|
||||
.expect("poisoned plugin store")
|
||||
.webview_created(webview_);
|
||||
});
|
||||
|
||||
#[cfg(all(target_os = "ios", feature = "wry"))]
|
||||
{
|
||||
webview
|
||||
.with_webview(|w| {
|
||||
unsafe { crate::ios::on_webview_created(w.inner() as _, w.view_controller() as _) };
|
||||
})
|
||||
.expect("failed to run on_webview_created hook");
|
||||
}
|
||||
|
||||
let event = crate::EventName::from_str("tauri://webview-created");
|
||||
let payload = Some(crate::webview::CreatedEvent {
|
||||
label: webview.label().into(),
|
||||
});
|
||||
|
||||
let _ = webview
|
||||
.manager
|
||||
.emit(event, EmitPayload::Serialize(&payload));
|
||||
|
||||
webview
|
||||
}
|
||||
|
||||
pub fn eval_script_all<S: Into<String>>(&self, script: S) -> crate::Result<()> {
|
||||
let script = script.into();
|
||||
let webviews = self.webviews_lock().values().cloned().collect::<Vec<_>>();
|
||||
webviews
|
||||
.iter()
|
||||
.try_for_each(|webview| webview.eval(&script))
|
||||
}
|
||||
|
||||
pub fn labels(&self) -> HashSet<String> {
|
||||
self.webviews_lock().keys().cloned().collect()
|
||||
}
|
||||
|
||||
pub(crate) fn invoke_key(&self) -> &str {
|
||||
&self.invoke_key
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> Webview<R> {
|
||||
/// Emits event to [`EventTarget::Window`] and [`EventTarget::WebviewWindow`]
|
||||
fn emit_to_webview<S: Serialize>(
|
||||
&self,
|
||||
event: crate::EventName<&str>,
|
||||
payload: &S,
|
||||
) -> crate::Result<()> {
|
||||
let window_label = self.label();
|
||||
let payload = EmitPayload::Serialize(payload);
|
||||
self
|
||||
.manager()
|
||||
.emit_filter(event, payload, |target| match target {
|
||||
EventTarget::Webview { label } | EventTarget::WebviewWindow { label } => {
|
||||
label == window_label
|
||||
}
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn on_webview_event<R: Runtime>(webview: &Webview<R>, event: &WebviewEvent) -> crate::Result<()> {
|
||||
match event {
|
||||
WebviewEvent::DragDrop(event) => match event {
|
||||
DragDropEvent::Enter { paths, position } => {
|
||||
let payload = DragDropPayload {
|
||||
paths: Some(paths),
|
||||
position,
|
||||
};
|
||||
webview.emit_to_webview(DRAG_ENTER_EVENT, &payload)?
|
||||
}
|
||||
DragDropEvent::Over { position } => {
|
||||
let payload = DragDropPayload {
|
||||
position,
|
||||
paths: None,
|
||||
};
|
||||
webview.emit_to_webview(DRAG_OVER_EVENT, &payload)?
|
||||
}
|
||||
DragDropEvent::Drop { paths, position } => {
|
||||
let scopes = webview.state::<Scopes>();
|
||||
for path in paths {
|
||||
if path.is_file() {
|
||||
let _ = scopes.allow_file(path);
|
||||
} else {
|
||||
let _ = scopes.allow_directory(path, false);
|
||||
}
|
||||
}
|
||||
let payload = DragDropPayload {
|
||||
paths: Some(paths),
|
||||
position,
|
||||
};
|
||||
webview.emit_to_webview(DRAG_DROP_EVENT, &payload)?
|
||||
}
|
||||
DragDropEvent::Leave => webview.emit_to_webview(DRAG_LEAVE_EVENT, &())?,
|
||||
_ => unimplemented!(),
|
||||
},
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_local_network_url(url: &url::Url) -> bool {
|
||||
match url.host() {
|
||||
Some(url::Host::Domain(s)) => s == "localhost",
|
||||
Some(url::Host::Ipv4(_)) | Some(url::Host::Ipv6(_)) => true,
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn local_network_url() {
|
||||
assert!(is_local_network_url(&"http://localhost".parse().unwrap()));
|
||||
assert!(is_local_network_url(
|
||||
&"http://127.0.0.1:8080".parse().unwrap()
|
||||
));
|
||||
assert!(is_local_network_url(
|
||||
&"https://192.168.3.17".parse().unwrap()
|
||||
));
|
||||
|
||||
assert!(!is_local_network_url(&"https://tauri.app".parse().unwrap()));
|
||||
}
|
||||
}
|
||||
285
crates/tauri/src/manager/window.rs
Normal file
285
crates/tauri/src/manager/window.rs
Normal file
@@ -0,0 +1,285 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
fmt,
|
||||
path::PathBuf,
|
||||
sync::{Arc, Mutex, MutexGuard},
|
||||
};
|
||||
|
||||
use serde::Serialize;
|
||||
use tauri_runtime::{
|
||||
dpi::{PhysicalPosition, PhysicalSize},
|
||||
window::WindowBuilder,
|
||||
window::{DetachedWindow, DragDropEvent, PendingWindow},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
AppHandle, EventLoopMessage, EventTarget, Manager, Runtime, Scopes, Window, WindowEvent,
|
||||
app::GlobalWindowEventListener, event::EventName, image::Image, sealed::ManagerBase,
|
||||
};
|
||||
|
||||
use super::EmitPayload;
|
||||
|
||||
const WINDOW_RESIZED_EVENT: EventName<&str> = EventName::from_str("tauri://resize");
|
||||
const WINDOW_MOVED_EVENT: EventName<&str> = EventName::from_str("tauri://move");
|
||||
const WINDOW_CLOSE_REQUESTED_EVENT: EventName<&str> =
|
||||
EventName::from_str("tauri://close-requested");
|
||||
const WINDOW_DESTROYED_EVENT: EventName<&str> = EventName::from_str("tauri://destroyed");
|
||||
const WINDOW_FOCUS_EVENT: EventName<&str> = EventName::from_str("tauri://focus");
|
||||
const WINDOW_BLUR_EVENT: EventName<&str> = EventName::from_str("tauri://blur");
|
||||
const WINDOW_SCALE_FACTOR_CHANGED_EVENT: EventName<&str> =
|
||||
EventName::from_str("tauri://scale-change");
|
||||
const WINDOW_THEME_CHANGED: EventName<&str> = EventName::from_str("tauri://theme-changed");
|
||||
pub(crate) const DRAG_ENTER_EVENT: EventName<&str> = EventName::from_str("tauri://drag-enter");
|
||||
pub(crate) const DRAG_OVER_EVENT: EventName<&str> = EventName::from_str("tauri://drag-over");
|
||||
pub(crate) const DRAG_DROP_EVENT: EventName<&str> = EventName::from_str("tauri://drag-drop");
|
||||
pub(crate) const DRAG_LEAVE_EVENT: EventName<&str> = EventName::from_str("tauri://drag-leave");
|
||||
#[cfg(mobile)]
|
||||
pub(crate) const WINDOW_SUSPENDED_EVENT: EventName<&str> = EventName::from_str("tauri://suspended");
|
||||
#[cfg(mobile)]
|
||||
pub(crate) const WINDOW_RESUMED_EVENT: EventName<&str> = EventName::from_str("tauri://resumed");
|
||||
|
||||
pub struct WindowManager<R: Runtime> {
|
||||
pub windows: Mutex<HashMap<String, Window<R>>>,
|
||||
pub default_icon: Option<Image<'static>>,
|
||||
/// Window event listeners to all windows.
|
||||
pub event_listeners: Arc<Vec<GlobalWindowEventListener<R>>>,
|
||||
}
|
||||
|
||||
impl<R: Runtime> fmt::Debug for WindowManager<R> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("WindowManager")
|
||||
.field("default_window_icon", &self.default_icon)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> WindowManager<R> {
|
||||
/// Get a locked handle to the windows.
|
||||
pub(crate) fn windows_lock(&self) -> MutexGuard<'_, HashMap<String, Window<R>>> {
|
||||
self.windows.lock().expect("poisoned window manager")
|
||||
}
|
||||
|
||||
pub fn prepare_window(
|
||||
&self,
|
||||
mut pending: PendingWindow<EventLoopMessage, R>,
|
||||
) -> crate::Result<PendingWindow<EventLoopMessage, R>> {
|
||||
if self.windows_lock().contains_key(&pending.label) {
|
||||
return Err(crate::Error::WindowLabelAlreadyExists(pending.label));
|
||||
}
|
||||
|
||||
if !pending.window_builder.has_icon() {
|
||||
if let Some(default_window_icon) = self.default_icon.clone() {
|
||||
pending.window_builder = pending.window_builder.icon(default_window_icon.into())?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(pending)
|
||||
}
|
||||
|
||||
pub(crate) fn attach_window(
|
||||
&self,
|
||||
app_handle: AppHandle<R>,
|
||||
window: DetachedWindow<EventLoopMessage, R>,
|
||||
#[cfg(desktop)] menu: Option<crate::window::WindowMenu<R>>,
|
||||
) -> Window<R> {
|
||||
let window = Window::new(
|
||||
app_handle.manager.clone(),
|
||||
window,
|
||||
app_handle,
|
||||
#[cfg(desktop)]
|
||||
menu,
|
||||
);
|
||||
|
||||
let window_ = window.clone();
|
||||
let window_event_listeners = self.event_listeners.clone();
|
||||
window.on_window_event(move |event| {
|
||||
let _ = on_window_event(&window_, event);
|
||||
for handler in window_event_listeners.iter() {
|
||||
handler(&window_, event);
|
||||
}
|
||||
});
|
||||
|
||||
// insert the window into our manager
|
||||
{
|
||||
self
|
||||
.windows_lock()
|
||||
.insert(window.label().to_string(), window.clone());
|
||||
}
|
||||
|
||||
// let plugins know that a new window has been added to the manager
|
||||
let manager = window.manager.clone();
|
||||
let window_ = window.clone();
|
||||
// run on main thread so the plugin store doesn't dead lock with the event loop handler in App
|
||||
let _ = window.run_on_main_thread(move || {
|
||||
manager
|
||||
.plugins
|
||||
.lock()
|
||||
.expect("poisoned plugin store")
|
||||
.window_created(window_);
|
||||
});
|
||||
|
||||
window
|
||||
}
|
||||
|
||||
pub fn labels(&self) -> HashSet<String> {
|
||||
self.windows_lock().keys().cloned().collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> Window<R> {
|
||||
/// Emits event to [`EventTarget::Window`] and [`EventTarget::WebviewWindow`]
|
||||
fn emit_to_window<S: Serialize>(&self, event: EventName<&str>, payload: &S) -> crate::Result<()> {
|
||||
let window_label = self.label();
|
||||
let payload = EmitPayload::Serialize(payload);
|
||||
self
|
||||
.manager()
|
||||
.emit_filter(event, payload, |target| match target {
|
||||
EventTarget::Window { label } | EventTarget::WebviewWindow { label } => {
|
||||
label == window_label
|
||||
}
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Checks whether has js listener for [`EventTarget::Window`] or [`EventTarget::WebviewWindow`]
|
||||
fn has_js_listener(&self, event: EventName<&str>) -> bool {
|
||||
let window_label = self.label();
|
||||
let listeners = self.manager().listeners();
|
||||
listeners.has_js_listener(event, |target| match target {
|
||||
EventTarget::Window { label } | EventTarget::WebviewWindow { label } => label == window_label,
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
pub(crate) struct DragDropPayload<'a> {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub paths: Option<&'a Vec<PathBuf>>,
|
||||
pub position: &'a PhysicalPosition<f64>,
|
||||
}
|
||||
|
||||
fn on_window_event<R: Runtime>(window: &Window<R>, event: &WindowEvent) -> crate::Result<()> {
|
||||
match event {
|
||||
WindowEvent::Resized(size) => window.emit_to_window(WINDOW_RESIZED_EVENT, size)?,
|
||||
WindowEvent::Moved(position) => window.emit_to_window(WINDOW_MOVED_EVENT, position)?,
|
||||
WindowEvent::CloseRequested { api } => {
|
||||
if window.has_js_listener(WINDOW_CLOSE_REQUESTED_EVENT) {
|
||||
api.prevent_close();
|
||||
}
|
||||
window.emit_to_window(WINDOW_CLOSE_REQUESTED_EVENT, &())?;
|
||||
}
|
||||
WindowEvent::Destroyed => {
|
||||
window.emit_to_window(WINDOW_DESTROYED_EVENT, &())?;
|
||||
}
|
||||
WindowEvent::Focused(focused) => window.emit_to_window(
|
||||
if *focused {
|
||||
WINDOW_FOCUS_EVENT
|
||||
} else {
|
||||
WINDOW_BLUR_EVENT
|
||||
},
|
||||
&(),
|
||||
)?,
|
||||
WindowEvent::ScaleFactorChanged {
|
||||
scale_factor,
|
||||
new_inner_size,
|
||||
..
|
||||
} => window.emit_to_window(
|
||||
WINDOW_SCALE_FACTOR_CHANGED_EVENT,
|
||||
&ScaleFactorChanged {
|
||||
scale_factor: *scale_factor,
|
||||
size: *new_inner_size,
|
||||
},
|
||||
)?,
|
||||
WindowEvent::DragDrop(event) => match event {
|
||||
DragDropEvent::Enter { paths, position } => {
|
||||
let payload = DragDropPayload {
|
||||
paths: Some(paths),
|
||||
position,
|
||||
};
|
||||
|
||||
if window.is_webview_window() {
|
||||
// use underlying manager, otherwise have to recheck EventName
|
||||
window.manager().emit_to(
|
||||
EventTarget::labeled(window.label()),
|
||||
DRAG_ENTER_EVENT,
|
||||
EmitPayload::Serialize(&payload),
|
||||
)?
|
||||
} else {
|
||||
window.emit_to_window(DRAG_ENTER_EVENT, &payload)?
|
||||
}
|
||||
}
|
||||
DragDropEvent::Over { position } => {
|
||||
let payload = DragDropPayload {
|
||||
position,
|
||||
paths: None,
|
||||
};
|
||||
if window.is_webview_window() {
|
||||
// use underlying manager, otherwise have to recheck EventName
|
||||
window.manager().emit_to(
|
||||
EventTarget::labeled(window.label()),
|
||||
DRAG_OVER_EVENT,
|
||||
EmitPayload::Serialize(&payload),
|
||||
)?
|
||||
} else {
|
||||
window.emit_to_window(DRAG_OVER_EVENT, &payload)?
|
||||
}
|
||||
}
|
||||
DragDropEvent::Drop { paths, position } => {
|
||||
let scopes = window.state::<Scopes>();
|
||||
for path in paths {
|
||||
if path.is_file() {
|
||||
let _ = scopes.allow_file(path);
|
||||
} else {
|
||||
let _ = scopes.allow_directory(path, true);
|
||||
}
|
||||
}
|
||||
let payload = DragDropPayload {
|
||||
paths: Some(paths),
|
||||
position,
|
||||
};
|
||||
|
||||
if window.is_webview_window() {
|
||||
// use underlying manager, otherwise have to recheck EventName
|
||||
window.manager().emit_to(
|
||||
EventTarget::labeled(window.label()),
|
||||
DRAG_DROP_EVENT,
|
||||
EmitPayload::Serialize(&payload),
|
||||
)?
|
||||
} else {
|
||||
window.emit_to_window(DRAG_DROP_EVENT, &payload)?
|
||||
}
|
||||
}
|
||||
DragDropEvent::Leave => {
|
||||
if window.is_webview_window() {
|
||||
// use underlying manager, otherwise have to recheck EventName
|
||||
window.manager().emit_to(
|
||||
EventTarget::labeled(window.label()),
|
||||
DRAG_LEAVE_EVENT,
|
||||
EmitPayload::Serialize(&()),
|
||||
)?
|
||||
} else {
|
||||
window.emit_to_window(DRAG_LEAVE_EVENT, &())?
|
||||
}
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
},
|
||||
WindowEvent::ThemeChanged(theme) => window.emit_to_window(WINDOW_THEME_CHANGED, &theme)?,
|
||||
#[cfg(mobile)]
|
||||
WindowEvent::Suspended => window.emit_to_window(WINDOW_SUSPENDED_EVENT, &())?,
|
||||
#[cfg(mobile)]
|
||||
WindowEvent::Resumed => window.emit_to_window(WINDOW_RESUMED_EVENT, &())?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ScaleFactorChanged {
|
||||
scale_factor: f64,
|
||||
size: PhysicalSize<u32>,
|
||||
}
|
||||
90
crates/tauri/src/menu/builders/check.rs
Normal file
90
crates/tauri/src/menu/builders/check.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use crate::{Manager, Runtime, menu::CheckMenuItem, menu::MenuId};
|
||||
|
||||
/// A builder type for [`CheckMenuItem`]
|
||||
pub struct CheckMenuItemBuilder {
|
||||
id: Option<MenuId>,
|
||||
text: String,
|
||||
enabled: bool,
|
||||
checked: bool,
|
||||
accelerator: Option<String>,
|
||||
}
|
||||
|
||||
impl CheckMenuItemBuilder {
|
||||
/// Create a new menu item builder.
|
||||
///
|
||||
/// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic
|
||||
/// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
|
||||
pub fn new<S: AsRef<str>>(text: S) -> Self {
|
||||
Self {
|
||||
id: None,
|
||||
text: text.as_ref().to_string(),
|
||||
enabled: true,
|
||||
checked: true,
|
||||
accelerator: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new menu item builder with the specified id.
|
||||
///
|
||||
/// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic
|
||||
/// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
|
||||
pub fn with_id<I: Into<MenuId>, S: AsRef<str>>(id: I, text: S) -> Self {
|
||||
Self {
|
||||
id: Some(id.into()),
|
||||
text: text.as_ref().to_string(),
|
||||
enabled: true,
|
||||
checked: true,
|
||||
accelerator: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the id for this menu item.
|
||||
pub fn id<I: Into<MenuId>>(mut self, id: I) -> Self {
|
||||
self.id.replace(id.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the enabled state for this menu item.
|
||||
pub fn enabled(mut self, enabled: bool) -> Self {
|
||||
self.enabled = enabled;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the checked state for this menu item.
|
||||
pub fn checked(mut self, checked: bool) -> Self {
|
||||
self.checked = checked;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the accelerator for this menu item.
|
||||
pub fn accelerator<S: AsRef<str>>(mut self, accelerator: S) -> Self {
|
||||
self.accelerator.replace(accelerator.as_ref().to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the menu item
|
||||
pub fn build<R: Runtime, M: Manager<R>>(self, manager: &M) -> crate::Result<CheckMenuItem<R>> {
|
||||
if let Some(id) = self.id {
|
||||
CheckMenuItem::with_id(
|
||||
manager,
|
||||
id,
|
||||
self.text,
|
||||
self.enabled,
|
||||
self.checked,
|
||||
self.accelerator,
|
||||
)
|
||||
} else {
|
||||
CheckMenuItem::new(
|
||||
manager,
|
||||
self.text,
|
||||
self.enabled,
|
||||
self.checked,
|
||||
self.accelerator,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
130
crates/tauri/src/menu/builders/icon.rs
Normal file
130
crates/tauri/src/menu/builders/icon.rs
Normal file
@@ -0,0 +1,130 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use crate::{
|
||||
Manager, Runtime,
|
||||
image::Image,
|
||||
menu::{IconMenuItem, MenuId, NativeIcon},
|
||||
};
|
||||
|
||||
/// A builder type for [`IconMenuItem`]
|
||||
pub struct IconMenuItemBuilder<'a> {
|
||||
id: Option<MenuId>,
|
||||
text: String,
|
||||
enabled: bool,
|
||||
icon: Option<Image<'a>>,
|
||||
native_icon: Option<NativeIcon>,
|
||||
accelerator: Option<String>,
|
||||
}
|
||||
|
||||
impl<'a> IconMenuItemBuilder<'a> {
|
||||
/// Create a new menu item builder.
|
||||
///
|
||||
/// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic
|
||||
/// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
|
||||
pub fn new<S: AsRef<str>>(text: S) -> Self {
|
||||
Self {
|
||||
id: None,
|
||||
text: text.as_ref().to_string(),
|
||||
enabled: true,
|
||||
icon: None,
|
||||
native_icon: None,
|
||||
accelerator: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new menu item builder with the specified id.
|
||||
///
|
||||
/// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic
|
||||
/// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
|
||||
pub fn with_id<I: Into<MenuId>, S: AsRef<str>>(id: I, text: S) -> Self {
|
||||
Self {
|
||||
id: Some(id.into()),
|
||||
text: text.as_ref().to_string(),
|
||||
enabled: true,
|
||||
icon: None,
|
||||
native_icon: None,
|
||||
accelerator: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the id for this menu item.
|
||||
pub fn id<I: Into<MenuId>>(mut self, id: I) -> Self {
|
||||
self.id.replace(id.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the enabled state for this menu item.
|
||||
pub fn enabled(mut self, enabled: bool) -> Self {
|
||||
self.enabled = enabled;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the accelerator for this menu item.
|
||||
pub fn accelerator<S: AsRef<str>>(mut self, accelerator: S) -> Self {
|
||||
self.accelerator.replace(accelerator.as_ref().to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the icon for this menu item.
|
||||
///
|
||||
/// **Note:** This method conflicts with [`Self::native_icon`]
|
||||
/// so calling one of them, will reset the other.
|
||||
pub fn icon(mut self, icon: Image<'a>) -> Self {
|
||||
self.icon.replace(icon);
|
||||
self.native_icon = None;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the icon for this menu item.
|
||||
///
|
||||
/// **Note:** This method conflicts with [`Self::icon`]
|
||||
/// so calling one of them, will reset the other.
|
||||
pub fn native_icon(mut self, icon: NativeIcon) -> Self {
|
||||
self.native_icon.replace(icon);
|
||||
self.icon = None;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the menu item
|
||||
pub fn build<R: Runtime, M: Manager<R>>(self, manager: &M) -> crate::Result<IconMenuItem<R>> {
|
||||
if self.icon.is_some() {
|
||||
if let Some(id) = self.id {
|
||||
IconMenuItem::with_id(
|
||||
manager,
|
||||
id,
|
||||
self.text,
|
||||
self.enabled,
|
||||
self.icon,
|
||||
self.accelerator,
|
||||
)
|
||||
} else {
|
||||
IconMenuItem::new(
|
||||
manager,
|
||||
self.text,
|
||||
self.enabled,
|
||||
self.icon,
|
||||
self.accelerator,
|
||||
)
|
||||
}
|
||||
} else if let Some(id) = self.id {
|
||||
IconMenuItem::with_id_and_native_icon(
|
||||
manager,
|
||||
id,
|
||||
self.text,
|
||||
self.enabled,
|
||||
self.native_icon,
|
||||
self.accelerator,
|
||||
)
|
||||
} else {
|
||||
IconMenuItem::with_native_icon(
|
||||
manager,
|
||||
self.text,
|
||||
self.enabled,
|
||||
self.native_icon,
|
||||
self.accelerator,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
675
crates/tauri/src/menu/builders/menu.rs
Normal file
675
crates/tauri/src/menu/builders/menu.rs
Normal file
@@ -0,0 +1,675 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use crate::{Manager, Runtime, image::Image, menu::*};
|
||||
|
||||
/// A builder type for [`Menu`]
|
||||
///
|
||||
/// ## Platform-specific:
|
||||
///
|
||||
/// - **macOS**: if using [`MenuBuilder`] for the global menubar, it can only contain [`Submenu`]s
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// use tauri::menu::*;
|
||||
/// tauri::Builder::default()
|
||||
/// .setup(move |app| {
|
||||
/// let handle = app.handle();
|
||||
/// # let icon1 = tauri::image::Image::new(&[], 0, 0);
|
||||
/// let menu = MenuBuilder::new(handle)
|
||||
/// .item(&MenuItem::new(handle, "MenuItem 1", true, None::<&str>)?)
|
||||
/// .items(&[
|
||||
/// &CheckMenuItem::new(handle, "CheckMenuItem 1", true, true, None::<&str>)?,
|
||||
/// &IconMenuItem::new(handle, "IconMenuItem 1", true, Some(icon1), None::<&str>)?,
|
||||
/// ])
|
||||
/// .separator()
|
||||
/// .cut()
|
||||
/// .copy()
|
||||
/// .paste()
|
||||
/// .separator()
|
||||
/// .text("item2", "MenuItem 2")
|
||||
/// .check("checkitem2", "CheckMenuItem 2")
|
||||
/// .icon("iconitem2", "IconMenuItem 2", app.default_window_icon().cloned().unwrap())
|
||||
/// .build()?;
|
||||
/// app.set_menu(menu);
|
||||
/// Ok(())
|
||||
/// });
|
||||
/// ```
|
||||
pub struct MenuBuilder<'m, R: Runtime, M: Manager<R>> {
|
||||
pub(crate) id: Option<MenuId>,
|
||||
pub(crate) manager: &'m M,
|
||||
pub(crate) items: Vec<crate::Result<MenuItemKind<R>>>,
|
||||
}
|
||||
|
||||
impl<'m, R: Runtime, M: Manager<R>> MenuBuilder<'m, R, M> {
|
||||
/// Create a new menu builder.
|
||||
pub fn new(manager: &'m M) -> Self {
|
||||
Self {
|
||||
id: None,
|
||||
items: Vec::new(),
|
||||
manager,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new menu builder with the specified id.
|
||||
pub fn with_id<I: Into<MenuId>>(manager: &'m M, id: I) -> Self {
|
||||
Self {
|
||||
id: Some(id.into()),
|
||||
items: Vec::new(),
|
||||
manager,
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds this menu
|
||||
pub fn build(self) -> crate::Result<Menu<R>> {
|
||||
let menu = if let Some(id) = self.id {
|
||||
Menu::with_id(self.manager, id)?
|
||||
} else {
|
||||
Menu::new(self.manager)?
|
||||
};
|
||||
|
||||
for item in self.items {
|
||||
let item = item?;
|
||||
menu.append(&item)?;
|
||||
}
|
||||
|
||||
Ok(menu)
|
||||
}
|
||||
}
|
||||
|
||||
/// A builder type for [`Submenu`]
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// use tauri::menu::*;
|
||||
/// tauri::Builder::default()
|
||||
/// .setup(move |app| {
|
||||
/// let handle = app.handle();
|
||||
/// # let icon1 = tauri::image::Image::new(&[], 0, 0);
|
||||
/// # let icon2 = icon1.clone();
|
||||
/// let menu = Menu::new(handle)?;
|
||||
/// let submenu = SubmenuBuilder::new(handle, "File")
|
||||
/// .item(&MenuItem::new(handle, "MenuItem 1", true, None::<&str>)?)
|
||||
/// .items(&[
|
||||
/// &CheckMenuItem::new(handle, "CheckMenuItem 1", true, true, None::<&str>)?,
|
||||
/// &IconMenuItem::new(handle, "IconMenuItem 1", true, Some(icon1), None::<&str>)?,
|
||||
/// ])
|
||||
/// .separator()
|
||||
/// .cut()
|
||||
/// .copy()
|
||||
/// .paste()
|
||||
/// .separator()
|
||||
/// .text("item2", "MenuItem 2")
|
||||
/// .check("checkitem2", "CheckMenuItem 2")
|
||||
/// .icon("iconitem2", "IconMenuItem 2", app.default_window_icon().cloned().unwrap())
|
||||
/// .build()?;
|
||||
/// menu.append(&submenu)?;
|
||||
/// app.set_menu(menu);
|
||||
/// Ok(())
|
||||
/// });
|
||||
/// ```
|
||||
pub struct SubmenuBuilder<'m, R: Runtime, M: Manager<R>> {
|
||||
pub(crate) id: Option<MenuId>,
|
||||
pub(crate) manager: &'m M,
|
||||
pub(crate) text: String,
|
||||
pub(crate) enabled: bool,
|
||||
pub(crate) items: Vec<crate::Result<MenuItemKind<R>>>,
|
||||
pub(crate) icon: Option<crate::image::Image<'m>>,
|
||||
pub(crate) native_icon: Option<NativeIcon>,
|
||||
}
|
||||
|
||||
impl<'m, R: Runtime, M: Manager<R>> SubmenuBuilder<'m, R, M> {
|
||||
/// Create a new submenu builder.
|
||||
///
|
||||
/// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic
|
||||
/// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
|
||||
pub fn new<S: AsRef<str>>(manager: &'m M, text: S) -> Self {
|
||||
Self {
|
||||
id: None,
|
||||
items: Vec::new(),
|
||||
text: text.as_ref().to_string(),
|
||||
enabled: true,
|
||||
manager,
|
||||
icon: None,
|
||||
native_icon: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new submenu builder with the specified id.
|
||||
///
|
||||
/// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic
|
||||
/// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
|
||||
pub fn with_id<I: Into<MenuId>, S: AsRef<str>>(manager: &'m M, id: I, text: S) -> Self {
|
||||
Self {
|
||||
id: Some(id.into()),
|
||||
text: text.as_ref().to_string(),
|
||||
enabled: true,
|
||||
items: Vec::new(),
|
||||
manager,
|
||||
icon: None,
|
||||
native_icon: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set an icon for the submenu.
|
||||
/// Calling this method resets the native_icon.
|
||||
pub fn submenu_icon(mut self, icon: crate::image::Image<'m>) -> Self {
|
||||
self.icon = Some(icon);
|
||||
self.native_icon = None;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set a native icon for the submenu.
|
||||
/// Calling this method resets the icon.
|
||||
pub fn submenu_native_icon(mut self, icon: NativeIcon) -> Self {
|
||||
self.native_icon = Some(icon);
|
||||
self.icon = None;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the enabled state for the submenu.
|
||||
pub fn enabled(mut self, enabled: bool) -> Self {
|
||||
self.enabled = enabled;
|
||||
self
|
||||
}
|
||||
|
||||
/// Builds this submenu
|
||||
pub fn build(self) -> crate::Result<Submenu<R>> {
|
||||
let submenu = if let Some(id) = self.id {
|
||||
if let Some(icon) = self.icon {
|
||||
Submenu::with_id_and_icon(self.manager, id, self.text, self.enabled, Some(icon))?
|
||||
} else if let Some(native_icon) = self.native_icon {
|
||||
Submenu::with_id_and_native_icon(
|
||||
self.manager,
|
||||
id,
|
||||
self.text,
|
||||
self.enabled,
|
||||
Some(native_icon),
|
||||
)?
|
||||
} else {
|
||||
Submenu::with_id(self.manager, id, self.text, self.enabled)?
|
||||
}
|
||||
} else if let Some(icon) = self.icon {
|
||||
Submenu::new_with_icon(self.manager, self.text, self.enabled, Some(icon))?
|
||||
} else if let Some(native_icon) = self.native_icon {
|
||||
Submenu::new_with_native_icon(self.manager, self.text, self.enabled, Some(native_icon))?
|
||||
} else {
|
||||
Submenu::new(self.manager, self.text, self.enabled)?
|
||||
};
|
||||
|
||||
for item in self.items {
|
||||
let item = item?;
|
||||
submenu.append(&item)?;
|
||||
}
|
||||
|
||||
Ok(submenu)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! shared_menu_builder {
|
||||
($menu: ty) => {
|
||||
impl<'m, R: Runtime, M: Manager<R>> $menu {
|
||||
/// Set the id for this menu.
|
||||
pub fn id<I: Into<MenuId>>(mut self, id: I) -> Self {
|
||||
self.id.replace(id.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Add this item to the menu.
|
||||
pub fn item(mut self, item: &dyn IsMenuItem<R>) -> Self {
|
||||
self.items.push(Ok(item.kind()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add these items to the menu.
|
||||
pub fn items(mut self, items: &[&dyn IsMenuItem<R>]) -> Self {
|
||||
for item in items {
|
||||
self = self.item(*item);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a [MenuItem] to the menu.
|
||||
pub fn text<I: Into<MenuId>, S: AsRef<str>>(mut self, id: I, text: S) -> Self {
|
||||
self
|
||||
.items
|
||||
.push(MenuItem::with_id(self.manager, id, text, true, None::<&str>).map(|i| i.kind()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a [CheckMenuItem] to the menu.
|
||||
pub fn check<I: Into<MenuId>, S: AsRef<str>>(mut self, id: I, text: S) -> Self {
|
||||
self.items.push(
|
||||
CheckMenuItem::with_id(self.manager, id, text, true, true, None::<&str>)
|
||||
.map(|i| i.kind()),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add an [IconMenuItem] to the menu.
|
||||
pub fn icon<I: Into<MenuId>, S: AsRef<str>>(
|
||||
mut self,
|
||||
id: I,
|
||||
text: S,
|
||||
icon: Image<'_>,
|
||||
) -> Self {
|
||||
self.items.push(
|
||||
IconMenuItem::with_id(self.manager, id, text, true, Some(icon), None::<&str>)
|
||||
.map(|i| i.kind()),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add an [IconMenuItem] with a native icon to the menu.
|
||||
///
|
||||
/// ## Platform-specific:
|
||||
///
|
||||
/// - **Windows / Linux**: Unsupported.
|
||||
pub fn native_icon<I: Into<MenuId>, S: AsRef<str>>(
|
||||
mut self,
|
||||
id: I,
|
||||
text: S,
|
||||
icon: NativeIcon,
|
||||
) -> Self {
|
||||
self.items.push(
|
||||
IconMenuItem::with_id_and_native_icon(
|
||||
self.manager,
|
||||
id,
|
||||
text,
|
||||
true,
|
||||
Some(icon),
|
||||
None::<&str>,
|
||||
)
|
||||
.map(|i| i.kind()),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add Separator menu item to the menu.
|
||||
pub fn separator(mut self) -> Self {
|
||||
self
|
||||
.items
|
||||
.push(PredefinedMenuItem::separator(self.manager).map(|i| i.kind()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add Copy menu item to the menu.
|
||||
pub fn copy(mut self) -> Self {
|
||||
self
|
||||
.items
|
||||
.push(PredefinedMenuItem::copy(self.manager, None).map(|i| i.kind()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add Copy menu item with specified text to the menu.
|
||||
pub fn copy_with_text<S: AsRef<str>>(mut self, text: S) -> Self {
|
||||
self
|
||||
.items
|
||||
.push(PredefinedMenuItem::copy(self.manager, Some(text.as_ref())).map(|i| i.kind()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add Cut menu item to the menu.
|
||||
pub fn cut(mut self) -> Self {
|
||||
self
|
||||
.items
|
||||
.push(PredefinedMenuItem::cut(self.manager, None).map(|i| i.kind()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add Cut menu item with specified text to the menu.
|
||||
pub fn cut_with_text<S: AsRef<str>>(mut self, text: S) -> Self {
|
||||
self
|
||||
.items
|
||||
.push(PredefinedMenuItem::cut(self.manager, Some(text.as_ref())).map(|i| i.kind()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add Paste menu item to the menu.
|
||||
pub fn paste(mut self) -> Self {
|
||||
self
|
||||
.items
|
||||
.push(PredefinedMenuItem::paste(self.manager, None).map(|i| i.kind()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add Paste menu item with specified text to the menu.
|
||||
pub fn paste_with_text<S: AsRef<str>>(mut self, text: S) -> Self {
|
||||
self
|
||||
.items
|
||||
.push(PredefinedMenuItem::paste(self.manager, Some(text.as_ref())).map(|i| i.kind()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add SelectAll menu item to the menu.
|
||||
pub fn select_all(mut self) -> Self {
|
||||
self
|
||||
.items
|
||||
.push(PredefinedMenuItem::select_all(self.manager, None).map(|i| i.kind()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add SelectAll menu item with specified text to the menu.
|
||||
pub fn select_all_with_text<S: AsRef<str>>(mut self, text: S) -> Self {
|
||||
self.items.push(
|
||||
PredefinedMenuItem::select_all(self.manager, Some(text.as_ref())).map(|i| i.kind()),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add Undo menu item to the menu.
|
||||
///
|
||||
/// ## Platform-specific:
|
||||
///
|
||||
/// - **Windows / Linux:** Unsupported.
|
||||
pub fn undo(mut self) -> Self {
|
||||
self
|
||||
.items
|
||||
.push(PredefinedMenuItem::undo(self.manager, None).map(|i| i.kind()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add Undo menu item with specified text to the menu.
|
||||
///
|
||||
/// ## Platform-specific:
|
||||
///
|
||||
/// - **Windows / Linux:** Unsupported.
|
||||
pub fn undo_with_text<S: AsRef<str>>(mut self, text: S) -> Self {
|
||||
self
|
||||
.items
|
||||
.push(PredefinedMenuItem::undo(self.manager, Some(text.as_ref())).map(|i| i.kind()));
|
||||
self
|
||||
}
|
||||
/// Add Redo menu item to the menu.
|
||||
///
|
||||
/// ## Platform-specific:
|
||||
///
|
||||
/// - **Windows / Linux:** Unsupported.
|
||||
pub fn redo(mut self) -> Self {
|
||||
self
|
||||
.items
|
||||
.push(PredefinedMenuItem::redo(self.manager, None).map(|i| i.kind()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add Redo menu item with specified text to the menu.
|
||||
///
|
||||
/// ## Platform-specific:
|
||||
///
|
||||
/// - **Windows / Linux:** Unsupported.
|
||||
pub fn redo_with_text<S: AsRef<str>>(mut self, text: S) -> Self {
|
||||
self
|
||||
.items
|
||||
.push(PredefinedMenuItem::redo(self.manager, Some(text.as_ref())).map(|i| i.kind()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add Minimize window menu item to the menu.
|
||||
///
|
||||
/// ## Platform-specific:
|
||||
///
|
||||
/// - **Linux:** Unsupported.
|
||||
pub fn minimize(mut self) -> Self {
|
||||
self
|
||||
.items
|
||||
.push(PredefinedMenuItem::minimize(self.manager, None).map(|i| i.kind()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add Minimize window menu item with specified text to the menu.
|
||||
///
|
||||
/// ## Platform-specific:
|
||||
///
|
||||
/// - **Linux:** Unsupported.
|
||||
pub fn minimize_with_text<S: AsRef<str>>(mut self, text: S) -> Self {
|
||||
self
|
||||
.items
|
||||
.push(PredefinedMenuItem::minimize(self.manager, Some(text.as_ref())).map(|i| i.kind()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add Maximize window menu item to the menu.
|
||||
///
|
||||
/// ## Platform-specific:
|
||||
///
|
||||
/// - **Linux:** Unsupported.
|
||||
pub fn maximize(mut self) -> Self {
|
||||
self
|
||||
.items
|
||||
.push(PredefinedMenuItem::maximize(self.manager, None).map(|i| i.kind()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add Maximize window menu item with specified text to the menu.
|
||||
///
|
||||
/// ## Platform-specific:
|
||||
///
|
||||
/// - **Linux:** Unsupported.
|
||||
pub fn maximize_with_text<S: AsRef<str>>(mut self, text: S) -> Self {
|
||||
self
|
||||
.items
|
||||
.push(PredefinedMenuItem::maximize(self.manager, Some(text.as_ref())).map(|i| i.kind()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add Fullscreen menu item to the menu.
|
||||
///
|
||||
/// ## Platform-specific:
|
||||
///
|
||||
/// - **Windows / Linux:** Unsupported.
|
||||
pub fn fullscreen(mut self) -> Self {
|
||||
self
|
||||
.items
|
||||
.push(PredefinedMenuItem::fullscreen(self.manager, None).map(|i| i.kind()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add Fullscreen menu item with specified text to the menu.
|
||||
///
|
||||
/// ## Platform-specific:
|
||||
///
|
||||
/// - **Windows / Linux:** Unsupported.
|
||||
pub fn fullscreen_with_text<S: AsRef<str>>(mut self, text: S) -> Self {
|
||||
self.items.push(
|
||||
PredefinedMenuItem::fullscreen(self.manager, Some(text.as_ref())).map(|i| i.kind()),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add Hide window menu item to the menu.
|
||||
///
|
||||
/// ## Platform-specific:
|
||||
///
|
||||
/// - **Linux:** Unsupported.
|
||||
pub fn hide(mut self) -> Self {
|
||||
self
|
||||
.items
|
||||
.push(PredefinedMenuItem::hide(self.manager, None).map(|i| i.kind()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add Hide window menu item with specified text to the menu.
|
||||
///
|
||||
/// ## Platform-specific:
|
||||
///
|
||||
/// - **Linux:** Unsupported.
|
||||
pub fn hide_with_text<S: AsRef<str>>(mut self, text: S) -> Self {
|
||||
self
|
||||
.items
|
||||
.push(PredefinedMenuItem::hide(self.manager, Some(text.as_ref())).map(|i| i.kind()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add Hide other windows menu item to the menu.
|
||||
///
|
||||
/// ## Platform-specific:
|
||||
///
|
||||
/// - **Linux:** Unsupported.
|
||||
pub fn hide_others(mut self) -> Self {
|
||||
self
|
||||
.items
|
||||
.push(PredefinedMenuItem::hide_others(self.manager, None).map(|i| i.kind()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add Hide other windows menu item with specified text to the menu.
|
||||
///
|
||||
/// ## Platform-specific:
|
||||
///
|
||||
/// - **Linux:** Unsupported.
|
||||
pub fn hide_others_with_text<S: AsRef<str>>(mut self, text: S) -> Self {
|
||||
self.items.push(
|
||||
PredefinedMenuItem::hide_others(self.manager, Some(text.as_ref())).map(|i| i.kind()),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add Show all app windows menu item to the menu.
|
||||
///
|
||||
/// ## Platform-specific:
|
||||
///
|
||||
/// - **Windows / Linux:** Unsupported.
|
||||
pub fn show_all(mut self) -> Self {
|
||||
self
|
||||
.items
|
||||
.push(PredefinedMenuItem::show_all(self.manager, None).map(|i| i.kind()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add Show all app windows menu item with specified text to the menu.
|
||||
///
|
||||
/// ## Platform-specific:
|
||||
///
|
||||
/// - **Windows / Linux:** Unsupported.
|
||||
pub fn show_all_with_text<S: AsRef<str>>(mut self, text: S) -> Self {
|
||||
self
|
||||
.items
|
||||
.push(PredefinedMenuItem::show_all(self.manager, Some(text.as_ref())).map(|i| i.kind()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add Close window menu item to the menu.
|
||||
///
|
||||
/// ## Platform-specific:
|
||||
///
|
||||
/// - **Linux:** Unsupported.
|
||||
pub fn close_window(mut self) -> Self {
|
||||
self
|
||||
.items
|
||||
.push(PredefinedMenuItem::close_window(self.manager, None).map(|i| i.kind()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add Close window menu item with specified text to the menu.
|
||||
///
|
||||
/// ## Platform-specific:
|
||||
///
|
||||
/// - **Linux:** Unsupported.
|
||||
pub fn close_window_with_text<S: AsRef<str>>(mut self, text: S) -> Self {
|
||||
self.items.push(
|
||||
PredefinedMenuItem::close_window(self.manager, Some(text.as_ref())).map(|i| i.kind()),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add Quit app menu item to the menu.
|
||||
///
|
||||
/// ## Platform-specific:
|
||||
///
|
||||
/// - **Linux:** Unsupported.
|
||||
pub fn quit(mut self) -> Self {
|
||||
self
|
||||
.items
|
||||
.push(PredefinedMenuItem::quit(self.manager, None).map(|i| i.kind()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add Quit app menu item with specified text to the menu.
|
||||
///
|
||||
/// ## Platform-specific:
|
||||
///
|
||||
/// - **Linux:** Unsupported.
|
||||
pub fn quit_with_text<S: AsRef<str>>(mut self, text: S) -> Self {
|
||||
self
|
||||
.items
|
||||
.push(PredefinedMenuItem::quit(self.manager, Some(text.as_ref())).map(|i| i.kind()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add About app menu item to the menu.
|
||||
pub fn about(mut self, metadata: Option<AboutMetadata<'_>>) -> Self {
|
||||
self
|
||||
.items
|
||||
.push(PredefinedMenuItem::about(self.manager, None, metadata).map(|i| i.kind()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add About app menu item with specified text to the menu.
|
||||
pub fn about_with_text<S: AsRef<str>>(
|
||||
mut self,
|
||||
text: S,
|
||||
metadata: Option<AboutMetadata<'_>>,
|
||||
) -> Self {
|
||||
self.items.push(
|
||||
PredefinedMenuItem::about(self.manager, Some(text.as_ref()), metadata).map(|i| i.kind()),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add Services menu item to the menu.
|
||||
///
|
||||
/// ## Platform-specific:
|
||||
///
|
||||
/// - **Windows / Linux:** Unsupported.
|
||||
pub fn services(mut self) -> Self {
|
||||
self
|
||||
.items
|
||||
.push(PredefinedMenuItem::services(self.manager, None).map(|i| i.kind()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add Services menu item with specified text to the menu.
|
||||
///
|
||||
/// ## Platform-specific:
|
||||
///
|
||||
/// - **Windows / Linux:** Unsupported.
|
||||
pub fn services_with_text<S: AsRef<str>>(mut self, text: S) -> Self {
|
||||
self
|
||||
.items
|
||||
.push(PredefinedMenuItem::services(self.manager, Some(text.as_ref())).map(|i| i.kind()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add Bring All to Front menu item to the menu.
|
||||
///
|
||||
/// ## Platform-specific:
|
||||
///
|
||||
/// - **Windows / Linux:** Unsupported.
|
||||
pub fn bring_all_to_front(mut self) -> Self {
|
||||
self
|
||||
.items
|
||||
.push(PredefinedMenuItem::bring_all_to_front(self.manager, None).map(|i| i.kind()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add Bring All to Front menu item with specified text to the menu.
|
||||
///
|
||||
/// ## Platform-specific:
|
||||
///
|
||||
/// - **Windows / Linux:** Unsupported.
|
||||
pub fn bring_all_to_front_with_text<S: AsRef<str>>(mut self, text: S) -> Self {
|
||||
self.items.push(
|
||||
PredefinedMenuItem::bring_all_to_front(self.manager, Some(text.as_ref()))
|
||||
.map(|i| i.kind()),
|
||||
);
|
||||
self
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
shared_menu_builder!(MenuBuilder<'m, R, M>);
|
||||
shared_menu_builder!(SubmenuBuilder<'m, R, M>);
|
||||
17
crates/tauri/src/menu/builders/mod.rs
Normal file
17
crates/tauri/src/menu/builders/mod.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#![cfg(desktop)]
|
||||
|
||||
//! A module containing menu builder types
|
||||
|
||||
mod menu;
|
||||
pub use menu::MenuBuilder;
|
||||
pub use menu::SubmenuBuilder;
|
||||
mod normal;
|
||||
pub use normal::MenuItemBuilder;
|
||||
mod check;
|
||||
pub use check::CheckMenuItemBuilder;
|
||||
mod icon;
|
||||
pub use icon::IconMenuItemBuilder;
|
||||
68
crates/tauri/src/menu/builders/normal.rs
Normal file
68
crates/tauri/src/menu/builders/normal.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use crate::{Manager, Runtime, menu::MenuId, menu::MenuItem};
|
||||
|
||||
/// A builder type for [`MenuItem`]
|
||||
pub struct MenuItemBuilder {
|
||||
id: Option<MenuId>,
|
||||
text: String,
|
||||
enabled: bool,
|
||||
accelerator: Option<String>,
|
||||
}
|
||||
|
||||
impl MenuItemBuilder {
|
||||
/// Create a new menu item builder.
|
||||
///
|
||||
/// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic
|
||||
/// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
|
||||
pub fn new<S: AsRef<str>>(text: S) -> Self {
|
||||
Self {
|
||||
id: None,
|
||||
text: text.as_ref().to_string(),
|
||||
enabled: true,
|
||||
accelerator: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new menu item builder with the specified id.
|
||||
///
|
||||
/// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic
|
||||
/// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
|
||||
pub fn with_id<I: Into<MenuId>, S: AsRef<str>>(id: I, text: S) -> Self {
|
||||
Self {
|
||||
id: Some(id.into()),
|
||||
text: text.as_ref().to_string(),
|
||||
enabled: true,
|
||||
accelerator: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the id for this menu item.
|
||||
pub fn id<I: Into<MenuId>>(mut self, id: I) -> Self {
|
||||
self.id.replace(id.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the enabled state for this menu item.
|
||||
pub fn enabled(mut self, enabled: bool) -> Self {
|
||||
self.enabled = enabled;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the accelerator for this menu item.
|
||||
pub fn accelerator<S: AsRef<str>>(mut self, accelerator: S) -> Self {
|
||||
self.accelerator.replace(accelerator.as_ref().to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the menu item
|
||||
pub fn build<R: Runtime, M: Manager<R>>(self, manager: &M) -> crate::Result<MenuItem<R>> {
|
||||
if let Some(id) = self.id {
|
||||
MenuItem::with_id(manager, id, self.text, self.enabled, self.accelerator)
|
||||
} else {
|
||||
MenuItem::new(manager, self.text, self.enabled, self.accelerator)
|
||||
}
|
||||
}
|
||||
}
|
||||
129
crates/tauri/src/menu/check.rs
Normal file
129
crates/tauri/src/menu/check.rs
Normal file
@@ -0,0 +1,129 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::run_item_main_thread;
|
||||
use crate::menu::CheckMenuItemInner;
|
||||
use crate::run_main_thread;
|
||||
use crate::{AppHandle, Manager, Runtime, menu::MenuId};
|
||||
|
||||
use super::CheckMenuItem;
|
||||
|
||||
impl<R: Runtime> CheckMenuItem<R> {
|
||||
/// Create a new menu item.
|
||||
///
|
||||
/// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic
|
||||
/// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
|
||||
pub fn new<M, T, A>(
|
||||
manager: &M,
|
||||
text: T,
|
||||
enabled: bool,
|
||||
checked: bool,
|
||||
accelerator: Option<A>,
|
||||
) -> crate::Result<Self>
|
||||
where
|
||||
M: Manager<R>,
|
||||
T: AsRef<str>,
|
||||
A: AsRef<str>,
|
||||
{
|
||||
let handle = manager.app_handle();
|
||||
let app_handle = handle.clone();
|
||||
|
||||
let text = text.as_ref().to_owned();
|
||||
let accelerator = accelerator.and_then(|s| s.as_ref().parse().ok());
|
||||
|
||||
let item = run_main_thread!(handle, || {
|
||||
let item = muda::CheckMenuItem::new(text, enabled, checked, accelerator);
|
||||
CheckMenuItemInner::new(app_handle, item)
|
||||
})?;
|
||||
|
||||
Ok(Self(Arc::new(item)))
|
||||
}
|
||||
|
||||
/// Create a new menu item with the specified id.
|
||||
///
|
||||
/// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic
|
||||
/// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
|
||||
pub fn with_id<M, I, T, A>(
|
||||
manager: &M,
|
||||
id: I,
|
||||
text: T,
|
||||
enabled: bool,
|
||||
checked: bool,
|
||||
accelerator: Option<A>,
|
||||
) -> crate::Result<Self>
|
||||
where
|
||||
M: Manager<R>,
|
||||
I: Into<MenuId>,
|
||||
T: AsRef<str>,
|
||||
A: AsRef<str>,
|
||||
{
|
||||
let handle = manager.app_handle();
|
||||
let app_handle = handle.clone();
|
||||
|
||||
let id = id.into();
|
||||
let text = text.as_ref().to_owned();
|
||||
let accelerator = accelerator.and_then(|s| s.as_ref().parse().ok());
|
||||
|
||||
let item = run_main_thread!(handle, move || {
|
||||
let item = muda::CheckMenuItem::with_id(id, text, enabled, checked, accelerator);
|
||||
CheckMenuItemInner::new(app_handle, item)
|
||||
})?;
|
||||
|
||||
Ok(Self(Arc::new(item)))
|
||||
}
|
||||
|
||||
/// The application handle associated with this type.
|
||||
pub fn app_handle(&self) -> &AppHandle<R> {
|
||||
&self.0.app_handle
|
||||
}
|
||||
|
||||
/// Returns a unique identifier associated with this menu item.
|
||||
pub fn id(&self) -> &MenuId {
|
||||
self.0.inner.id()
|
||||
}
|
||||
|
||||
/// Get the text for this menu item.
|
||||
pub fn text(&self) -> crate::Result<String> {
|
||||
run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().text())
|
||||
}
|
||||
|
||||
/// Set the text for this menu item. `text` could optionally contain
|
||||
/// an `&` before a character to assign this character as the mnemonic
|
||||
/// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
|
||||
pub fn set_text<S: AsRef<str>>(&self, text: S) -> crate::Result<()> {
|
||||
let text = text.as_ref().to_string();
|
||||
run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().set_text(text))
|
||||
}
|
||||
|
||||
/// Get whether this menu item is enabled or not.
|
||||
pub fn is_enabled(&self) -> crate::Result<bool> {
|
||||
run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().is_enabled())
|
||||
}
|
||||
|
||||
/// Enable or disable this menu item.
|
||||
pub fn set_enabled(&self, enabled: bool) -> crate::Result<()> {
|
||||
run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().set_enabled(enabled))
|
||||
}
|
||||
|
||||
/// Set this menu item accelerator.
|
||||
pub fn set_accelerator<S: AsRef<str>>(&self, accelerator: Option<S>) -> crate::Result<()> {
|
||||
let accel = accelerator.and_then(|s| s.as_ref().parse().ok());
|
||||
run_item_main_thread!(self, |self_: Self| {
|
||||
(*self_.0).as_ref().set_accelerator(accel)
|
||||
})?
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Get whether this check menu item is checked or not.
|
||||
pub fn is_checked(&self) -> crate::Result<bool> {
|
||||
run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().is_checked())
|
||||
}
|
||||
|
||||
/// Check or Uncheck this check menu item.
|
||||
pub fn set_checked(&self, checked: bool) -> crate::Result<()> {
|
||||
run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().set_checked(checked))
|
||||
}
|
||||
}
|
||||
220
crates/tauri/src/menu/icon.rs
Normal file
220
crates/tauri/src/menu/icon.rs
Normal file
@@ -0,0 +1,220 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::run_item_main_thread;
|
||||
use super::{IconMenuItem, NativeIcon};
|
||||
use crate::menu::IconMenuItemInner;
|
||||
use crate::run_main_thread;
|
||||
use crate::{AppHandle, Manager, Runtime, image::Image, menu::MenuId};
|
||||
|
||||
impl<R: Runtime> IconMenuItem<R> {
|
||||
/// Create a new menu item.
|
||||
///
|
||||
/// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic
|
||||
/// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
|
||||
pub fn new<M, T, A>(
|
||||
manager: &M,
|
||||
text: T,
|
||||
enabled: bool,
|
||||
icon: Option<Image<'_>>,
|
||||
accelerator: Option<A>,
|
||||
) -> crate::Result<Self>
|
||||
where
|
||||
M: Manager<R>,
|
||||
T: AsRef<str>,
|
||||
A: AsRef<str>,
|
||||
{
|
||||
let handle = manager.app_handle();
|
||||
let app_handle = handle.clone();
|
||||
|
||||
let text = text.as_ref().to_owned();
|
||||
let accelerator = accelerator.and_then(|s| s.as_ref().parse().ok());
|
||||
let icon = match icon {
|
||||
Some(i) => Some(i.try_into()?),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let item = run_main_thread!(handle, || {
|
||||
let item = muda::IconMenuItem::new(text, enabled, icon, accelerator);
|
||||
IconMenuItemInner::new(app_handle, item)
|
||||
})?;
|
||||
|
||||
Ok(Self(Arc::new(item)))
|
||||
}
|
||||
|
||||
/// Create a new menu item with the specified id.
|
||||
///
|
||||
/// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic
|
||||
/// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
|
||||
pub fn with_id<M, I, T, A>(
|
||||
manager: &M,
|
||||
id: I,
|
||||
text: T,
|
||||
enabled: bool,
|
||||
icon: Option<Image<'_>>,
|
||||
accelerator: Option<A>,
|
||||
) -> crate::Result<Self>
|
||||
where
|
||||
M: Manager<R>,
|
||||
I: Into<MenuId>,
|
||||
T: AsRef<str>,
|
||||
A: AsRef<str>,
|
||||
{
|
||||
let handle = manager.app_handle();
|
||||
let app_handle = handle.clone();
|
||||
|
||||
let id = id.into();
|
||||
let text = text.as_ref().to_owned();
|
||||
let accelerator = accelerator.and_then(|s| s.as_ref().parse().ok());
|
||||
let icon = match icon {
|
||||
Some(i) => Some(i.try_into()?),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let item = run_main_thread!(handle, move || {
|
||||
let item = muda::IconMenuItem::with_id(id, text, enabled, icon, accelerator);
|
||||
IconMenuItemInner::new(app_handle, item)
|
||||
})?;
|
||||
|
||||
Ok(Self(Arc::new(item)))
|
||||
}
|
||||
|
||||
/// Create a new icon menu item but with a native icon.
|
||||
///
|
||||
/// See [`IconMenuItem::new`] for more info.
|
||||
///
|
||||
/// ## Platform-specific:
|
||||
///
|
||||
/// - **Windows / Linux**: Unsupported.
|
||||
pub fn with_native_icon<M, T, A>(
|
||||
manager: &M,
|
||||
text: T,
|
||||
enabled: bool,
|
||||
native_icon: Option<NativeIcon>,
|
||||
accelerator: Option<A>,
|
||||
) -> crate::Result<Self>
|
||||
where
|
||||
M: Manager<R>,
|
||||
T: AsRef<str>,
|
||||
A: AsRef<str>,
|
||||
{
|
||||
let handle = manager.app_handle();
|
||||
let app_handle = handle.clone();
|
||||
|
||||
let text = text.as_ref().to_owned();
|
||||
let icon = native_icon.map(Into::into);
|
||||
let accelerator = accelerator.and_then(|s| s.as_ref().parse().ok());
|
||||
|
||||
let item = run_main_thread!(handle, || {
|
||||
let item = muda::IconMenuItem::with_native_icon(text, enabled, icon, accelerator);
|
||||
IconMenuItemInner::new(app_handle, item)
|
||||
})?;
|
||||
|
||||
Ok(Self(Arc::new(item)))
|
||||
}
|
||||
|
||||
/// Create a new icon menu item with the specified id but with a native icon.
|
||||
///
|
||||
/// See [`IconMenuItem::new`] for more info.
|
||||
///
|
||||
/// ## Platform-specific:
|
||||
///
|
||||
/// - **Windows / Linux**: Unsupported.
|
||||
pub fn with_id_and_native_icon<M, I, T, A>(
|
||||
manager: &M,
|
||||
id: I,
|
||||
text: T,
|
||||
enabled: bool,
|
||||
native_icon: Option<NativeIcon>,
|
||||
accelerator: Option<A>,
|
||||
) -> crate::Result<Self>
|
||||
where
|
||||
M: Manager<R>,
|
||||
I: Into<MenuId>,
|
||||
T: AsRef<str>,
|
||||
A: AsRef<str>,
|
||||
{
|
||||
let handle = manager.app_handle();
|
||||
let app_handle = handle.clone();
|
||||
|
||||
let id = id.into();
|
||||
let text = text.as_ref().to_owned();
|
||||
let icon = native_icon.map(Into::into);
|
||||
let accelerator = accelerator.and_then(|s| s.as_ref().parse().ok());
|
||||
|
||||
let item = run_main_thread!(handle, move || {
|
||||
let item = muda::IconMenuItem::with_id_and_native_icon(id, text, enabled, icon, accelerator);
|
||||
IconMenuItemInner::new(app_handle, item)
|
||||
})?;
|
||||
|
||||
Ok(Self(Arc::new(item)))
|
||||
}
|
||||
|
||||
/// The application handle associated with this type.
|
||||
pub fn app_handle(&self) -> &AppHandle<R> {
|
||||
&self.0.app_handle
|
||||
}
|
||||
|
||||
/// Returns a unique identifier associated with this menu item.
|
||||
pub fn id(&self) -> &MenuId {
|
||||
self.0.inner.id()
|
||||
}
|
||||
|
||||
/// Get the text for this menu item.
|
||||
pub fn text(&self) -> crate::Result<String> {
|
||||
run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().text())
|
||||
}
|
||||
|
||||
/// Set the text for this menu item. `text` could optionally contain
|
||||
/// an `&` before a character to assign this character as the mnemonic
|
||||
/// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
|
||||
pub fn set_text<S: AsRef<str>>(&self, text: S) -> crate::Result<()> {
|
||||
let text = text.as_ref().to_string();
|
||||
run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().set_text(text))
|
||||
}
|
||||
|
||||
/// Get whether this menu item is enabled or not.
|
||||
pub fn is_enabled(&self) -> crate::Result<bool> {
|
||||
run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().is_enabled())
|
||||
}
|
||||
|
||||
/// Enable or disable this menu item.
|
||||
pub fn set_enabled(&self, enabled: bool) -> crate::Result<()> {
|
||||
run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().set_enabled(enabled))
|
||||
}
|
||||
|
||||
/// Set this menu item accelerator.
|
||||
pub fn set_accelerator<S: AsRef<str>>(&self, accelerator: Option<S>) -> crate::Result<()> {
|
||||
let accel = accelerator.and_then(|s| s.as_ref().parse().ok());
|
||||
run_item_main_thread!(self, |self_: Self| {
|
||||
(*self_.0).as_ref().set_accelerator(accel)
|
||||
})?
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Change this menu item icon or remove it.
|
||||
pub fn set_icon(&self, icon: Option<Image<'_>>) -> crate::Result<()> {
|
||||
let icon = match icon {
|
||||
Some(i) => Some(i.try_into()?),
|
||||
None => None,
|
||||
};
|
||||
run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().set_icon(icon))
|
||||
}
|
||||
|
||||
/// Change this menu item icon to a native image or remove it.
|
||||
///
|
||||
/// ## Platform-specific:
|
||||
///
|
||||
/// - **Windows / Linux**: Unsupported.
|
||||
pub fn set_native_icon(&self, _icon: Option<NativeIcon>) -> crate::Result<()> {
|
||||
#[cfg(target_os = "macos")]
|
||||
return run_item_main_thread!(self, |self_: Self| {
|
||||
(*self_.0).as_ref().set_native_icon(_icon.map(Into::into))
|
||||
});
|
||||
#[allow(unreachable_code)]
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user