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

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

BIN
examples/.icons/128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
examples/.icons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@@ -0,0 +1,35 @@
{
"fill": {
"automatic-gradient": "extended-srgb:0.00000,0.53333,1.00000,1.00000"
},
"groups": [
{
"layers": [
{
"blend-mode": "normal",
"fill": "automatic",
"glass": false,
"hidden": false,
"image-name": "icon.png",
"name": "icon",
"position": {
"scale": 2,
"translation-in-points": [0, 0]
}
}
],
"shadow": {
"kind": "neutral",
"opacity": 0.5
},
"translucency": {
"enabled": true,
"value": 0.5
}
}
],
"supported-platforms": {
"circles": ["watchOS"],
"squares": "shared"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
examples/.icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
examples/.icons/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 804 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1015 B

5
examples/README.md Normal file
View File

@@ -0,0 +1,5 @@
# Examples
Following directories contain examples of tauri app functionality.
There are different execution steps depending on the example. See each directory `README.md` for execution method.

2
examples/api/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/dist/*
!/dist/.gitkeep

View File

@@ -0,0 +1,6 @@
#!/usr/bin/env bash
export ICONS_VOLUME="$(realpath ../.icons)"
export DIST_VOLUME="$(realpath dist)"
export ISOLATION_VOLUME="$(realpath isolation-dist)"
export WORKSPACE_VOLUME="$(realpath ../..)"

3
examples/api/.taurignore Normal file
View File

@@ -0,0 +1,3 @@
src-tauri/locales/
src-tauri/Cross.toml
src-tauri/.gitignore

29
examples/api/README.md Normal file
View File

@@ -0,0 +1,29 @@
# API example
This example demonstrates Tauri's API capabilities using the `@tauri-apps/api` package. It's used as the main validation app, serving as the testbed of our development process.
In the future, this app will be used on Tauri's integration tests.
![App screenshot](./screenshot.png?raw=true)
## Running the example
- Compile Tauri
go to root of the Tauri repo and run:
```
pnpm i
pnpm build:debug
```
- Run the app in development mode (Run inside of this folder `examples/api/`)
```bash
$ pnpm tauri dev
```
- Build an run the release app (Run inside of this folder `examples/api/`)
```bash
$ pnpm tauri build
$ ../../target/release/api
```

0
examples/api/dist/.gitkeep vendored Normal file
View File

14
examples/api/index.html Normal file
View File

@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="dark light" />
<title>API Example App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@@ -0,0 +1,10 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Isolation Secure Script</title>
</head>
<body>
<script src="index.js"></script>
</body>
</html>

View File

@@ -0,0 +1,7 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
window.__TAURI_ISOLATION_HOOK__ = (payload, options) => {
return payload
}

26
examples/api/package.json Normal file
View File

@@ -0,0 +1,26 @@
{
"name": "api",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"tauri": "node ../../packages/cli/tauri.js"
},
"dependencies": {
"@tauri-apps/api": "../../packages/api/dist"
},
"devDependencies": {
"@iconify-json/codicon": "^1.2.49",
"@iconify-json/ph": "^1.2.2",
"@sveltejs/vite-plugin-svelte": "^7.2.0",
"@types/sortablejs": "^1.15.9",
"@unocss/extractor-svelte": "^66.7.5",
"sortablejs": "^1.15.7",
"svelte": "^5.56.4",
"unocss": "^66.7.5",
"vite": "^8.1.4"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

6
examples/api/src-tauri/.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# cargo-mobile
/gen

View File

@@ -0,0 +1 @@
tauri-plugin-sample/

View File

@@ -0,0 +1,51 @@
[package]
name = "api"
version = "0.1.0"
description = "An example Tauri Application showcasing the api"
edition = "2024"
rust-version = "1.90"
license = "Apache-2.0 OR MIT"
[lib]
name = "api_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { workspace = true, default-features = true, features = [
"codegen",
"isolation",
] }
[dependencies]
serde_json = "1"
serde = { version = "1", features = ["derive"] }
log = "0.4.21"
tauri-plugin-sample = { path = "./tauri-plugin-sample/" }
tauri-plugin-log = "2"
# WebDriver automation bridge, used by the `@tauri-apps/api` e2e suite (packages/api-e2e).
# Desktop-only and behind the off-by-default `automation` feature so it never ships in a
# regular build.
[target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies]
tauri-plugin-automation = { version = "0.2", optional = true }
[dependencies.tauri]
workspace = true
default-features = true
features = [
"protocol-asset",
"image-ico",
"image-png",
"isolation",
"macos-private-api",
"tray-icon",
]
[dev-dependencies.tauri]
workspace = true
features = ["test"]
[features]
prod = ["tauri/custom-protocol"]
# Enables the WebDriver automation bridge; set by the e2e suite's build (packages/api-e2e).
automation = ["dep:tauri-plugin-automation"]

View File

@@ -0,0 +1,17 @@
[build.env]
# must set ICONS_VOLUME, DIST_VOLUME, ISOLATION_VOLUME and WORKSPACE_VOLUME environment variables
# ICONS_VOLUME: absolute path to the .icons folder
# DIST_VOLUME: absolute path to the dist folder
# ISOLATION_VOLUME: absolute path to the isolation dist folder
# WORKSPACE_VOLUME: absolute path to the workspace
# this can be done running `$ . .setup-cross.sh` in the examples/api folder
volumes = [
"ICONS_VOLUME",
"DIST_VOLUME",
"ISOLATION_VOLUME",
"WORKSPACE_VOLUME",
]
[target.aarch64-unknown-linux-gnu]
image = "aarch64-unknown-linux-gnu:latest"
#image = "ghcr.io/tauri-apps/tauri/aarch64-unknown-linux-gnu:latest"

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSCameraUsageDescription</key>
<string>Request camera access for WebRTC</string>
<key>NSMicrophoneUsageDescription</key>
<string>Request microphone access for WebRTC</string>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<true/>
<key>UISceneConfigurations</key>
<dict/>
</dict>
</dict>
</plist>

View File

@@ -0,0 +1,56 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use tauri_build::WindowsAttributes;
fn main() {
tauri_build::try_build(
tauri_build::Attributes::new()
.codegen(tauri_build::CodegenContext::new())
.windows_attributes(WindowsAttributes::new_without_app_manifest())
.plugin(
"app-menu",
tauri_build::InlinedPlugin::new().commands(&["toggle", "popup"]),
)
.app_manifest(tauri_build::AppManifest::new().commands(&[
"log_operation",
"perform_request",
"echo",
"spam",
])),
)
.expect("failed to run tauri-build");
#[cfg(windows)]
{
// workaround needed to prevent `STATUS_ENTRYPOINT_NOT_FOUND` error in tests
// see https://github.com/tauri-apps/tauri/pull/4383#issuecomment-1212221864
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
let target_env = std::env::var("CARGO_CFG_TARGET_ENV");
let is_tauri_workspace = std::env::var("__TAURI_WORKSPACE__").is_ok_and(|v| v == "true");
if is_tauri_workspace && target_os == "windows" && Ok("msvc") == target_env.as_deref() {
embed_manifest_for_tests();
}
}
}
#[cfg(windows)]
fn embed_manifest_for_tests() {
static WINDOWS_MANIFEST_FILE: &str = "windows-app-manifest.xml";
let manifest = std::env::current_dir()
.unwrap()
.join("../../../crates/tauri-build/src")
.join(WINDOWS_MANIFEST_FILE);
println!("cargo:rerun-if-changed={}", manifest.display());
// Embed the Windows application manifest file.
println!("cargo:rustc-link-arg=/MANIFEST:EMBED");
println!(
"cargo:rustc-link-arg=/MANIFESTINPUT:{}",
manifest.to_str().unwrap()
);
// Turn linker warnings into errors.
println!("cargo:rustc-link-arg=/WX");
}

View File

@@ -0,0 +1 @@
schemas/

View File

@@ -0,0 +1,16 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "secondary-window",
"description": "capability for secondary window",
"windows": ["main-*"],
"permissions": [
{
"identifier": "sample:allow-ping",
"deny": [
{
"path": "tauri.app"
}
]
}
]
}

View File

@@ -0,0 +1,75 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "run-app",
"description": "permissions to run the app",
"windows": ["main", "main-*"],
"permissions": [
"core:window:allow-is-enabled",
"core:window:allow-set-enabled",
{
"identifier": "allow-log-operation",
"allow": [
{
"event": "tauri-click"
}
]
},
"allow-perform-request",
"allow-echo",
"allow-spam",
"app-menu:default",
"sample:allow-ping-scoped",
"sample:allow-js-values",
"sample:global-scope",
"core:default",
"core:app:allow-app-hide",
"core:app:allow-app-show",
"core:app:allow-set-app-theme",
"core:app:allow-set-dock-visibility",
"core:app:allow-default-window-icon",
"core:app:allow-exit",
"core:window:allow-set-theme",
"core:window:allow-center",
"core:window:allow-request-user-attention",
"core:window:allow-set-resizable",
"core:window:allow-set-maximizable",
"core:window:allow-set-minimizable",
"core:window:allow-set-closable",
"core:window:allow-set-title",
"core:window:allow-maximize",
"core:window:allow-unmaximize",
"core:window:allow-minimize",
"core:window:allow-unminimize",
"core:window:allow-show",
"core:window:allow-hide",
"core:window:allow-close",
"core:window:allow-set-decorations",
"core:window:allow-set-shadow",
"core:window:allow-set-effects",
"core:window:allow-set-always-on-top",
"core:window:allow-set-always-on-bottom",
"core:window:allow-set-content-protected",
"core:window:allow-set-size",
"core:window:allow-set-min-size",
"core:window:allow-set-max-size",
"core:window:allow-set-position",
"core:window:allow-set-fullscreen",
"core:window:allow-set-fullscreen-on-monitor",
"core:window:allow-set-simple-fullscreen",
"core:window:allow-set-focus",
"core:window:allow-set-focusable",
"core:window:allow-set-skip-taskbar",
"core:window:allow-set-cursor-grab",
"core:window:allow-set-cursor-visible",
"core:window:allow-set-cursor-icon",
"core:window:allow-set-cursor-position",
"core:window:allow-set-ignore-cursor-events",
"core:window:allow-start-dragging",
"core:window:allow-set-progress-bar",
"core:window:allow-set-icon",
"core:window:allow-toggle-maximize",
"core:webview:allow-create-webview-window",
"core:webview:allow-print",
"core:webview:allow-set-webview-zoom"
]
}

View File

@@ -0,0 +1,6 @@
<WixLocalization Culture="pt-BR" xmlns="http://schemas.microsoft.com/wix/2006/localization">
<String Id="LaunchApp">Executar Tauri API</String>
<String Id="DowngradeErrorMessage">Uma versão mais recente de Tauri API está instalada.</String>
<String Id="PathEnvVarFeature">Adiciona o caminho do executável de Tauri API para a variável de ambiente PATH. Isso permite Tauri API ser executado pela linha de comando.</String>
<String Id="InstallAppFeature">Instala Tauri API.</String>
</WixLocalization>

View File

@@ -0,0 +1,3 @@
[default]
description = "Default permissions for the plugin"
permissions = ["allow-toggle", "allow-popup"]

View File

@@ -0,0 +1,11 @@
# Automatically generated - DO NOT EDIT!
[[permission]]
identifier = "allow-echo"
description = "Enables the echo command without any pre-configured scope."
commands.allow = ["echo"]
[[permission]]
identifier = "deny-echo"
description = "Denies the echo command without any pre-configured scope."
commands.deny = ["echo"]

View File

@@ -0,0 +1,11 @@
# Automatically generated - DO NOT EDIT!
[[permission]]
identifier = "allow-log-operation"
description = "Enables the log_operation command without any pre-configured scope."
commands.allow = ["log_operation"]
[[permission]]
identifier = "deny-log-operation"
description = "Denies the log_operation command without any pre-configured scope."
commands.deny = ["log_operation"]

View File

@@ -0,0 +1,11 @@
# Automatically generated - DO NOT EDIT!
[[permission]]
identifier = "allow-perform-request"
description = "Enables the perform_request command without any pre-configured scope."
commands.allow = ["perform_request"]
[[permission]]
identifier = "deny-perform-request"
description = "Denies the perform_request command without any pre-configured scope."
commands.deny = ["perform_request"]

View File

@@ -0,0 +1,11 @@
# Automatically generated - DO NOT EDIT!
[[permission]]
identifier = "allow-spam"
description = "Enables the spam command without any pre-configured scope."
commands.allow = ["spam"]
[[permission]]
identifier = "deny-spam"
description = "Denies the spam command without any pre-configured scope."
commands.deny = ["spam"]

View File

@@ -0,0 +1,63 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use serde::{Deserialize, Serialize};
use tauri::{
command,
ipc::{Channel, CommandScope},
};
#[derive(Debug, Deserialize)]
#[allow(unused)]
pub struct RequestBody {
id: i32,
name: String,
}
#[derive(Debug, Deserialize)]
pub struct LogScope {
event: String,
}
#[command]
pub fn log_operation(
event: String,
payload: Option<String>,
command_scope: CommandScope<LogScope>,
) -> Result<(), &'static str> {
if command_scope.denies().iter().any(|s| s.event == event) {
Err("denied")
} else if !command_scope.allows().iter().any(|s| s.event == event) {
Err("not allowed")
} else {
log::info!("{event} {payload:?}");
Ok(())
}
}
#[derive(Serialize)]
pub struct ApiResponse {
message: String,
}
#[command]
pub fn perform_request(endpoint: String, body: RequestBody) -> ApiResponse {
println!("{endpoint} {body:?}");
ApiResponse {
message: "message response".into(),
}
}
#[command]
pub fn echo(request: tauri::ipc::Request<'_>) -> tauri::ipc::Response {
tauri::ipc::Response::new(request.body().clone())
}
#[command]
pub fn spam(channel: Channel<i32>) -> tauri::Result<()> {
for i in 1..=1_000 {
channel.send(i)?;
}
Ok(())
}

View File

@@ -0,0 +1,231 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
mod cmd;
#[cfg(all(desktop, not(test)))]
mod menu_plugin;
#[cfg(all(desktop, not(test)))]
mod tray;
use serde::Serialize;
use tauri::{
App, Emitter, Listener, Runtime, WebviewUrl,
ipc::Channel,
webview::{PageLoadEvent, WebviewWindowBuilder},
};
#[allow(unused)]
use tauri::{Manager, RunEvent};
use tauri_plugin_sample::{PingRequest, SampleExt};
#[derive(Clone, Serialize)]
struct Reply {
data: String,
}
#[cfg(target_os = "macos")]
pub struct AppMenu<R: Runtime>(pub std::sync::Mutex<Option<tauri::menu::Menu<R>>>);
#[cfg(all(desktop, not(test)))]
pub struct PopupMenu<R: Runtime>(tauri::menu::Menu<R>);
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
run_app(tauri::Builder::default(), |_app| {})
}
pub fn run_app<R: Runtime, F: FnOnce(&App<R>) + Send + 'static>(
builder: tauri::Builder<R>,
setup: F,
) {
// WebDriver automation bridge for the `@tauri-apps/api` e2e suite (packages/api-e2e).
// Registered as early as possible per the plugin's docs. Behind the off-by-default
// `automation` feature, and `not(test)` so it never interferes with the mock-runtime
// unit test below.
#[cfg(all(desktop, feature = "automation", not(test)))]
let builder = builder.plugin(tauri_plugin_automation::init());
let builder = builder
.plugin(
tauri_plugin_log::Builder::default()
.level(log::LevelFilter::Info)
.build(),
)
.plugin(tauri_plugin_sample::init())
.setup(move |app| {
#[cfg(all(desktop, not(test)))]
{
let handle = app.handle();
tray::create_tray(handle)?;
handle.plugin(menu_plugin::init())?;
}
#[cfg(target_os = "macos")]
app.manage(AppMenu::<R>(Default::default()));
#[cfg(all(desktop, not(test)))]
app.manage(PopupMenu(
tauri::menu::MenuBuilder::new(app)
.check("check", "Tauri is awesome!")
.text("text", "Do something")
.copy()
.build()?,
));
#[allow(unused_mut)]
let mut window_builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::default())
.disable_drag_drop_handler()
.on_document_title_changed(|_window, title| {
println!("document title changed: {title}");
});
#[cfg(all(desktop, not(test)))]
{
use std::sync::atomic::{AtomicU64, Ordering};
let app_ = app.handle().clone();
let created_window_count = AtomicU64::new(0);
window_builder = window_builder
.title("Tauri API Validation")
.inner_size(1000., 800.)
.min_inner_size(600., 400.)
.menu(tauri::menu::Menu::default(app.handle())?)
.on_new_window(move |url, features| {
println!("new window requested: {url:?} {features:?}");
let number = created_window_count.fetch_add(1, Ordering::Relaxed);
let builder = WebviewWindowBuilder::new(
&app_,
format!("new-{number}"),
tauri::WebviewUrl::External("about:blank".parse().unwrap()),
)
.window_features(features)
.on_document_title_changed(|window, title| {
window.set_title(&title).unwrap();
})
.title(url.as_str());
let window = builder.build().unwrap();
tauri::webview::NewWindowResponse::Create { window }
});
// Liquid Glass effects need a transparent window to show through
#[cfg(target_os = "macos")]
{
window_builder = window_builder.transparent(true);
}
}
let _webview = window_builder.build()?;
let value = Some("test".to_string());
let response = app.sample().ping(PingRequest {
value: value.clone(),
on_event: Channel::new(|event| {
println!("got channel event: {event:?}");
Ok(())
}),
});
log::info!("got response: {response:?}");
if let Ok(res) = response {
assert_eq!(res.value, value);
}
setup(app);
Ok(())
})
.on_page_load(|webview, payload| {
if payload.event() == PageLoadEvent::Finished {
let webview_ = webview.clone();
webview.listen("js-event", move |event| {
println!("got js-event with message '{:?}'", event.payload());
let reply = Reply {
data: "something else".to_string(),
};
webview_
.emit("rust-event", Some(reply))
.expect("failed to emit");
});
}
});
#[allow(unused_mut)]
let mut app = builder
.invoke_handler(tauri::generate_handler![
cmd::log_operation,
cmd::perform_request,
cmd::echo,
cmd::spam,
])
.build(tauri::tauri_build_context!())
.expect("error while building tauri application");
#[cfg(target_os = "macos")]
app.set_activation_policy(tauri::ActivationPolicy::Regular);
#[cfg(target_os = "ios")]
let mut counter = 0;
app.run(move |_app_handle, event| {
match event {
// Keep the event loop running even if all windows are closed
// This allow us to catch tray icon events when there is no window
// if we manually requested an exit (code is Some(_)) we will let it go through
#[cfg(all(desktop, not(test)))]
RunEvent::ExitRequested { api, code, .. } if code.is_none() => {
api.prevent_exit();
}
#[cfg(desktop)]
RunEvent::WindowEvent {
event: tauri::WindowEvent::CloseRequested { api, .. },
label,
..
} => {
println!("closing window...");
// run the window destroy manually just for fun :)
// usually you'd show a dialog here to ask for confirmation or whatever
api.prevent_close();
_app_handle
.get_webview_window(&label)
.unwrap()
.destroy()
.unwrap();
}
#[cfg(target_os = "ios")]
RunEvent::SceneRequested { .. } => {
counter += 1;
WebviewWindowBuilder::new(
_app_handle,
format!("main-from-scene-{counter}"),
WebviewUrl::default(),
)
.build()
.unwrap();
}
#[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
RunEvent::Opened { urls } => {
println!("opened urls: {:?}", urls);
}
_ => (),
}
})
}
#[cfg(test)]
mod tests {
use tauri::Manager;
#[test]
fn run_app() {
super::run_app(tauri::test::mock_builder(), |app| {
let window = app.get_webview_window("main").unwrap();
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_secs(1));
window.close().unwrap();
});
})
}
}

View File

@@ -0,0 +1,10 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
api_lib::run();
}

View File

@@ -0,0 +1,50 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use tauri::{
Runtime, command,
plugin::{Builder, TauriPlugin},
};
#[cfg(not(target_os = "macos"))]
#[command]
pub fn toggle<R: tauri::Runtime>(window: tauri::Window<R>) {
if window.is_menu_visible().unwrap_or_default() {
let _ = window.hide_menu();
} else {
let _ = window.show_menu();
}
}
#[cfg(target_os = "macos")]
#[command]
pub fn toggle<R: tauri::Runtime>(
app: tauri::AppHandle<R>,
app_menu: tauri::State<'_, crate::AppMenu<R>>,
) {
if let Some(menu) = app.remove_menu().unwrap() {
app_menu.0.lock().unwrap().replace(menu);
} else {
app
.set_menu(app_menu.0.lock().unwrap().clone().expect("no app menu"))
.unwrap();
}
}
#[command]
pub fn popup<R: tauri::Runtime>(
window: tauri::Window<R>,
popup_menu: tauri::State<'_, crate::PopupMenu<R>>,
) {
window.popup_menu(&popup_menu.0).unwrap();
}
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("app-menu")
.invoke_handler(tauri::generate_handler![
#![plugin(app_menu)]
popup, toggle
])
.build()
}

View File

@@ -0,0 +1,126 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::sync::atomic::{AtomicBool, Ordering};
use tauri::{
Manager, Runtime, WebviewUrl, include_image,
menu::{Menu, MenuItem},
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
};
pub fn create_tray<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> {
let toggle_i = MenuItem::with_id(app, "toggle", "Toggle", true, None::<&str>)?;
let new_window_i = MenuItem::with_id(app, "new-window", "New window", true, None::<&str>)?;
let icon_i_1 = MenuItem::with_id(app, "icon-1", "Icon 1", true, None::<&str>)?;
let icon_i_2 = MenuItem::with_id(app, "icon-2", "Icon 2", true, None::<&str>)?;
#[cfg(target_os = "macos")]
let set_title_i = MenuItem::with_id(app, "set-title", "Set Title", true, None::<&str>)?;
let switch_i = MenuItem::with_id(app, "switch-menu", "Switch Menu", true, None::<&str>)?;
let quit_i = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?;
let remove_tray_i =
MenuItem::with_id(app, "remove-tray", "Remove Tray icon", true, None::<&str>)?;
let menu1 = Menu::with_items(
app,
&[
&toggle_i,
&new_window_i,
&icon_i_1,
&icon_i_2,
#[cfg(target_os = "macos")]
&set_title_i,
&switch_i,
&quit_i,
&remove_tray_i,
],
)?;
let menu2 = Menu::with_items(
app,
&[&toggle_i, &new_window_i, &switch_i, &quit_i, &remove_tray_i],
)?;
let is_menu1 = AtomicBool::new(true);
let _tray = TrayIconBuilder::with_id("tray-1")
.tooltip("Tauri")
.icon(app.default_window_icon().unwrap().clone())
.menu(&menu1)
.show_menu_on_left_click(false)
.on_menu_event(move |app, event| match event.id.as_ref() {
"quit" => {
app.exit(0);
}
"remove-tray" => {
app.remove_tray_by_id("tray-1");
}
"toggle" => {
if let Some(window) = app.get_webview_window("main") {
let new_title = if window.is_visible().unwrap_or_default() {
let _ = window.hide();
"Show"
} else {
let _ = window.show();
let _ = window.set_focus();
"Hide"
};
toggle_i.set_text(new_title).unwrap();
}
}
"new-window" => {
let _webview =
tauri::WebviewWindowBuilder::new(app, "new", WebviewUrl::App("index.html".into()))
.title("Tauri")
.build()
.unwrap();
}
#[cfg(target_os = "macos")]
"set-title" => {
if let Some(tray) = app.tray_by_id("tray-1") {
let _ = tray.set_title(Some("Tauri"));
}
}
i @ "icon-1" | i @ "icon-2" => {
if let Some(tray) = app.tray_by_id("tray-1") {
let icon = if i == "icon-1" {
include_image!("../../.icons/icon.ico")
} else {
include_image!("../../.icons/tray_icon_with_transparency.png")
};
let _ = tray.set_icon(Some(icon));
}
}
"switch-menu" => {
let flag = is_menu1.load(Ordering::Relaxed);
let (menu, tooltip) = if flag {
(menu2.clone(), "Menu 2")
} else {
(menu1.clone(), "Tauri")
};
if let Some(tray) = app.tray_by_id("tray-1") {
let _ = tray.set_menu(Some(menu));
let _ = tray.set_tooltip(Some(tooltip));
}
is_menu1.store(!flag, Ordering::Relaxed);
}
_ => {}
})
.on_tray_icon_event(|tray, event| {
if let TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Up,
..
} = event
{
let app = tray.app_handle();
if let Some(window) = app.get_webview_window("main") {
let _ = window.unminimize();
let _ = window.show();
let _ = window.set_focus();
}
}
})
.build(app)?;
Ok(())
}

View File

@@ -0,0 +1 @@
.tauri

View File

@@ -0,0 +1,18 @@
[package]
name = "tauri-plugin-sample"
version = "0.1.0"
edition = "2024"
links = "tauri-plugin-sample"
[dependencies]
tauri = { workspace = true }
log = "0.4"
serde = "1"
serde_json = "1"
thiserror = "2"
[target.'cfg(any(target_os = "ios", target_os = "android"))'.dependencies]
tauri = { workspace = true, features = ["wry"] }
[build-dependencies]
tauri-plugin = { workspace = true, features = ["build"] }

View File

@@ -0,0 +1,2 @@
/build
.tauri

View File

@@ -0,0 +1,40 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "com.plugin.sample"
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
}
}
kotlin {
compilerOptions {
jvmTarget = JvmTarget.JVM_1_8
}
}
dependencies {
implementation("androidx.core:core-ktx:1.9.0")
implementation("androidx.appcompat:appcompat:1.6.0")
implementation("com.google.android.material:material:1.7.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
implementation(project(":tauri-android"))
}

View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -0,0 +1,31 @@
pluginManagement {
repositories {
mavenCentral()
gradlePluginPortal()
google()
}
resolutionStrategy {
eachPlugin {
switch (requested.id.id) {
case "com.android.library":
useVersion("8.0.2")
break
case "org.jetbrains.kotlin.android":
useVersion("1.8.20")
break
}
}
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
mavenCentral()
google()
}
}
include ':tauri-android'
project(':tauri-android').projectDir = new File('./.tauri/tauri-api')

View File

@@ -0,0 +1,28 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package com.plugin.sample
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("com.plugin.sample", appContext.packageName)
}
}

View File

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

View File

@@ -0,0 +1,14 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package com.plugin.sample
import android.util.Log
class Example {
fun pong(value: String): String {
Log.i("Pong", value)
return value
}
}

View File

@@ -0,0 +1,57 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package com.plugin.sample
import android.app.Activity
import app.tauri.annotation.Command
import app.tauri.annotation.InvokeArg
import app.tauri.annotation.TauriPlugin
import app.tauri.plugin.Channel
import app.tauri.plugin.JSArray
import app.tauri.plugin.JSObject
import app.tauri.plugin.Plugin
import app.tauri.plugin.Invoke
@InvokeArg
class PingArgs {
var value: String? = null
var onEvent: Channel? = null
}
class JsValues(val array: JSArray, val obj: JSObject)
@TauriPlugin
class ExamplePlugin(private val activity: Activity): Plugin(activity) {
private val implementation = Example()
@Command
fun ping(invoke: Invoke) {
val args = invoke.parseArgs(PingArgs::class.java)
val event = JSObject()
event.put("kind", "ping")
args.onEvent?.send(event)
val ret = JSObject()
ret.put("value", implementation.pong(args.value ?: "default value :("))
invoke.resolve(ret)
}
// Resolves org.json values through the plugin JSON mapper (`resolveObject`),
// both as a property of a plain object and nested in one another.
@Command
fun jsValues(invoke: Invoke) {
val array = JSArray()
array.put("a")
array.put(1)
array.put(true)
val obj = JSObject()
obj.put("kind", "object")
obj.put("nested", JSArray(listOf(1, 2)))
invoke.resolveObject(JsValues(array, obj))
}
}

View File

@@ -0,0 +1,21 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package com.plugin.sample
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)
}
}

View File

@@ -0,0 +1,7 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
if ('__TAURI__' in window) {
window.__TAURI__.sample = {}
}

View File

@@ -0,0 +1,13 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
const COMMANDS: &[&str] = &["ping", "js_values"];
fn main() {
tauri_plugin::Builder::new(COMMANDS)
.android_path("android")
.ios_path("ios")
.global_api_script_path("./api-iife.js")
.build();
}

View File

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

View File

@@ -0,0 +1,35 @@
// 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-plugin-sample",
platforms: [
.macOS(.v10_13),
.iOS(.v13),
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "tauri-plugin-sample",
type: .static,
targets: ["tauri-plugin-sample"]),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
.package(name: "Tauri", path: "../../../../../crates/tauri/mobile/ios-api")
],
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-plugin-sample",
dependencies: [
.byName(name: "Tauri")
],
path: "Sources")
]
)

View File

@@ -0,0 +1,3 @@
# Tauri Plugin sample
A description of this package.

View File

@@ -0,0 +1,32 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
import SwiftRs
import Tauri
import UIKit
import WebKit
class PingArgs: Decodable {
let value: String?
let onEvent: Channel?
}
class ExamplePlugin: Plugin {
@objc public func ping(_ invoke: Invoke) throws {
let args = try invoke.parseArgs(PingArgs.self)
try args.onEvent?.send(["kind": "ping"])
invoke.resolve(["value": args.value ?? ""])
}
@objc public func jsValues(_ invoke: Invoke) throws {
let array: JSArray = ["a", 1, true]
let obj: JSObject = ["kind": "object", "nested": [1, 2] as JSArray]
invoke.resolve(["array": array, "obj": obj])
}
}
@_cdecl("init_plugin_sample")
func initPlugin() -> Plugin {
return ExamplePlugin()
}

View File

@@ -0,0 +1,12 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
import XCTest
@testable import ExamplePlugin
final class ExamplePluginTests: XCTestCase {
func testExample() throws {
let plugin = ExamplePlugin()
}
}

View File

@@ -0,0 +1 @@
schemas/

View File

@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-js-values"
description = "Enables the js_values command without any pre-configured scope."
commands.allow = ["js_values"]
[[permission]]
identifier = "deny-js-values"
description = "Denies the js_values command without any pre-configured scope."
commands.deny = ["js_values"]

View File

@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-ping"
description = "Enables the ping command without any pre-configured scope."
commands.allow = ["ping"]
[[permission]]
identifier = "deny-ping"
description = "Denies the ping command without any pre-configured scope."
commands.deny = ["ping"]

View File

@@ -0,0 +1,87 @@
## Permission Table
<table>
<tr>
<th>Identifier</th>
<th>Description</th>
</tr>
<tr>
<td>
`sample:allow-js-values`
</td>
<td>
Enables the js_values command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`sample:deny-js-values`
</td>
<td>
Denies the js_values command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`sample:allow-ping`
</td>
<td>
Enables the ping command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`sample:deny-ping`
</td>
<td>
Denies the ping command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`sample:global-scope`
</td>
<td>
Sets a global scope.
</td>
</tr>
<tr>
<td>
`sample:allow-ping-scoped`
</td>
<td>
Enables the ping command with a test scope.
</td>
</tr>
</table>

View File

@@ -0,0 +1,7 @@
"$schema" = "schemas/schema.json"
[[permission]]
identifier = "global-scope"
description = "Sets a global scope."
[[permission.scope.allow]]
path = "global"

View File

@@ -0,0 +1,8 @@
"$schema" = "schemas/schema.json"
[[permission]]
identifier = "allow-ping-scoped"
description = "Enables the ping command with a test scope."
commands.allow = ["ping"]
[[permission.scope.allow]]
path = "x"

View File

@@ -0,0 +1,30 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use serde::de::DeserializeOwned;
use tauri::{AppHandle, Runtime, plugin::PluginApi};
use crate::models::*;
pub fn init<R: Runtime, C: DeserializeOwned>(
app: &AppHandle<R>,
_api: PluginApi<R, C>,
) -> crate::Result<Sample<R>> {
Ok(Sample(app.clone()))
}
/// A helper class to access the sample APIs.
pub struct Sample<R: Runtime>(AppHandle<R>);
impl<R: Runtime> Sample<R> {
pub fn ping(&self, payload: PingRequest) -> crate::Result<PingResponse> {
payload.on_event.send(Event {
kind: "ping".to_string(),
value: payload.value.clone(),
})?;
Ok(PingResponse {
value: payload.value,
})
}
}

View File

@@ -0,0 +1,14 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[cfg(mobile)]
#[error(transparent)]
PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError),
#[error(transparent)]
Tauri(#[from] tauri::Error),
}
pub type Result<T> = std::result::Result<T, Error>;

View File

@@ -0,0 +1,101 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use serde::Deserialize;
use std::path::PathBuf;
use tauri::{
Manager, Runtime, State,
plugin::{Builder, TauriPlugin},
};
pub use models::*;
#[cfg(desktop)]
mod desktop;
#[cfg(mobile)]
mod mobile;
mod error;
mod models;
#[cfg(desktop)]
use desktop::Sample;
#[cfg(mobile)]
use mobile::Sample;
pub use error::*;
/// Extensions to [`tauri::App`], [`tauri::AppHandle`] and [`tauri::Window`] to access the sample APIs.
pub trait SampleExt<R: Runtime> {
fn sample(&self) -> State<'_, Sample<R>>;
}
impl<R: Runtime, T: Manager<R>> crate::SampleExt<R> for T {
fn sample(&self) -> State<'_, Sample<R>> {
self.state::<Sample<R>>()
}
}
#[allow(dead_code)]
#[derive(Debug, Deserialize)]
struct PingScope {
path: PathBuf,
}
#[allow(dead_code)]
#[derive(Debug, Deserialize)]
struct SampleScope {
path: PathBuf,
}
#[tauri::command]
fn ping<R: tauri::Runtime>(
app: tauri::AppHandle<R>,
value: Option<String>,
scope: tauri::ipc::CommandScope<PingScope>,
global_scope: tauri::ipc::GlobalScope<SampleScope>,
) -> std::result::Result<PingResponse, String> {
println!("local scope {scope:?}");
println!("global scope {global_scope:?}");
app
.sample()
.ping(PingRequest {
value,
on_event: tauri::ipc::Channel::new(|_| Ok(())),
})
.map_err(|e| e.to_string())
}
/// Resolves `JSArray` and `JSObject` values from the native (Kotlin or Swift) plugin.
#[tauri::command]
fn js_values<R: tauri::Runtime>(
app: tauri::AppHandle<R>,
) -> std::result::Result<serde_json::Value, String> {
#[cfg(mobile)]
return app.sample().js_values().map_err(|e| e.to_string());
#[cfg(desktop)]
{
let _ = app;
Err("js_values is only implemented on mobile".into())
}
}
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("sample")
.setup(|app, api| {
#[cfg(mobile)]
let sample = mobile::init(app, api)?;
#[cfg(desktop)]
let sample = desktop::init(app, api)?;
app.manage(sample);
Ok(())
})
.invoke_handler(tauri::generate_handler![ping, js_values])
.on_navigation(|window, url| {
println!("navigation {} {url}", window.label());
true
})
.build()
}

View File

@@ -0,0 +1,45 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use serde::de::DeserializeOwned;
use tauri::{
AppHandle, Runtime,
plugin::{PluginApi, PluginHandle},
};
use crate::models::*;
#[cfg(target_os = "android")]
const PLUGIN_IDENTIFIER: &str = "com.plugin.sample";
#[cfg(target_os = "ios")]
tauri::ios_plugin_binding!(init_plugin_sample);
// initializes the Kotlin or Swift plugin classes
pub fn init<R: Runtime, C: DeserializeOwned>(
_app: &AppHandle<R>,
api: PluginApi<R, C>,
) -> crate::Result<Sample<R>> {
#[cfg(target_os = "android")]
let handle = api.register_android_plugin(PLUGIN_IDENTIFIER, "ExamplePlugin")?;
#[cfg(target_os = "ios")]
let handle = api.register_ios_plugin(init_plugin_sample)?;
Ok(Sample(handle))
}
/// A helper class to access the sample APIs.
pub struct Sample<R: Runtime>(PluginHandle<R>);
impl<R: Runtime> Sample<R> {
pub fn ping(&self, payload: PingRequest) -> crate::Result<PingResponse> {
self
.0
.run_mobile_plugin("ping", payload)
.map_err(Into::into)
}
pub fn js_values(&self) -> crate::Result<serde_json::Value> {
self.0.run_mobile_plugin("jsValues", ()).map_err(Into::into)
}
}

View File

@@ -0,0 +1,24 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use serde::{Deserialize, Serialize};
use tauri::ipc::Channel;
#[derive(Serialize)]
pub struct Event {
pub kind: String,
pub value: Option<String>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PingRequest {
pub value: Option<String>,
pub on_event: Channel<Event>,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct PingResponse {
pub value: Option<String>,
}

View File

@@ -0,0 +1,132 @@
{
"$schema": "../../../crates/tauri-schema-generator/schemas/config.schema.json",
"productName": "Tauri API",
"version": "1.0.0",
"identifier": "com.tauri.api",
"build": {
"frontendDist": "../dist",
"devUrl": "http://localhost:1420",
"beforeDevCommand": "pnpm dev",
"beforeBuildCommand": "pnpm build",
"removeUnusedCommands": true
},
"app": {
"withGlobalTauri": true,
"macOSPrivateApi": true,
"security": {
"pattern": {
"use": "isolation",
"options": {
"dir": "../isolation-dist/"
}
},
"csp": {
"default-src": "'self' customprotocol: asset:",
"connect-src": "ipc: http://ipc.localhost",
"font-src": ["https://fonts.gstatic.com"],
"img-src": "'self' asset: http://asset.localhost blob: data:",
"style-src": "'unsafe-inline' 'self' https://fonts.googleapis.com"
},
"freezePrototype": true,
"assetProtocol": {
"enable": true,
"scope": {
"allow": ["$APPDATA/db/**", "$RESOURCE/**"],
"deny": ["$APPDATA/db/*.stronghold"]
}
}
}
},
"plugins": {
"cli": {
"description": "Tauri API example",
"args": [
{
"short": "c",
"name": "config",
"takesValue": true,
"description": "Config path"
},
{
"short": "t",
"name": "theme",
"takesValue": true,
"description": "App theme",
"possibleValues": ["light", "dark", "system"]
},
{
"short": "v",
"name": "verbose",
"description": "Verbosity level"
}
],
"subcommands": {
"update": {
"description": "Updates the app",
"args": [
{
"short": "b",
"name": "background",
"description": "Update in background"
}
]
}
}
}
},
"bundle": {
"active": true,
"fileAssociations": [
{
"ext": ["png"],
"mimeType": "image/png",
"rank": "Default"
},
{
"ext": ["jpg", "jpeg"],
"mimeType": "image/jpeg",
"rank": "Alternate"
},
{
"ext": ["gif"],
"mimeType": "image/gif",
"rank": "Owner"
},
{
"ext": ["taurijson"],
"exportedType": {
"identifier": "com.tauri.dev-file-associations-demo.taurijson",
"conformsTo": ["public.json"]
}
},
{
"ext": ["taurid"],
"exportedType": {
"identifier": "com.tauri.dev-file-associations-demo.tauridata",
"conformsTo": ["public.data"]
}
}
],
"icon": [
"../../.icons/32x32.png",
"../../.icons/128x128.png",
"../../.icons/128x128@2x.png",
"../../.icons/icon.icns",
"../../.icons/icon.ico",
"../../.icons/AppIcon.icon"
],
"windows": {
"wix": {
"language": {
"en-US": {},
"pt-BR": {
"localePath": "locales/pt-BR.wxl"
}
}
},
"nsis": {
"compression": "none"
}
}
}
}

374
examples/api/src/App.svelte Normal file
View File

@@ -0,0 +1,374 @@
<script module lang="ts">
export type ViewProps = {
onMessage: (value: unknown) => void
}
</script>
<script lang="ts">
import { onMount, tick } from 'svelte'
import { writable } from 'svelte/store'
import { invoke } from '@tauri-apps/api/core'
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
import { setTheme } from '@tauri-apps/api/app'
import Welcome from './views/Welcome.svelte'
import Communication from './views/Communication.svelte'
import Window from './views/Window.svelte'
import WebRTC from './views/WebRTC.svelte'
import App from './views/App.svelte'
import Menu from './views/Menu.svelte'
import Tray from './views/Tray.svelte'
import type { Theme } from '@tauri-apps/api/window'
import { MediaQuery } from 'svelte/reactivity'
document.addEventListener('keydown', (event) => {
if (event.ctrlKey && event.key === 'b') {
invoke('plugin:app-menu|toggle')
}
})
const appWindow = getCurrentWebviewWindow()
appWindow.onDragDropEvent((event) => {
onMessage(event.payload)
})
const userAgent = navigator.userAgent.toLowerCase()
const isMobile = userAgent.includes('android') || userAgent.includes('iphone')
const desktopViews = [
{
label: 'App',
component: App,
icon: 'i-codicon-hubot'
},
{
label: 'Window',
component: Window,
icon: 'i-codicon-window'
},
{
label: 'Menu',
component: Menu,
icon: 'i-ph-list'
},
{
label: 'Tray',
component: Tray,
icon: 'i-ph-tray'
}
]
const views = [
{
label: 'Welcome',
component: Welcome,
icon: 'i-ph-hand-waving'
},
{
label: 'Communication',
component: Communication,
icon: 'i-codicon-radio-tower'
},
...(isMobile ? [] : desktopViews),
{
label: 'WebRTC',
component: WebRTC,
icon: 'i-ph-broadcast'
}
]
let selected = $state.raw(views[0])
// dark/light themes
const preferDark = new MediaQuery('prefers-color-scheme: dark')
let theme = $state<Theme | 'auto'>(
(localStorage.getItem('theme') as Theme | null) || 'auto'
)
function switchTheme() {
switch (theme) {
case 'dark':
theme = 'light'
break
case 'light':
theme = 'auto'
break
case 'auto':
theme = 'dark'
break
}
}
// Follows the selection, or the system when it is `auto`.
$effect(() => {
const isDark = theme === 'auto' ? preferDark.current : theme === 'dark'
document.documentElement.classList.toggle('dark', isDark)
document.documentElement.classList.toggle('light', !isDark)
})
// Only the selection drives the app-wide theme. Reading `preferDark` here would
// make a system (or externally applied) theme change run this again and reset the
// app back to `auto`, undoing whatever just set it.
$effect(() => {
setTheme(theme === 'auto' ? null : theme)
localStorage.setItem('theme', theme)
})
// Console
const messages = writable<string[]>([])
let consoleTextEl: HTMLDivElement
// this function is renders HTML without sanitizing it so it's insecure
// we only use it with our own input data
async function insecureRenderHtml(html: string) {
messages.update((r) => [
...r,
`<pre><strong class="text-accent dark:text-darkAccent">[${new Date().toLocaleTimeString()}]:</strong> ${html}</pre>`
])
await tick()
consoleTextEl.scrollTop = consoleTextEl.scrollHeight
}
async function onMessage(value: unknown) {
const valueStr =
typeof value === 'string'
? value
: JSON.stringify(
value instanceof ArrayBuffer
? Array.from(new Uint8Array(value))
: value,
null,
1
)
insecureRenderHtml(valueStr)
}
function clear() {
messages.update(() => [])
}
let consoleEl: HTMLDivElement
let consoleH = 0
let cStartY = 0
const minConsoleHeight = 50
function startResizingConsole(e: MouseEvent) {
cStartY = e.clientY
const styles = window.getComputedStyle(consoleEl)
consoleH = parseInt(styles.height, 10)
const moveHandler = (e: MouseEvent) => {
const dy = e.clientY - cStartY
const newH = consoleH - dy
consoleEl.style.height = `${newH < minConsoleHeight ? minConsoleHeight : newH}px`
}
const upHandler = () => {
document.removeEventListener('mouseup', upHandler)
document.removeEventListener('mousemove', moveHandler)
}
document.addEventListener('mouseup', upHandler)
document.addEventListener('mousemove', moveHandler)
}
// mobile
let isSideBarOpen = $state(false)
let sidebar: HTMLElement
let sidebarToggle: HTMLElement
let isDraggingSideBar = false
let draggingStartPosX = 0
let draggingEndPosX = 0
const clamp = (min: number, num: number, max: number) =>
Math.min(Math.max(num, min), max)
function toggleSidebar() {
sidebar.style.setProperty(
'--translate-x',
`${isSideBarOpen ? '0' : '-18.75'}rem`
)
}
onMount(() => {
document.addEventListener('click', (e) => {
if (!(e.target instanceof Node)) return
if (sidebarToggle.contains(e.target)) {
isSideBarOpen = !isSideBarOpen
} else if (isSideBarOpen && !sidebar.contains(e.target)) {
isSideBarOpen = false
}
})
document.addEventListener('touchstart', (e) => {
if (!(e.target instanceof Node)) return
if (sidebarToggle.contains(e.target)) return
const x = e.touches[0].clientX
if ((0 < x && x < 20 && !isSideBarOpen) || isSideBarOpen) {
isDraggingSideBar = true
draggingStartPosX = x
}
})
document.addEventListener('touchmove', (e) => {
if (isDraggingSideBar) {
const x = e.touches[0].clientX
draggingEndPosX = x
const delta = (x - draggingStartPosX) / 10
sidebar.style.setProperty(
'--translate-x',
`-${clamp(0, isSideBarOpen ? 0 - delta : 18.75 - delta, 18.75)}rem`
)
}
})
document.addEventListener('touchend', () => {
if (isDraggingSideBar) {
const delta = (draggingEndPosX - draggingStartPosX) / 10
isSideBarOpen = isSideBarOpen ? delta > -(18.75 / 2) : delta > 18.75 / 2
}
isDraggingSideBar = false
})
})
$effect(() => {
toggleSidebar()
})
</script>
<!-- Sidebar toggle, only visible on small screens -->
<div
id="sidebarToggle"
bind:this={sidebarToggle}
class="z-2000 hidden lt-sm:flex justify-center absolute items-center w-8 h-8 rd-8
bg-accent dark:bg-darkAccent active:bg-accentDark dark:active:bg-darkAccentDark text-accentText dark:text-darkAccentText"
>
{#if isSideBarOpen}
<span class="i-codicon-close animate-duration-300ms animate-fade-in"></span>
{:else}
<span class="i-codicon-menu animate-duration-300ms animate-fade-in"></span>
{/if}
</div>
<div
class="flex h-screen w-screen overflow-hidden text-primaryText dark:text-darkPrimaryText"
>
<aside
id="sidebar"
bind:this={sidebar}
class="lt-sm:h-screen lt-sm:shadow-lg lt-sm:shadow lt-sm:transition-transform lt-sm:absolute lt-sm:z-1999
bg-darkPrimaryLighter transition-colors-250 overflow-hidden grid grid-rows-[min-content_auto] content-start select-none px-2"
>
<img
class="self-center p-7 cursor-pointer"
src="tauri_logo.png"
alt="Tauri logo"
/>
<a href="##" class="nv justify-between" onclick={switchTheme}>
{#if theme === 'auto'}
Switch to Dark mode
<div class="i-ph-circle-half-fill"></div>
{:else if theme === 'dark'}
Switch to Light mode
<div class="i-ph-moon"></div>
{:else if theme === 'light'}
Switch to Auto mode
<div class="i-ph-sun"></div>
{/if}
</a>
<br />
<div class="bg-white/5 h-2px"></div>
<br />
<a
class="nv justify-between"
target="_blank"
href="https://v2.tauri.app/start/"
>
Documentation
<span class="i-codicon-link-external"></span>
</a>
<a
class="nv justify-between"
target="_blank"
href="https://github.com/tauri-apps/tauri"
>
GitHub
<span class="i-codicon-link-external"></span>
</a>
<a
class="nv justify-between"
target="_blank"
href="https://github.com/tauri-apps/tauri/tree/dev/examples/api"
>
Source
<span class="i-codicon-link-external"></span>
</a>
<br />
<div class="bg-white/5 h-2px"></div>
<br />
<div class="flex flex-col overflow-y-auto children-flex-none gap-1">
{#each views as view}
<a
href="##"
class="nv {selected === view ? 'nv_selected' : ''}"
onclick={() => {
selected = view
isSideBarOpen = false
}}
>
<div class="{view.icon} mr-2"></div>
<p>{view.label}</p>
</a>
{/each}
</div>
</aside>
<main
class="flex-1 bg-primary dark:bg-darkPrimary transition-transform transition-colors-250 grid grid-rows-[2fr_auto]"
>
<div class="px-5 overflow-hidden grid grid-rows-[auto_1fr]">
<h1>{selected.label}</h1>
<div class="overflow-y-auto">
<div class="mr-2">
<selected.component {onMessage} />
</div>
</div>
</div>
<div
bind:this={consoleEl}
id="console"
class="select-none h-15rem grid grid-rows-[2px_2rem_1fr] gap-1 overflow-hidden"
>
<div
role="button"
tabindex="0"
onmousedown={startResizingConsole}
class="bg-black/20 h-4px cursor-ns-resize"
></div>
<div class="flex justify-between items-center px-2">
<p class="font-semibold">Console</p>
<div
role="button"
tabindex="0"
class="cursor-pointer h-85% rd-1 p-1 flex justify-center items-center
hover:bg-hoverOverlay dark:hover:bg-darkHoverOverlay
active:bg-hoverOverlay/25 dark:active:bg-darkHoverOverlay/25
"
onkeypress={(e) => (e.key === 'Enter' ? clear() : {})}
onclick={clear}
>
<div class="i-codicon-clear-all"></div>
</div>
</div>
<div
bind:this={consoleTextEl}
class="px-2 overflow-y-auto all:font-mono code-block all:text-xs select-text mr-2"
>
{#each $messages as messageHtml}
{@html messageHtml}
{/each}
</div>
</div>
</main>
</div>

86
examples/api/src/app.css Normal file
View File

@@ -0,0 +1,86 @@
:root {
line-height: 1.5;
}
:root.dark {
color-scheme: dark;
}
:root.light {
color-scheme: light;
}
input,
select {
min-width: 0;
}
*:not(h1, h2, h3, h4, h5, h6) {
margin: 0;
padding: 0;
}
* {
box-sizing: border-box;
font-family: 'Rubik', sans-serif;
}
::-webkit-scrollbar {
width: 0.25rem;
height: 3px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
border-radius: 0.25rem;
}
code {
padding: 0.05rem 0.25rem;
}
code.code-block {
padding: 0.5rem;
}
#sidebar {
width: 18.75rem;
}
/* let window effects (vibrancy / Liquid Glass) show through the whole page */
:root.window-effects #sidebar,
:root.window-effects main {
background-color: transparent;
}
@media screen and (width < 640px) {
#sidebar {
--translate-x: -18.75rem;
transform: translateX(var(--translate-x));
}
}
#sidebar,
main {
padding-top: calc(env(safe-area-inset-top) + 1rem);
}
#sidebar {
padding-left: calc(env(safe-area-inset-left) + 0.5rem);
}
main {
padding-right: env(safe-area-inset-right);
}
#sidebarToggle {
margin-top: calc(env(safe-area-inset-top) + 0.5rem);
margin-left: calc(env(safe-area-inset-left) + 0.5rem);
}
#sidebar,
#console {
padding-bottom: calc(env(safe-area-inset-bottom) + 1.5rem);
}

View File

@@ -0,0 +1,293 @@
<script module lang="ts">
import {
CheckMenuItem,
IconMenuItem,
Menu,
MenuItem,
PredefinedMenuItem,
Submenu,
type PredefinedMenuItemOptions
} from '@tauri-apps/api/menu'
export type MenuOptions = {
id: number
kind: MenuItemComponentKind
text: string | undefined
iconPath: string | undefined
checked: boolean | undefined
}
export type Item = MenuOptions & {
menu?: MenuItems
}
export type MenuItems =
| MenuItem
| IconMenuItem
| CheckMenuItem
| PredefinedMenuItem
export type MenuItemClickDetail = {
id: string
text: string
}
export type MenuItemClickHandler = (detail: MenuItemClickDetail) => void
export async function reorderMenuItems(
menu: Menu | Submenu,
reorderItem: Item,
toIndex: number
) {
if (!reorderItem.menu) {
return
}
await menu.remove(reorderItem.menu)
menu.insert(reorderItem.menu, toIndex)
}
</script>
<script lang="ts">
import Sortable from 'sortablejs'
import { onDestroy, onMount } from 'svelte'
import MenuItemComponent, {
type MenuItemComponentKind
} from './MenuItemComponent.svelte'
type PredefinedItem = PredefinedMenuItemOptions['item']
let {
items = $bindable(),
itemClick,
onItemAdded,
onItemRemoved,
onItemMoved
}: {
items: Item[]
itemClick: MenuItemClickHandler
onItemAdded?: (item: Item) => Promise<void>
onItemRemoved?: (item: Item) => Promise<void>
onItemMoved?: (item: Item, toIndex: number) => void
} = $props()
const predefinedOptions: PredefinedItem[] = [
'Separator',
'Copy',
'Cut',
'Paste',
'SelectAll',
'Undo',
'Redo',
'Minimize',
'Maximize',
'Fullscreen',
'Hide',
'HideOthers',
'ShowAll',
'CloseWindow',
'Quit',
'Services',
'BringAllToFront'
]
async function create(options: MenuOptions) {
let { kind, text, iconPath, checked } = options
text ??= ''
switch (kind) {
case 'Normal': {
return await MenuItem.new({
text,
action: (id) => {
const item = items.find((item) => item.id === options.id)
itemClick({ id, text: item?.text ?? text })
}
})
}
case 'Icon': {
return await IconMenuItem.new({
text,
icon: iconPath,
action: (id) => {
const item = items.find((item) => item.id === options.id)
itemClick({ id, text: item?.text ?? text })
}
})
}
case 'Check': {
const checkItem = await CheckMenuItem.new({
text,
checked,
action: async (id) => {
const item = items.find((item) => item.id === options.id)
itemClick({ id, text: item?.text ?? text })
if (item) {
item.checked = await checkItem.isChecked()
}
}
})
return checkItem
}
default: {
return await PredefinedMenuItem.new({
item: kind
})
}
}
}
let currentId = 0
let sourceSortableEl1: HTMLElement
let sourceSortableEl2: HTMLElement
let targetSortableEl: HTMLElement
function makeSourceSortable(sourceSortableEl: HTMLElement) {
return new Sortable(sourceSortableEl, {
draggable: 'div.menu-item',
group: {
name: 'shared',
pull: 'clone',
put: false
},
sort: false,
delayOnTouchOnly: true,
delay: 500
})
}
onMount(() => {
const sourceSortable1 = makeSourceSortable(sourceSortableEl1)
const sourceSortable2 = makeSourceSortable(sourceSortableEl2)
const targetSortable = new Sortable(targetSortableEl, {
dataIdAttr: 'data-id',
group: {
name: 'shared'
},
async onAdd(event) {
const item = event.item
const kind = item.dataset.kind as MenuItemComponentKind
const text = item.dataset.text
const iconPath = item.dataset.iconPath
const checked = item.dataset.checked
const newItem = {
id: currentId,
kind,
text,
iconPath,
checked: checked !== undefined ? checked === 'true' : checked
} as Item
currentId += 1
// HACK: We can't track the element created by Sortable,
// just make a new one and delete the one from Sortable
item.remove()
if (event.newIndex !== undefined) {
items.splice(event.newIndex, 0, newItem)
} else {
items.push(newItem)
}
newItem.menu = await create(newItem)
await onItemAdded?.(newItem)
},
onUpdate(event) {
const [item] = items.splice(event.oldIndex!, 1)
// Sortable's DOM manipulation doesn't work with Svelte,
// so we recreate all the nodes on order changes...
// This must be `toSpliced` to trigger the `#key` block re-render
items = items.toSpliced(event.newIndex!, 0, item!)
onItemMoved?.(item, event.newIndex!)
},
delayOnTouchOnly: true,
delay: 500,
animation: 200
})
return () => {
targetSortable.destroy()
sourceSortable1.destroy()
sourceSortable2.destroy()
}
})
onDestroy(() => {
for (const item of items) {
item.menu?.close()
}
items = []
})
</script>
<div class="grid gap-4 max-w-5xl">
<div class="grid gap-2">
<div>
<h3>Custom</h3>
<div class="flex flex-wrap gap-2" bind:this={sourceSortableEl1}>
<MenuItemComponent kind="Normal" text="A menu item" />
<MenuItemComponent
kind="Icon"
text="Menu item with icon"
iconPath="../../.icons/icon.png"
/>
<MenuItemComponent kind="Check" text="A check menu" checked={false} />
</div>
</div>
<div>
<h3>Predefined</h3>
<div class="flex flex-wrap gap-2" bind:this={sourceSortableEl2}>
{#each predefinedOptions as predefinedOption}
<MenuItemComponent kind={predefinedOption} />
{/each}
</div>
</div>
</div>
<div class="h-1px bg-neutral/20 m-inline-4"></div>
<div
bind:this={targetSortableEl}
class="p-2 border border-solid border-neutral-300 dark:border-neutral-700 rounded-md min-h-24 grid items-start gap-1"
>
{#key items}
{#each items as item, i (item.id)}
<MenuItemComponent
kind={item.kind}
id={item.id}
bind:text={item.text}
bind:iconPath={item.iconPath}
bind:checked={item.checked}
onTextChange={item.text !== undefined
? () => {
item.menu?.setText(item.text ?? '')
}
: undefined}
onIconPathChange={item.iconPath !== undefined
? () => {
;(item.menu as IconMenuItem | undefined)?.setIcon(
item.iconPath!
)
}
: undefined}
onCheckedChange={item.checked !== undefined
? () => {
;(item.menu as CheckMenuItem | undefined)?.setChecked(
item.checked!
)
}
: undefined}
onRemove={async () => {
items.splice(i, 1)
await onItemRemoved?.(item).finally(() => {
item.menu?.close()
})
}}
/>
{:else}
<div class="place-self-center color-secondaryText">
Drag and drop the menu items above to start building
</div>
{/each}
{/key}
</div>
</div>

View File

@@ -0,0 +1,79 @@
<script module lang="ts">
import type { PredefinedMenuItemOptions } from '@tauri-apps/api/menu'
type PredefinedItem = PredefinedMenuItemOptions['item']
export type MenuItemComponentKind =
| PredefinedItem
| 'Normal'
| 'Icon'
| 'Check'
</script>
<script lang="ts">
let {
id,
kind,
text = $bindable(),
iconPath = $bindable(),
checked = $bindable(),
onTextChange,
onIconPathChange,
onCheckedChange,
onRemove
}: {
id?: number
kind: MenuItemComponentKind
text?: string
iconPath?: string
checked?: boolean
onTextChange?: () => void
onIconPathChange?: () => void
onCheckedChange?: () => void
onRemove?: () => void
} = $props()
</script>
<div
class="flex items-center gap-2 border border-solid border-neutral-200 dark:border-none bg-neutral-100 dark:bg-neutral-700 p-inline-2 p-block-1 rounded-md select-none menu-item"
data-kind={kind}
data-text={text}
data-icon-path={iconPath}
data-checked={checked}
data-id={id}
>
{kind}
{#if text !== undefined}
<input
type="text"
title="text"
placeholder="text"
class="border border-solid dark:bg-neutral-800 dark:color-neutral-100 border-neutral-200 dark:border-neutral-700 focus-visible:border-accent outline-none p-inline-2 p-block-1 rounded-1"
bind:value={text}
onchange={onTextChange}
/>
{/if}
{#if iconPath !== undefined}
<input
type="text"
title="icon path"
placeholder="icon path"
class="border border-solid dark:bg-neutral-800 dark:color-neutral-100 border-neutral-200 dark:border-neutral-700 focus-visible:border-accent outline-none p-inline-2 p-block-1 rounded-1"
bind:value={iconPath}
onchange={onIconPathChange}
/>
{/if}
{#if checked !== undefined}
<input
type="checkbox"
title="checked"
bind:checked
onchange={onCheckedChange}
/>
{/if}
{#if onRemove !== undefined}
<button onclick={onRemove} title="remove" class="btn m-l-auto">
<div class="i-ph-trash"></div>
</button>
{/if}
</div>

14
examples/api/src/main.ts Normal file
View File

@@ -0,0 +1,14 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
import 'uno.css'
import './app.css'
import App from './App.svelte'
import { mount } from 'svelte'
const app = mount(App, {
target: document.querySelector('#app')!
})
export default app

View File

@@ -0,0 +1,43 @@
<script lang="ts">
import { show, hide, setDockVisibility } from '@tauri-apps/api/app'
import type { ViewProps } from '../App.svelte'
let { onMessage }: ViewProps = $props()
let dockVisible = $state(true)
function showApp() {
hideApp()
.then(() => {
setTimeout(() => {
show()
.then(() => onMessage('Shown app'))
.catch(onMessage)
}, 2000)
})
.catch(onMessage)
}
function hideApp() {
return hide()
.then(() => onMessage('Hide app'))
.catch(onMessage)
}
async function toggleDockVisibility() {
await setDockVisibility(!dockVisible)
dockVisible = !dockVisible
}
</script>
<div class="flex gap-2">
<button
class="btn"
id="show"
title="Hides and shows the app after 2 seconds"
onclick={showApp}>Show</button
>
<button class="btn" id="hide" onclick={hideApp}>Hide</button>
<button class="btn" id="toggle-dock-visibility" onclick={toggleDockVisibility}
>Toggle dock visibility</button
>
</div>

View File

@@ -0,0 +1,70 @@
<script lang="ts">
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
import { Channel, invoke } from '@tauri-apps/api/core'
import { onMount, onDestroy } from 'svelte'
import type { UnlistenFn } from '@tauri-apps/api/event'
import type { ViewProps } from '../App.svelte'
let { onMessage }: ViewProps = $props()
const webviewWindow = getCurrentWebviewWindow()
let unlisten: UnlistenFn | undefined
onMount(async () => {
unlisten = await webviewWindow.listen('rust-event', onMessage)
})
onDestroy(() => {
unlisten?.()
})
function log() {
invoke('log_operation', {
event: 'tauri-click',
payload: 'this payload is optional because we used Option in Rust'
})
}
function performRequest() {
invoke('perform_request', {
endpoint: 'dummy endpoint arg',
body: {
id: 5,
name: 'test'
}
})
.then(onMessage)
.catch(onMessage)
}
function echo() {
invoke('echo', {
message: 'Tauri JSON request!'
})
.then(onMessage)
.catch(onMessage)
invoke('echo', [1, 2, 3]).then(onMessage).catch(onMessage)
}
function spam() {
const channel = new Channel()
channel.onmessage = onMessage
invoke('spam', { channel })
}
function emitEvent() {
webviewWindow.emit('js-event', 'this is the payload string')
}
</script>
<div class="flex gap-2">
<button class="btn" id="log" onclick={log}>Call Log API</button>
<button class="btn" id="request" onclick={performRequest}>
Call Request (async) API
</button>
<button class="btn" id="event" onclick={emitEvent}>
Send event to Rust
</button>
<button class="btn" id="request" onclick={echo}>Echo</button>
<button class="btn" id="request" onclick={spam}>Spam</button>
</div>

View File

@@ -0,0 +1,126 @@
<script lang="ts">
import { Menu, Submenu, NativeIcon } from '@tauri-apps/api/menu'
import MenuBuilder, {
reorderMenuItems,
type Item,
type MenuItemClickDetail,
type MenuItems
} from '../components/MenuBuilder.svelte'
import { defaultWindowIcon } from '@tauri-apps/api/app'
import type { ViewProps } from '../App.svelte'
import { onDestroy } from 'svelte'
import type { Image } from '@tauri-apps/api/image'
let { onMessage }: ViewProps = $props()
let items = $state<Item[]>([])
let menu: Menu | undefined
let popupMenu: Menu | undefined
let submenu: Submenu | undefined
const macOS = navigator.userAgent.includes('Macintosh')
async function createSubmenu(): Promise<Submenu> {
return await Submenu.new({
text: 'app',
items: items.map((i) => i.menu).filter(Boolean) as MenuItems[]
})
}
async function createSubmenuWithNativeIcon(): Promise<Submenu> {
return await Submenu.new({
text: 'Submenu with NativeIcon',
icon: NativeIcon.Folder,
items: items.map((i) => i.menu).filter(Boolean) as MenuItems[]
})
}
async function createSubmenuWithImageIcon(): Promise<Submenu> {
let icon: Image | undefined
try {
icon = (await defaultWindowIcon())!
return await Submenu.new({
text: 'Submenu with Image',
icon,
items: items.map((i) => i.menu).filter(Boolean) as MenuItems[]
})
} finally {
icon?.close()
}
}
async function setMenu(newSubmenu: Submenu) {
menu?.close()
menu = undefined
submenu?.close()
submenu = newSubmenu
menu = await Menu.new({
items: [submenu]
})
await (macOS ? menu.setAsAppMenu() : menu.setAsWindowMenu())
}
async function create() {
await setMenu(await createSubmenu())
}
async function createWithNativeIcon() {
await setMenu(await createSubmenuWithNativeIcon())
}
async function createWithImageIcon() {
await setMenu(await createSubmenuWithImageIcon())
}
async function popup() {
popupMenu?.close()
popupMenu = undefined
popupMenu = await Menu.new({
items: items.map((i) => i.menu).filter(Boolean) as MenuItems[]
})
await popupMenu.popup()
}
function onItemClick(detail: MenuItemClickDetail) {
onMessage(`Item ${detail.text} clicked`)
}
onDestroy(() => {
menu?.close()
submenu?.close()
popupMenu?.close()
})
</script>
<div class="grid gap-8 mb-4">
<MenuBuilder
bind:items
itemClick={onItemClick}
onItemAdded={async (item) => {
if (item.menu) {
await submenu?.append(item.menu)
}
}}
onItemRemoved={async (item) => {
if (item.menu) {
await submenu?.remove(item.menu)
}
}}
onItemMoved={(item, toIndex) => {
if (submenu) {
reorderMenuItems(submenu, item, toIndex)
}
}}
/>
<div class="flex gap-2">
<button class="btn" onclick={create}>Create menu</button>
<button class="btn" onclick={createWithNativeIcon}
>Create menu with NativeIcon</button
>
<button class="btn" onclick={createWithImageIcon}
>Create menu with Image icon</button
>
<button class="btn" onclick={popup}>Popup</button>
</div>
</div>

View File

@@ -0,0 +1,134 @@
<script lang="ts">
import { TrayIcon } from '@tauri-apps/api/tray'
import MenuBuilder, {
reorderMenuItems,
type Item,
type MenuItemClickDetail,
type MenuItems
} from '../components/MenuBuilder.svelte'
import { Menu } from '@tauri-apps/api/menu'
import type { ViewProps } from '../App.svelte'
import { onDestroy } from 'svelte'
let { onMessage }: ViewProps = $props()
let icon = $state<string>('../../.icons/tray_icon.png')
let tooltip = $state<string>()
let title = $state<string>()
let iconAsTemplate = $state(false)
let menuOnLeftClick = $state(true)
let menuItems = $state<Item[]>([])
let menu: Menu | undefined
let tray = $state<TrayIcon | undefined>()
function onItemClick(detail: MenuItemClickDetail) {
onMessage(`Item ${detail.text} clicked`)
}
async function create() {
try {
menu = await Menu.new({
items: menuItems.map((i) => i.menu).filter(Boolean) as MenuItems[]
})
tray = await TrayIcon.new({
icon,
tooltip,
title,
iconAsTemplate,
menuOnLeftClick,
menu,
action: (event) => onMessage(event)
})
} catch (error) {
menu?.close()
menu = undefined
tray?.close()
tray = undefined
onMessage(error)
}
}
onDestroy(() => {
menu?.close()
tray?.close()
})
</script>
<div class="grid gap-8 mb-4">
<MenuBuilder
bind:items={menuItems}
itemClick={onItemClick}
onItemAdded={async (item) => {
if (item.menu) {
await menu?.append(item.menu)
}
}}
onItemRemoved={async (item) => {
if (item.menu) {
await menu?.remove(item.menu)
}
}}
onItemMoved={(item, toIndex) => {
if (menu) {
reorderMenuItems(menu, item, toIndex)
}
}}
/>
<div class="flex items-center gap-8">
<div class="grid gap-2 grid-cols-3 items-center">
<input
class="input grow"
type="text"
placeholder="Title"
bind:value={title}
/>
<input
class="input grow"
type="text"
placeholder="Tooltip"
bind:value={tooltip}
/>
<label class="flex gap-2">
<input
type="checkbox"
class="checkbox"
bind:checked={menuOnLeftClick}
/>
Menu on left click
</label>
<input
class="input col-span-2"
type="text"
placeholder="Icon path"
bind:value={icon}
/>
<label class="flex gap-2">
<input type="checkbox" class="checkbox" bind:checked={iconAsTemplate} />
Icon as template
</label>
</div>
<div class="flex">
{#if tray}
<button
class="btn"
onclick={() => {
tray?.close()
tray = undefined
}}
title="Remove the tray icon">Remove tray</button
>
{:else}
<button class="btn" onclick={create} title="Creates the tray icon"
>Create tray</button
>
{/if}
</div>
</div>
</div>

View File

@@ -0,0 +1,72 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte'
import type { ViewProps } from '../App.svelte'
let { onMessage }: ViewProps = $props()
let video: HTMLVideoElement
let mediaStream: MediaStream | undefined
const constraints: MediaStreamConstraints = {
audio: true,
video: true
}
function handleSuccess(stream: MediaStream) {
const settings = stream.getTracks().map((track) => track.getSettings())
onMessage(`Got streams: ${JSON.stringify(settings, null, 2)}`)
const videoTracks = stream.getVideoTracks()
onMessage(`Using video device: ${videoTracks[0]?.label ?? 'Unknown'}`)
// @ts-expect-error
window.stream = mediaStream // make variable available to browser console
if (video) {
video.srcObject = stream
}
}
function handleError(error: unknown) {
if (!(error instanceof DOMException)) {
onMessage(`getUserMedia error: ${error}`)
return
}
if (error.name === 'ConstraintNotSatisfiedError') {
// const v = constraints.video
// onMessage(
// `The resolution ${v.width.exact}x${v.height.exact} px is not supported by your device.`
// )
onMessage(
`The constraints ${constraints} can not be satisified by your device.`
)
} else if (error.name === 'PermissionDeniedError') {
onMessage(
'Permissions have not been granted to use your camera and '
+ 'microphone, you need to allow the page access to your devices in '
+ 'order for the demo to work.'
)
}
onMessage(`getUserMedia error: ${error}`)
}
onMount(async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia(constraints)
handleSuccess(stream)
} catch (e) {
handleError(e)
}
})
onDestroy(() => {
for (const track of mediaStream?.getTracks() ?? []) {
track.stop()
}
})
</script>
<div class="flex flex-col gap-2">
<div class="note-red grow">Not available for Linux</div>
<video id="localVideo" autoplay playsinline bind:this={video}>
<track kind="captions" />
</video>
</div>

View File

@@ -0,0 +1,62 @@
<script lang="ts">
import { invoke } from '@tauri-apps/api/core'
import {
getName,
getVersion,
getTauriVersion,
getBundleType,
exit
} from '@tauri-apps/api/app'
import type { ViewProps } from '../App.svelte'
let { onMessage }: ViewProps = $props()
let version = $state('1.0.0')
let tauriVersion = $state('1.0.0')
let appName = $state('Unknown')
let bundleType = $state('Unknown')
getName().then((n) => {
appName = n
})
getVersion().then((v) => {
version = v
})
getTauriVersion().then((v) => {
tauriVersion = v
})
getBundleType().then((b) => {
if (b) {
bundleType = b
}
})
function contextMenu() {
invoke('plugin:app-menu|popup')
}
function exitApp() {
exit().catch(onMessage)
}
</script>
<div class="grid gap-8 justify-items-start">
<p>
This is a demo of Tauri's API capabilities using the <code
>@tauri-apps/api</code
> package. It's used as the main validation app, serving as the test bed of our
development process. In the future, this app will be used on Tauri's integration
tests.
</p>
<pre>
App name: <code>{appName}</code>
App version: <code>{version}</code>
Tauri version: <code>{tauriVersion}</code>
Bundle type: <code>{bundleType}</code>
</pre>
<div class="flex gap-2">
<button class="btn" onclick={contextMenu}>Context menu</button>
<button class="btn" id="exit" onclick={exitApp}>Exit</button>
</div>
</div>

View File

@@ -0,0 +1,996 @@
<script lang="ts">
import { onDestroy, onMount } from 'svelte'
import {
LogicalSize,
UserAttentionType,
PhysicalSize,
PhysicalPosition,
Effect,
EffectState,
ProgressBarStatus,
availableMonitors
} from '@tauri-apps/api/window'
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
import type {
CursorIcon,
Effects,
Monitor,
Theme
} from '@tauri-apps/api/window'
import type { UnlistenFn } from '@tauri-apps/api/event'
import type { ViewProps } from '../App.svelte'
let { onMessage }: ViewProps = $props()
const webview = WebviewWindow.getCurrent()
const webviewMap = $state({
[webview.label]: webview
})
let selectedWebviewLabel = $state(webview.label)
let selectedWebview = $derived(webviewMap[selectedWebviewLabel])
let monitorMap = $state<Record<string, Monitor>>({})
let selectedMonitor = $state('')
availableMonitors().then((monitors) => {
monitorMap = Object.fromEntries(
monitors.map((m, i) => [m.name ?? `Monitor ${i + 1}`, m])
)
})
let focusable = $state(true)
const cursorIconOptions: CursorIcon[] = [
'default',
'crosshair',
'hand',
'arrow',
'move',
'text',
'wait',
'help',
'progress',
// something cannot be done
'notAllowed',
'contextMenu',
'cell',
'verticalText',
'alias',
'copy',
'noDrop',
// something can be grabbed
'grab',
/// something is grabbed
'grabbing',
'allScroll',
'zoomIn',
'zoomOut',
// edge is to be moved
'eResize',
'nResize',
'neResize',
'nwResize',
'sResize',
'seResize',
'swResize',
'wResize',
'ewResize',
'nsResize',
'neswResize',
'nwseResize',
'colResize',
'rowResize'
]
const windowsEffects: Effect[] = [
Effect.Mica,
Effect.Blur,
Effect.Acrylic,
Effect.Tabbed,
Effect.TabbedDark,
Effect.TabbedLight
]
const isWindows = navigator.appVersion.includes('Windows')
const isMacOS = navigator.appVersion.includes('Macintosh')
const effectOptions: Effect[] = isWindows
? windowsEffects
: (Object.values(Effect) as Effect[]).filter(
(e) => !windowsEffects.includes(e)
)
const effectStateOptions = Object.values(EffectState) as EffectState[]
const progressBarStatusOptions = Object.values(ProgressBarStatus)
let newWebviewLabel = $state<string>()
let resizable = $state(true)
let maximizable = $state(true)
let minimizable = $state(true)
let closable = $state(true)
let maximized = $state(false)
let decorations = $state(true)
let alwaysOnTop = $state(false)
let alwaysOnBottom = $state(false)
let contentProtected = $state(false)
let fullscreen = $state(false)
let simpleFullscreen = $state(false)
let width = $state(100)
let height = $state(100)
let minWidth = $state<number | null>(null)
let minHeight = $state<number | null>(null)
let maxWidth = $state<number | null>(null)
let maxHeight = $state<number | null>(null)
let x = $state(0)
let y = $state(0)
let scaleFactor = $state(1)
let innerPosition = $state(new PhysicalPosition(0, 0))
let outerPosition = $state(new PhysicalPosition(0, 0))
let innerSize = $state(new PhysicalSize(0, 0))
let outerSize = $state(new PhysicalSize(0, 0))
// name of the monitor containing the selected window's top-left corner
let monitor = $derived(
Object.entries(monitorMap).find(
([, m]) =>
outerPosition.x >= m.position.x
&& outerPosition.x < m.position.x + m.size.width
&& outerPosition.y >= m.position.y
&& outerPosition.y < m.position.y + m.size.height
)?.[0] ?? ''
)
let resizeEventUnlisten: UnlistenFn | undefined
let moveEventUnlisten: UnlistenFn | undefined
let cursorGrab = $state(false)
let cursorVisible = $state(true)
let cursorX = $state<number | null>(null)
let cursorY = $state<number | null>(null)
let cursorIcon = $state<CursorIcon>('default')
let cursorIgnoreEvents = $state(false)
let windowTitle = $state('Awesome Tauri Example!')
let theme = $state<Theme | 'auto'>('auto')
let effects = $state<Effect[]>([])
let selectedEffect = $state<Effect>()
let effectState = $state<EffectState>()
let effectRadius = $state<number>()
let effectInteractive = $state(false)
let effectR = $state<number>(),
effectG = $state<number>(),
effectB = $state<number>(),
effectA = $state<number>()
let selectedProgressBarStatus = $state<ProgressBarStatus>(
ProgressBarStatus.None
)
let progress = $state(0)
let windowIconPath = $state('')
function setTitle() {
selectedWebview.setTitle(windowTitle)
}
async function hide() {
let visible = await selectedWebview.isVisible()
onMessage('window is ' + (visible ? 'visible' : 'invisible'))
await selectedWebview.hide()
setTimeout(async () => {
visible = await selectedWebview.isVisible()
onMessage('window is ' + (visible ? 'visible' : 'invisible'))
await selectedWebview.show()
visible = await selectedWebview.isVisible()
onMessage('window is ' + (visible ? 'visible' : 'invisible'))
}, 2000)
}
async function disable() {
let enabled = await selectedWebview.isEnabled()
onMessage('window is ' + (enabled ? 'enabled' : 'disabled'))
await selectedWebview.setEnabled(false)
setTimeout(async () => {
enabled = await selectedWebview.isEnabled()
onMessage('window is ' + (enabled ? 'enabled' : 'disabled'))
await selectedWebview.setEnabled(true)
enabled = await selectedWebview.isEnabled()
onMessage('window is ' + (enabled ? 'enabled' : 'disabled'))
}, 2000)
}
function minimize() {
selectedWebview.minimize()
setTimeout(selectedWebview.unminimize, 2000)
}
function changeIcon() {
selectedWebview.setIcon(windowIconPath)
}
function createWebviewWindow() {
if (!newWebviewLabel) return
const label = `main-${newWebviewLabel}`
const webview = new WebviewWindow(label)
webviewMap[label] = webview
webview.once('tauri://error', function (e) {
onMessage('Error creating new webview ' + JSON.stringify(e))
})
webview.once('tauri://created', function () {
onMessage('webview created')
})
}
function loadWindowSize() {
selectedWebview.innerSize().then((response) => {
innerSize = response
width = innerSize.width
height = innerSize.height
})
selectedWebview.outerSize().then((response) => {
outerSize = response
})
}
function loadWindowPosition() {
selectedWebview.innerPosition().then((response) => {
innerPosition = response
})
selectedWebview.outerPosition().then((response) => {
outerPosition = response
x = outerPosition.x
y = outerPosition.y
})
}
async function addWindowEventListeners(window: WebviewWindow | undefined) {
if (!window) return
resizeEventUnlisten?.()
moveEventUnlisten?.()
moveEventUnlisten = await window.listen('tauri://move', loadWindowPosition)
resizeEventUnlisten = await window.listen('tauri://resize', loadWindowSize)
}
async function requestUserAttention() {
await selectedWebview.minimize()
await selectedWebview.requestUserAttention(UserAttentionType.Critical)
await new Promise((resolve) => setTimeout(resolve, 3000))
await selectedWebview.requestUserAttention(null)
}
async function switchTheme() {
switch (theme) {
case 'dark':
theme = 'light'
break
case 'light':
theme = 'auto'
break
case 'auto':
theme = 'dark'
break
}
await selectedWebview.setTheme(theme === 'auto' ? null : theme)
}
async function updateProgressBar() {
selectedWebview.setProgressBar({
status: selectedProgressBarStatus,
progress
})
}
async function addEffect() {
if (selectedEffect && !effects.includes(selectedEffect)) {
effects = [...effects, selectedEffect]
}
const payload: Effects = {
effects,
state: effectState,
radius: effectRadius,
interactive: effectInteractive
}
if (
Number.isInteger(effectR)
&& Number.isInteger(effectG)
&& Number.isInteger(effectB)
&& Number.isInteger(effectA)
) {
payload.color = [effectR!, effectG!, effectB!, effectA!]
}
// see `:root.window-effects` in app.css
document.documentElement.classList.add('window-effects')
await selectedWebview.clearEffects()
await selectedWebview.setEffects(payload)
}
async function clearEffects() {
effects = []
await selectedWebview.clearEffects()
document.documentElement.classList.remove('window-effects')
}
async function updatePosition() {
selectedWebview.setPosition(new PhysicalPosition(x, y))
}
async function updateSize() {
if (width && height) {
selectedWebview.setSize(new PhysicalSize(width, height))
}
}
function refreshSelectedWebview() {
loadWindowPosition()
loadWindowSize()
selectedWebview.scaleFactor().then((factor) => (scaleFactor = factor))
addWindowEventListeners(selectedWebview)
}
function updateResizable() {
selectedWebview.setResizable(resizable)
}
function updateMaximizable() {
selectedWebview.setMaximizable(maximizable)
}
function updateMinimizable() {
selectedWebview.setMinimizable(minimizable)
}
function updateClosable() {
selectedWebview.setClosable(closable)
}
function updateMaximized() {
maximized ? selectedWebview.maximize() : selectedWebview.unmaximize()
}
function updateDecorations() {
selectedWebview.setDecorations(decorations)
}
function updateAlwaysOnTop() {
selectedWebview.setAlwaysOnTop(alwaysOnTop)
}
function updateAlwaysOnBottom() {
selectedWebview.setAlwaysOnBottom(alwaysOnBottom)
}
function updateContentProtected() {
selectedWebview.setContentProtected(contentProtected)
}
function updateFullscreen() {
selectedWebview.setFullscreen(fullscreen)
}
function updateSimpleFullscreen() {
selectedWebview.setSimpleFullscreen(simpleFullscreen)
}
async function setFullscreenOnMonitor() {
const target = monitorMap[selectedMonitor]
if (!target) return
await selectedWebview.setFullscreenOnMonitor(target.position)
fullscreen = true
}
function updateMinSize() {
minWidth && minHeight
? selectedWebview.setMinSize(new LogicalSize(minWidth, minHeight))
: selectedWebview.setMinSize(null)
}
function updateMaxSize() {
maxWidth && maxHeight && maxWidth > 800 && maxHeight > 400
? selectedWebview.setMaxSize(new LogicalSize(maxWidth, maxHeight))
: selectedWebview.setMaxSize(null)
}
function updateCursorGrab() {
selectedWebview.setCursorGrab(cursorGrab)
}
function updateCursorVisible() {
selectedWebview.setCursorVisible(cursorVisible)
}
function updateCursorIgnoreEvents() {
selectedWebview.setIgnoreCursorEvents(cursorIgnoreEvents)
}
function updateCursorIcon() {
selectedWebview.setCursorIcon(cursorIcon)
}
function updateCursorPosition() {
cursorX !== null
&& cursorY !== null
&& selectedWebview.setCursorPosition(
new PhysicalPosition(cursorX, cursorY)
)
}
$effect(() => {
selectedWebview
refreshSelectedWebview()
})
onDestroy(() => {
resizeEventUnlisten?.()
moveEventUnlisten?.()
})
</script>
<div class="flex flex-col *:grow gap-8 mb-4">
<div
class="flex flex-wrap items-center gap-4 pb-6 border-b-solid border-b-1 border-code"
>
{#if Object.keys(webviewMap).length >= 1}
<div class="grid gap-1">
<h4 class="my-2">Selected Window</h4>
<select
class="input"
bind:value={selectedWebviewLabel}
onchange={refreshSelectedWebview}
>
<option value="" disabled selected>Choose a window...</option>
{#each Object.keys(webviewMap) as label}
<option value={label}>{label}</option>
{/each}
</select>
</div>
{/if}
<div class="grid gap-1">
<h4 class="my-2">Create New Window</h4>
<form
class="flex gap-2"
onsubmit={(ev) => {
createWebviewWindow()
ev.preventDefault()
}}
>
<input
class="input"
type="text"
placeholder="New window label.."
bind:value={newWebviewLabel}
/>
<button class="btn" type="submit">Create</button>
</form>
</div>
</div>
{#if selectedWebview}
<div class="flex flex-wrap items-center gap-4">
<div class="grid gap-1 grow">
<h4 class="my-2">Change Window Icon</h4>
<form
class="flex gap-2"
onsubmit={(ev) => {
changeIcon()
ev.preventDefault()
}}
>
<input
class="input flex-1 min-w-10"
placeholder="Window icon path"
bind:value={windowIconPath}
/>
<button class="btn" type="submit">Change</button>
</form>
</div>
<div class="grid gap-1 grow">
<h4 class="my-2">Set Window Title</h4>
<form
class="flex gap-2"
onsubmit={(ev) => {
setTitle()
ev.preventDefault()
}}
>
<input class="input flex-1 min-w-10" bind:value={windowTitle} />
<button class="btn" type="submit">Set</button>
</form>
</div>
</div>
<div class="flex flex-wrap gap-2">
<button
class="btn"
title="Unminimizes after 2 seconds"
onclick={() => selectedWebview.center()}
>
Center
</button>
<button
class="btn"
title="Unminimizes after 2 seconds"
onclick={minimize}
>
Minimize
</button>
<button class="btn" title="Visible again after 2 seconds" onclick={hide}>
Hide
</button>
<button
class="btn"
title="Enabled again after 2 seconds"
onclick={disable}
>
Disable
</button>
<button
class="btn"
onclick={requestUserAttention}
title="Minimizes the window, requests attention for 3s and then resets it"
>Request attention</button
>
<button class="btn" onclick={switchTheme}>Switch Theme ({theme})</button>
<button
class="btn"
onclick={() => {
focusable = !focusable
selectedWebview.setFocusable(!focusable)
}}
>
Set focusable to {!focusable}
</button>
<form
class="flex gap-2"
onsubmit={(ev) => {
setFullscreenOnMonitor()
ev.preventDefault()
}}
>
<button class="btn" type="submit" disabled={!selectedMonitor}>
Set Fullscreen on Monitor
</button>
<select class="input" bind:value={selectedMonitor}>
<option value="" disabled>Choose a monitor...</option>
{#each Object.keys(monitorMap) as label}
<option value={label}>{label}</option>
{/each}
</select>
</form>
</div>
<div class="grid cols-[repeat(auto-fill,minmax(180px,1fr))] *:flex *:gap-2">
<label>
<input
type="checkbox"
class="checkbox"
bind:checked={resizable}
onchange={updateResizable}
/>
Resizable
</label>
<label>
<input
type="checkbox"
class="checkbox"
bind:checked={maximizable}
onchange={updateMaximizable}
/>
Maximizable
</label>
<label>
<input
type="checkbox"
class="checkbox"
bind:checked={minimizable}
onchange={updateMinimizable}
/>
Minimizable
</label>
<label>
<input
type="checkbox"
class="checkbox"
bind:checked={closable}
onchange={updateClosable}
/>
Closable
</label>
<label>
<input
type="checkbox"
class="checkbox"
bind:checked={decorations}
onchange={updateDecorations}
/>
Has decorations
</label>
<label>
<input
type="checkbox"
class="checkbox"
bind:checked={alwaysOnTop}
onchange={updateAlwaysOnTop}
/>
Always on top
</label>
<label>
<input
type="checkbox"
class="checkbox"
bind:checked={alwaysOnBottom}
onchange={updateAlwaysOnBottom}
/>
Always on bottom
</label>
<label>
<input
type="checkbox"
class="checkbox"
bind:checked={contentProtected}
onchange={updateContentProtected}
/>
Content protected
</label>
<label>
<input
type="checkbox"
class="checkbox"
bind:checked={maximized}
onchange={updateMaximized}
/>
Maximized
</label>
<label>
<input
type="checkbox"
class="checkbox"
bind:checked={fullscreen}
onchange={updateFullscreen}
/>
Fullscreen
</label>
<label>
<input
type="checkbox"
class="checkbox"
bind:checked={simpleFullscreen}
onchange={updateSimpleFullscreen}
/>
Simple fullscreen
</label>
</div>
<div class="flex flex-wrap *:flex-basis-30 gap-2">
<div class="grid gap-1 *:grid">
<label>
X
<input
class="input"
type="number"
bind:value={x}
onchange={updatePosition}
min="0"
/>
</label>
<label>
Y
<input
class="input"
type="number"
bind:value={y}
onchange={updatePosition}
min="0"
/>
</label>
</div>
<div class="grid gap-1 *:grid">
<label>
Width
<input
class="input"
type="number"
bind:value={width}
onchange={updateSize}
min="400"
/>
</label>
<div>
Height
<input
class="input"
type="number"
bind:value={height}
onchange={updateSize}
min="400"
/>
</div>
</div>
<div class="grid gap-1 *:grid">
<label>
Min width
<input
class="input"
type="number"
bind:value={minWidth}
onchange={updateMinSize}
/>
</label>
<label>
Min height
<input
class="input"
type="number"
bind:value={minHeight}
onchange={updateMinSize}
/>
</label>
</div>
<div class="grid gap-1 *:grid">
<label>
Max width
<input
class="input"
type="number"
bind:value={maxWidth}
onchange={updateMaxSize}
min="800"
/>
</label>
<label>
Max height
<input
class="input"
type="number"
bind:value={maxHeight}
onchange={updateMaxSize}
min="400"
/>
</label>
</div>
</div>
<div class="grid grid-cols-2 gap-2 max-inline-2xl">
<div>
<div class="text-accent dark:text-darkAccent font-700 m-block-1">
Inner Size
</div>
<span>Width: {innerSize.width}</span>
<span>Height: {innerSize.height}</span>
</div>
<div>
<div class="text-accent dark:text-darkAccent font-700 m-block-1">
Outer Size
</div>
<span>Width: {outerSize.width}</span>
<span>Height: {outerSize.height}</span>
</div>
<div>
<div class="text-accent dark:text-darkAccent font-700 m-block-1">
Inner Logical Size
</div>
<span>Width: {innerSize.toLogical(scaleFactor).width.toFixed(3)}</span>
<span>Height: {innerSize.toLogical(scaleFactor).height.toFixed(3)}</span
>
</div>
<div>
<div class="text-accent dark:text-darkAccent font-700 m-block-1">
Outer Logical Size
</div>
<span>Width: {outerSize.toLogical(scaleFactor).width.toFixed(3)}</span>
<span>Height: {outerSize.toLogical(scaleFactor).height.toFixed(3)}</span
>
</div>
<div>
<div class="text-accent dark:text-darkAccent font-700 m-block-1">
Inner Position
</div>
<span>x: {innerPosition.x}</span>
<span>y: {innerPosition.y}</span>
</div>
<div>
<div class="text-accent dark:text-darkAccent font-700 m-block-1">
Outer Position
</div>
<span>x: {outerPosition.x}</span>
<span>y: {outerPosition.y}</span>
</div>
<div>
<div class="text-accent dark:text-darkAccent font-700 m-block-1">
Inner Logical Position
</div>
<span>x: {innerPosition.toLogical(scaleFactor).x.toFixed(3)}</span>
<span>y: {innerPosition.toLogical(scaleFactor).y.toFixed(3)}</span>
</div>
<div>
<div class="text-accent dark:text-darkAccent font-700 m-block-1">
Outer Logical Position
</div>
<span>x: {outerPosition.toLogical(scaleFactor).x.toFixed(3)}</span>
<span>y: {outerPosition.toLogical(scaleFactor).y.toFixed(3)}</span>
</div>
<div>
<div class="text-accent dark:text-darkAccent font-700 m-block-1">
Current Monitor
</div>
<span>{monitor}</span>
</div>
</div>
<div class="grid gap-2">
<h4 class="my-2">Cursor</h4>
<div class="flex gap-4 *:flex *:gap-2">
<label>
<input
type="checkbox"
class="checkbox"
bind:checked={cursorGrab}
onchange={updateCursorGrab}
/>
Grab
</label>
<label>
<input
type="checkbox"
class="checkbox"
bind:checked={cursorVisible}
onchange={updateCursorVisible}
/>
Visible
</label>
<label>
<input
type="checkbox"
class="checkbox"
bind:checked={cursorIgnoreEvents}
onchange={updateCursorIgnoreEvents}
/>
Ignore events
</label>
</div>
<div class="flex gap-2">
<label>
Icon
<select
class="input"
bind:value={cursorIcon}
onchange={updateCursorIcon}
>
{#each cursorIconOptions as kind}
<option value={kind}>{kind}</option>
{/each}
</select>
</label>
<label>
X position
<input
class="input"
type="number"
bind:value={cursorX}
onchange={updateCursorPosition}
/>
</label>
<label>
Y position
<input
class="input"
type="number"
bind:value={cursorY}
onchange={updateCursorPosition}
/>
</label>
</div>
</div>
<div class="flex flex-col gap-1">
<div class="flex gap-2">
<label>
Progress Status
<select
class="input"
bind:value={selectedProgressBarStatus}
onchange={updateProgressBar}
>
{#each progressBarStatusOptions as status}
<option value={status}>{status}</option>
{/each}
</select>
</label>
<label>
Progress
<input
class="input"
type="number"
min="0"
max="100"
bind:value={progress}
onchange={updateProgressBar}
/>
</label>
</div>
</div>
{#if isWindows || isMacOS}
<div class="flex flex-col gap-2">
<div class="flex items-center gap-2">
<div>
Applied effects: {effects.length ? effects.join(', ') : 'None'}
</div>
<button class="btn" onclick={clearEffects}>Clear</button>
</div>
<div class="flex gap-2">
<label>
Effect
<select class="input" bind:value={selectedEffect}>
{#each effectOptions as effect}
<option value={effect}>{effect}</option>
{/each}
</select>
</label>
<label>
State
<select class="input" bind:value={effectState}>
{#each effectStateOptions as state}
<option value={state}>{state}</option>
{/each}
</select>
</label>
<label>
Radius
<input class="input" type="number" bind:value={effectRadius} />
</label>
<label class="flex items-center gap-1">
<input
type="checkbox"
class="checkbox"
bind:checked={effectInteractive}
/>
Interactive
</label>
</div>
<div class="flex">
<label>
Color
<div class="flex gap-2 *:flex-basis-30">
<input
class="input"
type="number"
placeholder="R"
bind:value={effectR}
/>
<input
class="input"
type="number"
placeholder="G"
bind:value={effectG}
/>
<input
class="input"
type="number"
placeholder="B"
bind:value={effectB}
/>
<input
class="input"
type="number"
placeholder="A"
bind:value={effectA}
/>
</div>
</label>
</div>
<div class="flex">
<button class="btn" onclick={addEffect}>Add</button>
</div>
</div>
{/if}
{/if}
</div>

6
examples/api/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
/// <reference types="svelte" />
/// <reference types="vite/client" />

View File

@@ -0,0 +1,7 @@
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
export default {
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
// for more information about preprocessors
preprocess: vitePreprocess()
}

View File

@@ -0,0 +1,15 @@
{
"compilerOptions": {
"moduleResolution": "bundler",
"target": "ESNext",
"module": "ESNext",
"verbatimModuleSyntax": true,
"isolatedModules": true,
"resolveJsonModule": true,
"sourceMap": true,
"esModuleInterop": true,
"skipLibCheck": true,
"strict": true
},
"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.svelte"]
}

View File

@@ -0,0 +1,101 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
import { defineConfig, presetIcons, presetWind3, presetWebFonts } from 'unocss'
import extractorSvelte from '@unocss/extractor-svelte'
export default defineConfig({
theme: {
colors: {
primary: '#FFFFFF',
primaryLighter: '#e9ecef',
darkPrimary: '#1B1B1D',
darkPrimaryLighter: '#242526',
primaryText: '#1C1E21',
darkPrimaryText: '#E3E3E3',
secondaryText: '#858A91',
darkSecondaryText: '#C2C5CA',
accent: '#3578E5',
accentDark: '#306cce',
accentDarker: '#2d66c3',
accentDarkest: '#2554a0',
accentLight: '#538ce9',
accentLighter: '#72a1ed',
accentLightest: '#9abcf2',
accentText: '#FFFFFF',
darkAccent: '#67d6ed',
darkAccentDark: '#49cee9',
darkAccentDarker: '#39cae8',
darkAccentDarkest: '#19b5d5',
darkAccentLight: '#85def1',
darkAccentLighter: '#95e2f2',
darkAccentLightest: '#c2eff8',
darkAccentText: '#1C1E21',
code: '#d6d8da',
codeDark: '#282a2e',
hoverOverlay: 'rgba(0,0,0,.05)',
hoverOverlayDarker: 'rgba(0,0,0,.1)',
darkHoverOverlay: 'hsla(0,0%,100%,.05)',
darkHoverOverlayDarker: 'hsla(0,0%,100%,.1)'
}
},
preflights: [
{
getCSS: ({ theme }) => `
::-webkit-scrollbar-thumb {
background-color: ${theme.colors.accent};
}
.dark ::-webkit-scrollbar-thumb {
background-color: ${theme.colors.darkAccent};
}
code {
font-size: ${theme.fontSize.xs[0]};
font-family: ${theme.fontFamily.mono};
border-radius: ${theme.borderRadius['DEFAULT']};
background-color: ${theme.colors.code};
}
.code-block {
font-family: ${theme.fontFamily.mono};
font-size: ${theme.fontSize.sm[0]};
}
.dark code {
background-color: ${theme.colors.codeDark};
}
`
}
],
shortcuts: {
btn: `select-none outline-none shadow-md p-2 rd-1 text-primaryText border-none font-400 dark:font-600
bg-accent hover:bg-accentDarker active:bg-accentDarkest text-accentText
dark:bg-darkAccent dark:hover:bg-darkAccentDarker dark:active:bg-darkAccentDarkest dark:text-darkAccentText`,
nv: `decoration-none flex items-center relative p-2 rd-1 transition-all-125 ease
text-darkSecondaryText
hover:text-accent dark:hover:text-darkAccent
hover:bg-darkHoverOverlay hover:border-l-4`,
nv_selected: `nv bg-darkHoverOverlay text-accent dark:text-darkAccent border-l-4`,
note: `decoration-none flex-inline items-center relative p-2 rd-1
border-l-4 border-accent dark:border-darkAccent
bg-accent/10 dark:bg-darkAccent/10`,
'note-red':
'note bg-red-700/10 dark:bg-red-700/10 after:bg-red-700 dark:after:bg-red-700',
input:
'flex items-center outline-none border-none py-3 px-2 rd-1 shadow-md bg-primaryLighter dark:bg-darkPrimaryLighter text-primaryText dark:text-darkPrimaryText',
checkbox: 'accent-accent'
},
presets: [
presetWind3(),
presetIcons(),
presetWebFonts({
fonts: {
sans: 'Rubik',
mono: ['Fira Code', 'Fira Mono:400,700']
}
})
],
extractors: [extractorSvelte]
})

View File

@@ -0,0 +1,44 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
import { defineConfig } from 'vite'
import Unocss from 'unocss/vite'
import { svelte } from '@sveltejs/vite-plugin-svelte'
const host = process.env.TAURI_DEV_HOST
// https://vite.dev/config/
export default defineConfig({
plugins: [Unocss(), svelte()],
build: {
emptyOutDir: false,
rollupOptions: {
output: {
entryFileNames: `assets/[name].js`,
chunkFileNames: `assets/[name].js`,
assetFileNames: `assets/[name].[ext]`
}
}
},
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
// prevent Vite from obscuring rust errors
clearScreen: false,
// tauri expects a fixed port, fail if that port is not available
server: {
host: host ?? false,
port: 1420,
strictPort: true,
hmr: host
? {
protocol: 'ws',
host: host,
port: 1430
}
: undefined,
fs: {
allow: ['.', '../../packages/api/dist']
}
}
})

View File

@@ -0,0 +1,5 @@
# Commands Example
A simple Tauri Application showcasing the command API.
To execute run the following on the root directory of the repository: `cargo run --example commands`.

View File

@@ -0,0 +1,32 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use tauri::{State, command};
#[command]
pub fn cmd(_argument: String) {}
#[command]
pub fn invoke(_argument: String) {}
#[command]
pub fn message(_argument: String) {}
#[command]
pub fn resolver(_argument: String) {}
#[command]
pub fn simple_command(the_argument: String) {
println!("{the_argument}");
}
#[command]
pub fn stateful_command(the_argument: Option<String>, state: State<'_, super::MyState>) {
println!("{:?} {:?}", the_argument, *state);
}
#[command(rename = "renamed_command_in_mod_new")]
pub fn renamed_command_in_mod() {
println!("renamed command in mod called");
}

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