diff --git a/.cargo/audit.toml b/.cargo/audit.toml index b6dd47060380..0e271c7fbbca 100644 --- a/.cargo/audit.toml +++ b/.cargo/audit.toml @@ -8,4 +8,17 @@ ignore = [ "RUSTSEC-2024-0370", # time crate can't be updated in the repo because of MSRV, users are unaffected "RUSTSEC-2026-0009", + # rand unsoundness, fixed when we remove kuchikiki from deps in v3, currently + # remains for semver reasons but is not built in default configuration + "RUSTSEC-2026-0097", + # glib 0.18.5 unsoundness, fixed by updating to gtk4 + "RUSTSEC-2024-0429", + # rustls, fixed when updating to apple-codesign 0.28.0 + "RUSTSEC-2026-0049", + # rustls, fixed when updating to apple-codesign 0.28.0 + "RUSTSEC-2026-0098", + # rustls, fixed when updating to apple-codesign 0.28.0 + "RUSTSEC-2026-0099", + # rustls, fixed when updating to apple-codesign 0.28.0 + "RUSTSEC-2026-0104", ] diff --git a/.changes/acl-command-permissions.md b/.changes/acl-command-permissions.md new file mode 100644 index 000000000000..cc411da16120 --- /dev/null +++ b/.changes/acl-command-permissions.md @@ -0,0 +1,12 @@ +--- +tauri: patch:perf +tauri-utils: patch:perf +tauri-build: patch:perf +tauri-plugin: patch:perf +tauri-macros: patch:perf +tauri-codegen: patch:perf +--- + +Reduce the size of the resolved ACL embedded in the app by storing the autogenerated command permissions as a `commands` list on the plugin/app manifest instead of two explicit permissions (`allow-$command` and `deny-$command`) per command. The `allow-$command`/`deny-$command` permissions are now materialized on demand when resolving the ACL. + +The application manifest also gains implicit `allow-*` and `deny-*` permissions that allow or deny **all** of its commands through a single resolved entry, so capabilities no longer need to list every command individually. diff --git a/.changes/acl-identifier-error-context.md b/.changes/acl-identifier-error-context.md new file mode 100644 index 000000000000..0918e9266fcd --- /dev/null +++ b/.changes/acl-identifier-error-context.md @@ -0,0 +1,9 @@ +--- +"tauri-utils": "patch:enhance" +--- + +Improve diagnostics for invalid plugin and permission identifiers. + +The `Identifier` deserializer now wraps the inner error with the offending identifier string so the message reads `invalid plugin or permission identifier '': ...`, surfacing the bad entry without requiring a grep through the file. + +The previous parse failure (`failed to parse JSON: identifiers can only include lowercase ASCII, hyphens which are not leading or trailing, and a single colon if using a prefix at line 16 column 23`) now reads `failed to parse JSON: invalid plugin or permission identifier 'sqlite_proxy:allow-foo': identifiers can only include lowercase ASCII, hyphens which are not leading or trailing, and a single colon if using a prefix at line 16 column 23`. diff --git a/.changes/acl-number-int-preserved-on-serdejson-roundtrip.md b/.changes/acl-number-int-preserved-on-serdejson-roundtrip.md new file mode 100644 index 000000000000..3247edeab5c2 --- /dev/null +++ b/.changes/acl-number-int-preserved-on-serdejson-roundtrip.md @@ -0,0 +1,7 @@ +--- +"tauri-utils": "patch:bug" +--- + +Fix `Number::Int` being silently coerced to `Number::Float` on `serde_json` round-trip. + +`From for Value` was checking `as_f64()` first, which succeeds for every integer that fits in an f64, so integer JSON numbers were always deserialized as `Number::Float`. The check order is now `as_i64()` → `as_u64()` (cast to `i64`, wrapping for values above `i64::MAX`) → `as_f64()`, matching serde_json's own visitor convention. diff --git a/.changes/base64.md b/.changes/base64.md deleted file mode 100644 index 2fb599baba49..000000000000 --- a/.changes/base64.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"tauri-macos-sign": patch:enhance ---- - -Do not rely on system base64 CLI to decode certificates. diff --git a/.changes/data-tauri-drag-region-deep.md b/.changes/data-tauri-drag-region-deep.md deleted file mode 100644 index 1391ff5ac589..000000000000 --- a/.changes/data-tauri-drag-region-deep.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"tauri": minor:feat ---- - -Add `data-tauri-drag-region="deep"` so clicks on non-clickable children will drag as well. Can still opt out of drag on some regions using `data-tauri-drag-region="false"` diff --git a/.changes/load-tauri-protocol-async.md b/.changes/load-tauri-protocol-async.md new file mode 100644 index 000000000000..7338a74a8216 --- /dev/null +++ b/.changes/load-tauri-protocol-async.md @@ -0,0 +1,5 @@ +--- +tauri: patch:perf +--- + +Load `tauri://` custom protocol handlers asynchronously to speed up load time diff --git a/.changes/mobile-run-command-deadlock.md b/.changes/mobile-run-command-deadlock.md new file mode 100644 index 000000000000..0abcde9a5e55 --- /dev/null +++ b/.changes/mobile-run-command-deadlock.md @@ -0,0 +1,5 @@ +--- +"tauri": patch:bug +--- + +Adjust mutex locking in `send_channel_data_handler`, `handle_android_plugin_response`, `send_channel_data` to avoid deadlocks diff --git a/.changes/nsis-config-path-context.md b/.changes/nsis-config-path-context.md new file mode 100644 index 000000000000..0166e9c2961e --- /dev/null +++ b/.changes/nsis-config-path-context.md @@ -0,0 +1,5 @@ +--- +"tauri-bundler": "patch:enhance" +--- + +Improve NSIS configuration path errors so missing installer icons and images include the related config key and path. diff --git a/.changes/nsis-stock-plugins-embed-signed.md b/.changes/nsis-stock-plugins-embed-signed.md new file mode 100644 index 000000000000..dbce16faaef1 --- /dev/null +++ b/.changes/nsis-stock-plugins-embed-signed.md @@ -0,0 +1,5 @@ +--- +"tauri-bundler": "patch:bug" +--- + +Fix NSIS stock plugins (`NSISdl.dll`, `StartMenu.dll`, `System.dll`, `nsDialogs.dll`) being embedded in the final installer as unsigned despite the signing step succeeding. The signed local copies under `/Plugins/x86-unicode/` were not on makensis' plugin search path, so makensis fell back to the unsigned DLLs from the NSIS toolset directory. The fix adds `!addplugindir` for the signed plugin directory before any plugin command is parsed in the script. diff --git a/.changes/prompt-signing-key-password-context.md b/.changes/prompt-signing-key-password-context.md deleted file mode 100644 index 59f9fbb9bf17..000000000000 --- a/.changes/prompt-signing-key-password-context.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"tauri-cli": patch:enhance -"@tauri-apps/cli": patch:enhance ---- - -Show the context before prompting for updater signing key password diff --git a/.changes/reuse-mobile-dev-reqwest-client.md b/.changes/reuse-mobile-dev-reqwest-client.md new file mode 100644 index 000000000000..fcfe0125cff1 --- /dev/null +++ b/.changes/reuse-mobile-dev-reqwest-client.md @@ -0,0 +1,5 @@ +--- +tauri: patch:perf +--- + +Reuse proxy reqwest client in mobile dev, improving the dev load speed diff --git a/.changes/tray-icon-0.24.md b/.changes/tray-icon-0.24.md new file mode 100644 index 000000000000..47e3c81abf29 --- /dev/null +++ b/.changes/tray-icon-0.24.md @@ -0,0 +1,5 @@ +--- +"tauri": "patch:deps" +--- + +Updated dependency `tray-icon` to 0.24 diff --git a/.changes/v1-migrate-aliased-plugin-imports.md b/.changes/v1-migrate-aliased-plugin-imports.md new file mode 100644 index 000000000000..7b31a70f99b0 --- /dev/null +++ b/.changes/v1-migrate-aliased-plugin-imports.md @@ -0,0 +1,8 @@ +--- +"tauri-cli": "patch:bug" +"@tauri-apps/cli": "patch:bug" +--- + +Fix `tauri migrate` generating invalid namespace imports for aliased pluginified imports from `@tauri-apps/api`. + +Inputs like `import { cli as superCli } from "@tauri-apps/api"` now migrate to `import * as superCli from "@tauri-apps/plugin-cli"` instead of producing invalid ESM syntax. The migration tests also reparse migrated JS, Svelte, and Vue output so syntax regressions are caught directly. diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index d840272be8bd..157717061338 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -16,6 +16,8 @@ on: - '**/package.json' - '**/pnpm-lock.yaml' push: + branches: + - dev paths: - '.github/workflows/audit.yml' - '**/Cargo.lock' diff --git a/.github/workflows/lint-rust.yml b/.github/workflows/lint-rust.yml index b71cc1648eca..8b2ca12d90c7 100644 --- a/.github/workflows/lint-rust.yml +++ b/.github/workflows/lint-rust.yml @@ -12,6 +12,8 @@ on: paths: - '.github/workflows/lint-rust.yml' - 'crates/**' + - 'Cargo.toml' + - 'Cargo.lock' env: RUST_BACKTRACE: 1 diff --git a/.github/workflows/publish-cli-js.yml b/.github/workflows/publish-cli-js.yml index a2a53afab4e0..67753fcb1702 100644 --- a/.github/workflows/publish-cli-js.yml +++ b/.github/workflows/publish-cli-js.yml @@ -53,6 +53,8 @@ jobs: docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian build: | npm i -g --force corepack + rustup install 1.88.0 + rustup default 1.88.0 cd packages/cli pnpm build --target x86_64-unknown-linux-gnu strip *.node @@ -60,12 +62,15 @@ jobs: target: x86_64-unknown-linux-musl docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine build: | + rustup install 1.88.0 + rustup default 1.88.0 cd packages/cli pnpm build strip *.node - host: macos-latest target: aarch64-apple-darwin build: | + rustup target add x86_64-apple-darwin pnpm build --features native-tls-vendored --target=aarch64-apple-darwin strip -x *.node - host: ubuntu-22.04 @@ -73,6 +78,9 @@ jobs: docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64 build: | npm i -g --force corepack + rustup install 1.88.0 + rustup default 1.88.0 + rustup target add aarch64-unknown-linux-gnu cd packages/cli pnpm build --target aarch64-unknown-linux-gnu aarch64-unknown-linux-gnu-strip *.node @@ -90,8 +98,10 @@ jobs: target: aarch64-unknown-linux-musl docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine build: | - cd packages/cli + rustup install 1.88.0 + rustup default 1.88.0 rustup target add aarch64-unknown-linux-musl + cd packages/cli pnpm build --target aarch64-unknown-linux-musl /aarch64-linux-musl-cross/bin/aarch64-linux-musl-strip *.node - host: ubuntu-22.04 @@ -107,6 +117,8 @@ jobs: runs-on: ${{ matrix.settings.host }} steps: - uses: actions/checkout@v4 + - name: Prepare CEF package metadata and versions + run: node ../../.scripts/ci/prepare-cli-cef-publish.js - run: npm i -g --force corepack - name: Setup node uses: actions/setup-node@v4 @@ -136,7 +148,12 @@ jobs: if: ${{ matrix.settings.docker }} with: image: ${{ matrix.settings.docker }} - options: --user 0:0 -v ${{ github.workspace }}/.cargo-cache/git/db:/root/.cargo/git/db -v ${{ github.workspace }}/.cargo/registry/cache:/root/.cargo/registry/cache -v ${{ github.workspace }}/.cargo/registry/index:/root/.cargo/registry/index -v ${{ github.workspace }}:/build -w /build + options: --user 0:0 -v ${{ github.workspace + }}/.cargo-cache/git/db:/root/.cargo/git/db -v ${{ github.workspace + }}/.cargo/registry/cache:/root/.cargo/registry/cache -v ${{ + github.workspace + }}/.cargo/registry/index:/root/.cargo/registry/index -v ${{ + github.workspace }}:/build -w /build run: ${{ matrix.settings.build }} - name: Build @@ -212,8 +229,8 @@ jobs: - host: windows-latest target: x86_64-pc-windows-msvc node: - - '18' - '20' + - '22' runs-on: ${{ matrix.settings.host }} steps: - uses: actions/checkout@v4 @@ -243,8 +260,8 @@ jobs: fail-fast: false matrix: node: - - '18' - '20' + - '22' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -278,8 +295,8 @@ jobs: fail-fast: false matrix: node: - - '18' - '20' + - '22' runs-on: ubuntu-latest container: image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine @@ -319,8 +336,8 @@ jobs: fail-fast: false matrix: node: - - '18' - '20' + - '22' image: - ghcr.io/napi-rs/napi-rs/nodejs:aarch64-16 runs-on: ubuntu-latest @@ -345,7 +362,8 @@ jobs: - uses: addnab/docker-run-action@v3 with: image: ${{ matrix.image }} - options: '-v ${{ github.workspace }}:/build -w /build -e RUSTUP_HOME=/usr/local/rustup -e CARGO_HOME=/usr/local/cargo' + options: '-v ${{ github.workspace }}:/build -w /build -e + RUSTUP_HOME=/usr/local/rustup -e CARGO_HOME=/usr/local/cargo' shell: bash run: | set -e @@ -386,12 +404,14 @@ jobs: path: packages/cli/artifacts - name: Move artifacts run: pnpm artifacts + - name: Rewrite package names for CEF publish + run: node ../../.scripts/ci/prepare-cli-cef-publish.js - name: List packages run: ls -R ./npm shell: bash - name: Publish run: | - npm publish + npm publish --tag latest env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NODE_AUTH_TOKEN: '' diff --git a/.github/workflows/publish-cli-rs.yml b/.github/workflows/publish-cli-rs.yml index 15698e932fe3..fa02c2adbba2 100644 --- a/.github/workflows/publish-cli-rs.yml +++ b/.github/workflows/publish-cli-rs.yml @@ -76,22 +76,21 @@ jobs: - name: Build CLI if: ${{ !matrix.config.cross }} - run: cargo build --manifest-path ./crates/tauri-cli/Cargo.toml --profile release-size-optimized ${{ matrix.config.args }} + run: >- + cargo build --manifest-path ./crates/tauri-cli/Cargo.toml + --target ${{ matrix.config.rust_target }} + --profile release-size-optimized + ${{ matrix.config.args }} - name: Build CLI (cross) if: ${{ matrix.config.cross }} - run: cross build --manifest-path ./crates/tauri-cli/Cargo.toml --target ${{ matrix.config.rust_target }} --profile release-size-optimized ${{ matrix.config.args }} + run: >- + cross build --manifest-path ./crates/tauri-cli/Cargo.toml + --target ${{ matrix.config.rust_target }} + --profile release-size-optimized + ${{ matrix.config.args }} - name: Upload CLI - if: ${{ !matrix.config.cross }} - uses: actions/upload-artifact@v4 - with: - name: cargo-tauri-${{ matrix.config.rust_target }}${{ matrix.config.ext }} - path: target/release-size-optimized/cargo-tauri${{ matrix.config.ext }} - if-no-files-found: error - - - name: Upload CLI (cross) - if: ${{ matrix.config.cross }} uses: actions/upload-artifact@v4 with: name: cargo-tauri-${{ matrix.config.rust_target }}${{ matrix.config.ext }} diff --git a/.github/workflows/test-cli-rs.yml b/.github/workflows/test-cli-rs.yml index f7a5a08ca355..15f7d7de6bc2 100644 --- a/.github/workflows/test-cli-rs.yml +++ b/.github/workflows/test-cli-rs.yml @@ -14,6 +14,8 @@ on: - 'crates/tauri-utils/**' - 'crates/tauri-bundler/**' - 'crates/tauri-cli/**' + - 'Cargo.toml' + - 'Cargo.lock' env: RUST_BACKTRACE: 1 diff --git a/.github/workflows/test-core.yml b/.github/workflows/test-core.yml index 0256448e0e58..5c228bb11f5b 100644 --- a/.github/workflows/test-core.yml +++ b/.github/workflows/test-core.yml @@ -12,6 +12,8 @@ on: paths: - '.github/workflows/test-core.yml' - 'crates/**' + - 'Cargo.toml' + - 'Cargo.lock' - '!crates/tauri/scripts/**' - '!crates/tauri-cli/**' - '!crates/tauri-bundler/**' diff --git a/.scripts/ci/prepare-cli-cef-publish.js b/.scripts/ci/prepare-cli-cef-publish.js new file mode 100755 index 000000000000..daf16c142304 --- /dev/null +++ b/.scripts/ci/prepare-cli-cef-publish.js @@ -0,0 +1,66 @@ +#!/usr/bin/env node + +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +const { readdirSync, readFileSync, writeFileSync } = require('node:fs') +const { join } = require('node:path') + +const SOURCE_NAME = '@tauri-apps/cli' +const TARGET_NAME = '@tauri-apps/cli-cef' + +const cliDir = process.cwd() +const npmDir = join(cliDir, 'npm') +const cefCliVersionPath = join(cliDir, '.cef-cli-version') +const tauriCliCargoTomlPath = join(cliDir, '../../crates/tauri-cli/Cargo.toml') + +const cefCliVersion = readFileSync(cefCliVersionPath, 'utf8').trim() + +if (!cefCliVersion) { + throw new Error(`expected a version in ${cefCliVersionPath}`) +} + +function rewritePackageName(packageJsonPath, setVersion = false) { + const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf8')) + if (setVersion) { + pkg.version = cefCliVersion + } + if (typeof pkg.name === 'string' && pkg.name.startsWith(SOURCE_NAME)) { + pkg.name = pkg.name.replace(SOURCE_NAME, TARGET_NAME) + } + writeFileSync(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`) + console.log(`updated package metadata in ${packageJsonPath}`) +} + +rewritePackageName(join(cliDir, 'package.json'), true) + +for (const entry of readdirSync(npmDir, { withFileTypes: true })) { + if (!entry.isDirectory()) { + continue + } + rewritePackageName(join(npmDir, entry.name, 'package.json')) +} + +const indexJsPath = join(cliDir, 'index.js') +const indexContents = readFileSync(indexJsPath, 'utf8') +const rewrittenIndexContents = indexContents.replace( + /@tauri-apps\/cli(?=[-/'"`])/g, + TARGET_NAME +) + +if (rewrittenIndexContents !== indexContents) { + writeFileSync(indexJsPath, rewrittenIndexContents) + console.log(`rewrote native binding imports in ${indexJsPath}`) +} + +const tauriCliCargoToml = readFileSync(tauriCliCargoTomlPath, 'utf8') +const tauriCliCargoTomlWithVersion = tauriCliCargoToml.replace( + /^version = ".*"$/m, + `version = "${cefCliVersion}"` +) + +if (tauriCliCargoTomlWithVersion !== tauriCliCargoToml) { + writeFileSync(tauriCliCargoTomlPath, tauriCliCargoTomlWithVersion) + console.log(`updated tauri-cli version in ${tauriCliCargoTomlPath}`) +} diff --git a/Cargo.lock b/Cargo.lock index 171e8afe45b6..bade05be4bdb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,22 @@ version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +[[package]] +name = "ab_glyph" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" + [[package]] name = "acl-tests" version = "0.1.0" @@ -38,6 +54,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ + "bytes", "crypto-common", "generic-array", ] @@ -137,6 +154,31 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "android-activity" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd" +dependencies = [ + "android-properties", + "bitflags 2.7.0", + "cc", + "jni 0.22.4", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "num_enum", + "thiserror 2.0.18", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -235,7 +277,6 @@ dependencies = [ "tauri-build", "tauri-plugin-log", "tauri-plugin-sample", - "tiny_http", ] [[package]] @@ -292,7 +333,7 @@ dependencies = [ "pkcs1", "pkcs8", "plist", - "rand 0.8.5", + "rand 0.8.6", "rasn", "rayon", "regex", @@ -359,7 +400,7 @@ dependencies = [ "flate2", "log", "md-5", - "rand 0.8.5", + "rand 0.8.6", "reqwest 0.11.27", "scroll", "serde", @@ -426,10 +467,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] -name = "ascii" -version = "1.1.0" +name = "as-raw-xcb-connection" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" [[package]] name = "asn1-rs" @@ -440,7 +481,7 @@ dependencies = [ "asn1-rs-derive", "asn1-rs-impl", "displaydoc", - "nom", + "nom 7.1.3", "num-traits", "rusticata-macros", "thiserror 1.0.69", @@ -539,7 +580,7 @@ dependencies = [ "anyhow", "arrayvec", "log", - "nom", + "nom 7.1.3", "num-rational", "v_frame", ] @@ -717,10 +758,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" [[package]] -name = "bitfield" -version = "0.14.0" +name = "bitfields" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d7e60934ceec538daadb9d8432424ed043a904d8e0243f3c6446bce549a46ac" +checksum = "ef6e59298da389bc0649c7463856b34c6e17fe542f88939426ede4436c6b1195" +dependencies = [ + "bitfields-impl", +] + +[[package]] +name = "bitfields-impl" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2c044f98f86f15414668d6c8187c7e4fadab1ad2b31680f648703e0fe07c555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "thiserror 2.0.18", +] [[package]] name = "bitflags" @@ -774,7 +830,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d988fcc40055ceaa85edc55875a08f8abd29018582647fd82ad6128dba14a5f0" dependencies = [ "bitvec", - "nom", + "nom 7.1.3", ] [[package]] @@ -815,11 +871,11 @@ dependencies = [ [[package]] name = "block2" -version = "0.6.0" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d59b4c170e16f0405a2e95aff44432a0d41aa97675f3d52623effe95792a037" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" dependencies = [ - "objc2 0.6.3", + "objc2 0.6.4", ] [[package]] @@ -1043,6 +1099,31 @@ dependencies = [ "system-deps", ] +[[package]] +name = "calloop" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7" +dependencies = [ + "bitflags 2.7.0", + "polling", + "rustix 1.0.7", + "slab", + "tracing", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" +dependencies = [ + "calloop", + "rustix 1.0.7", + "wayland-backend", + "wayland-client", +] + [[package]] name = "camellia" version = "0.1.0" @@ -1064,9 +1145,9 @@ dependencies = [ [[package]] name = "cargo-mobile2" -version = "0.21.1" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcea7efeaac9f0fd9f886f43a13dde186a1e2266fe6b53a42659e4e0689570de" +checksum = "caa0060b6b7e1c0f14312c8ccebf28f5713b3045ffaae033a180622122a361dc" dependencies = [ "colored", "core-foundation 0.10.0", @@ -1083,16 +1164,17 @@ dependencies = [ "java-properties", "libc", "log", - "once-cell-regex", + "once_cell", "os_info", "os_pipe", "path_abs", "plist", + "regex", "serde", "serde_json", "textwrap", - "thiserror 2.0.12", - "toml 0.9.10+spec-1.1.0", + "thiserror 2.0.18", + "toml 1.0.6+spec-1.1.0", "ureq", "which", "windows 0.61.1", @@ -1108,6 +1190,16 @@ dependencies = [ "serde", ] +[[package]] +name = "cargo-platform" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0061da739915fae12ea00e16397555ed4371a6bb285431aab930f61b0aa4ba" +dependencies = [ + "serde", + "serde_core", +] + [[package]] name = "cargo_metadata" version = "0.19.0" @@ -1115,13 +1207,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afc309ed89476c8957c50fb818f56fe894db857866c3e163335faa91dc34eb85" dependencies = [ "camino", - "cargo-platform", + "cargo-platform 0.1.8", "semver", "serde", "serde_json", "thiserror 1.0.69", ] +[[package]] +name = "cargo_metadata" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef987d17b0a113becdd19d3d0022d04d7ef41f9efe4f3fb63ac44ba61df3ade9" +dependencies = [ + "camino", + "cargo-platform 0.3.3", + "semver", + "serde", + "serde_json", + "thiserror 2.0.18", +] + [[package]] name = "cargo_toml" version = "0.22.3" @@ -1129,7 +1235,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" dependencies = [ "serde", - "toml 0.9.10+spec-1.1.0", + "toml 0.9.12+spec-1.1.0", ] [[package]] @@ -1173,24 +1279,33 @@ dependencies = [ [[package]] name = "cef" -version = "146.4.1+146.0.9" +version = "148.0.0+147.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5dad6495c583fedab04a24f6fc08274c59a28b33967ca87709398e1f11c2ebe" +checksum = "2445d2f1e820efc064c511faa3a07c3ee79e565fdae026ca62ed3c80d0459223" dependencies = [ + "anyhow", + "cargo_metadata 0.23.1", "cef-dll-sys", + "clap", "libloading 0.9.0", - "objc2 0.6.3", + "objc2 0.6.4", + "plist", + "semver", + "serde", + "serde_json", + "thiserror 2.0.18", "windows-sys 0.61.1", ] [[package]] name = "cef-dll-sys" -version = "146.4.1+146.0.9" -source = "git+https://github.com/tauri-apps/cef-rs?branch=fix%2F146-location-windows#7fc79c7aa4f1d691ed9996d440f13eb34a151f2d" +version = "148.0.0+147.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d32a7c89af6c77688ad58dc75555c9398c5ded2c328a3ff83554fccc840af57" dependencies = [ "anyhow", "cmake", - "download-cef 2.3.1 (git+https://github.com/tauri-apps/cef-rs?branch=fix%2F146-location-windows)", + "download-cef", "serde_json", ] @@ -1257,12 +1372,6 @@ dependencies = [ "windows-link 0.1.1", ] -[[package]] -name = "chunked_transfer" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901" - [[package]] name = "cipher" version = "0.4.4" @@ -1394,6 +1503,15 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "console" version = "0.15.10" @@ -1534,6 +1652,19 @@ dependencies = [ "libc", ] +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.7.0", + "core-foundation 0.10.0", + "core-graphics-types", + "foreign-types 0.5.0", + "libc", +] + [[package]] name = "core-graphics-types" version = "0.2.0" @@ -1560,12 +1691,6 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "417bef24afe1460300965a25ff4a24b8b45ad011948302ec221e8a0a81eb2c79" -[[package]] -name = "cpio" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "938e716cb1ade5d6c8f959c13a7248b889c07491fc7e41167c3afe20f8f0de1e" - [[package]] name = "cpio-archive" version = "0.9.0" @@ -1705,6 +1830,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf 0.13.1", + "smallvec", +] + [[package]] name = "cssparser-macros" version = "0.6.1" @@ -1717,29 +1855,29 @@ dependencies = [ [[package]] name = "ctor" -version = "0.2.9" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +checksum = "424e0138278faeb2b401f174ad17e715c829512d74f3d1e81eb43365c2e0590e" dependencies = [ - "quote", - "syn 2.0.117", + "ctor-proc-macro", + "dtor 0.1.1", ] [[package]] name = "ctor" -version = "0.4.2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4735f265ba6a1188052ca32d461028a7d1125868be18e287e756019da7607b5" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" dependencies = [ "ctor-proc-macro", - "dtor", + "dtor 0.3.0", ] [[package]] name = "ctor-proc-macro" -version = "0.0.5" +version = "0.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f211af61d8efdd104f96e57adf5e426ba1bc3ed7a4ead616e15e5881fd79c4d" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" [[package]] name = "ctr" @@ -1760,6 +1898,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "cursor-icon" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -1770,7 +1914,7 @@ dependencies = [ "cpufeatures", "curve25519-dalek-derive", "digest", - "fiat-crypto 0.2.9", + "fiat-crypto", "rand_core 0.6.4", "rustc_version", "subtle", @@ -1788,6 +1932,23 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "cx448" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c0cf476284b03eb6c10e78787b21c7abb7d7d43cb2f02532ba6b831ed892fa" +dependencies = [ + "crypto-bigint", + "elliptic-curve", + "pkcs8", + "rand_core 0.6.4", + "serdect 0.3.0", + "sha3", + "signature", + "subtle", + "zeroize", +] + [[package]] name = "darling" version = "0.20.10" @@ -1858,6 +2019,17 @@ dependencies = [ "generic-array", ] +[[package]] +name = "dbus" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b3aa68d7e7abee336255bd7248ea965cc393f3e70411135a6f6a4b651345d4" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.59.0", +] + [[package]] name = "der" version = "0.7.9" @@ -1936,18 +2108,18 @@ dependencies = [ [[package]] name = "derive_more" -version = "1.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" dependencies = [ "derive_more-impl", ] [[package]] name = "derive_more-impl" -version = "1.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" dependencies = [ "proc-macro2", "quote", @@ -2056,10 +2228,16 @@ dependencies = [ ] [[package]] -name = "dispatch" -version = "0.2.0" +name = "dispatch2" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.7.0", + "block2 0.6.2", + "libc", + "objc2 0.6.4", +] [[package]] name = "displaydoc" @@ -2072,6 +2250,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading 0.8.9", +] + [[package]] name = "dlopen2" version = "0.8.0" @@ -2103,36 +2290,39 @@ checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" [[package]] name = "document-features" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" dependencies = [ "litrs", ] [[package]] -name = "download-cef" -version = "2.3.1" +name = "dom_query" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7471a7d5d3bd8df1b3b75871f0317d8a6dd73270ab861cc97ae7907ee63e554" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" dependencies = [ - "bzip2 0.6.0", - "clap", - "indicatif", - "regex", - "semver", - "serde", - "serde_json", - "sha1_smol", - "tar", - "thiserror 2.0.12", - "ureq", + "bit-set", + "cssparser 0.36.0", + "foldhash", + "html5ever 0.38.0", + "precomputed-hash", + "selectors 0.36.1", + "tendril 0.5.0", ] +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + [[package]] name = "download-cef" version = "2.3.1" -source = "git+https://github.com/tauri-apps/cef-rs?branch=fix%2F146-location-windows#7fc79c7aa4f1d691ed9996d440f13eb34a151f2d" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7471a7d5d3bd8df1b3b75871f0317d8a6dd73270ab861cc97ae7907ee63e554" dependencies = [ "bzip2 0.6.0", "clap", @@ -2143,15 +2333,15 @@ dependencies = [ "serde_json", "sha1_smol", "tar", - "thiserror 2.0.12", + "thiserror 2.0.18", "ureq", ] [[package]] name = "dpi" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" dependencies = [ "serde", ] @@ -2189,24 +2379,33 @@ dependencies = [ [[package]] name = "dtor" -version = "0.0.6" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "404d02eeb088a82cfd873006cb713fe411306c7d182c344905e101fb1167d301" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97cbdf2ad6846025e8e25df05171abfb30e3ababa12ee0a0e44b9bbe570633a8" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" dependencies = [ "dtor-proc-macro", ] [[package]] name = "dtor-proc-macro" -version = "0.0.5" +version = "0.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7454e41ff9012c00d53cf7f475c5e3afa3b91b7c90568495495e8d9bf47a1055" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" [[package]] name = "duct" -version = "1.0.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6ce170a0e8454fa0f9b0e5ca38a6ba17ed76a50916839d217eb5357e05cdfde" +checksum = "7e66e9c0c03d094e1a0ba1be130b849034aa80c3a2ab8ee94316bc809f3fa684" dependencies = [ "libc", "os_pipe", @@ -2271,23 +2470,13 @@ checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" dependencies = [ "curve25519-dalek", "ed25519", + "rand_core 0.6.4", "serde", "sha2", "subtle", "zeroize", ] -[[package]] -name = "ed448-goldilocks" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87b5fa9e9e3dd5fe1369f380acd3dcdfa766dbd0a1cd5b048fb40e38a6a78e79" -dependencies = [ - "fiat-crypto 0.1.20", - "hex", - "subtle", -] - [[package]] name = "either" version = "1.13.0" @@ -2307,6 +2496,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ "base16ct", + "base64ct", "crypto-bigint", "digest", "ff", @@ -2317,7 +2507,10 @@ dependencies = [ "pkcs8", "rand_core 0.6.4", "sec1", + "serde_json", + "serdect 0.2.0", "subtle", + "tap", "zeroize", ] @@ -2339,7 +2532,7 @@ dependencies = [ "cc", "memchr", "rustc_version", - "toml 0.9.10+spec-1.1.0", + "toml 0.9.12+spec-1.1.0", "vswhom", "winreg 0.55.0", ] @@ -2528,16 +2721,11 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" dependencies = [ + "bitvec", "rand_core 0.6.4", "subtle", ] -[[package]] -name = "fiat-crypto" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77" - [[package]] name = "fiat-crypto" version = "0.2.9" @@ -2629,6 +2817,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "fontconfig-parser" version = "0.5.7" @@ -2715,12 +2909,12 @@ dependencies = [ [[package]] name = "freedesktop_entry_parser" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db9c27b72f19a99a895f8ca89e2d26e4ef31013376e56fdafef697627306c3e4" +checksum = "2b368437186ec63ceb50d0832ee1ebcb5878037fe16ead1c68081d4aee0d140a" dependencies = [ - "nom", - "thiserror 1.0.69", + "indexmap 2.11.4", + "nom 8.0.0", ] [[package]] @@ -2962,6 +3156,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix 1.0.7", + "windows-link 0.2.1", +] + [[package]] name = "getrandom" version = "0.1.16" @@ -2992,17 +3196,28 @@ checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 5.3.0", "wasi 0.14.2+wasi-0.2.4", ] [[package]] -name = "ghash" -version = "0.5.1" +name = "getrandom" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ - "opaque-debug", + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", "polyval", ] @@ -3277,7 +3492,7 @@ dependencies = [ "pest_derive", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.18", ] [[package]] @@ -3328,6 +3543,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -3369,10 +3590,20 @@ checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" dependencies = [ "log", "mac", - "markup5ever", + "markup5ever 0.14.1", "match_token", ] +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever 0.38.0", +] + [[package]] name = "http" version = "0.2.12" @@ -3453,6 +3684,25 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "hybrid-array" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2d35805454dc9f8662a98d6d61886ffe26bd465f5960e0e55345c70d5c0d2a9" +dependencies = [ + "typenum", +] + +[[package]] +name = "hybrid-array" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d15931895091dea5c47afa5b3c9a01ba634b311919fd4d41388fa0e3d76af" +dependencies = [ + "typenum", + "zeroize", +] + [[package]] name = "hyper" version = "0.14.32" @@ -3601,7 +3851,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" dependencies = [ "byteorder", - "png", + "png 0.17.16", ] [[package]] @@ -3787,7 +4037,7 @@ dependencies = [ "gif", "image-webp", "num-traits", - "png", + "png 0.17.16", "qoi", "ravif", "rayon", @@ -3995,12 +4245,6 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" -[[package]] -name = "iter-read" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071ed4cc1afd86650602c7b11aa2e1ce30762a1c27193201cb5cee9c6ebb1294" - [[package]] name = "itertools" version = "0.10.5" @@ -4028,6 +4272,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.14" @@ -4077,19 +4330,68 @@ dependencies = [ "cesu8", "cfg-if", "combine", - "jni-sys", + "jni-sys 0.3.0", "log", "thiserror 1.0.69", "walkdir", "windows-sys 0.45.0", ] +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link 0.2.1", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.117", +] + [[package]] name = "jni-sys" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.117", +] + [[package]] name = "jobserver" version = "0.1.32" @@ -4107,10 +4409,12 @@ checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" [[package]] name = "js-sys" -version = "0.3.83" +version = "0.3.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] @@ -4195,7 +4499,7 @@ dependencies = [ "jsonrpsee-types", "parking_lot", "pin-project", - "rand 0.8.5", + "rand 0.8.6", "rustc-hash", "serde", "serde_json", @@ -4312,6 +4616,16 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "kem" +version = "0.3.0-pre.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b8645470337db67b01a7f966decf7d0bafedbae74147d33e641c67a91df239f" +dependencies = [ + "rand_core 0.6.4", + "zeroize", +] + [[package]] name = "keyboard-types" version = "0.7.0" @@ -4323,6 +4637,16 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "keyboard-types" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbe853b403ae61a04233030ae8a79d94975281ed9770a1f9e246732b534b28d" +dependencies = [ + "bitflags 2.7.0", + "serde", +] + [[package]] name = "konst" version = "0.3.16" @@ -4369,10 +4693,10 @@ version = "0.8.8-speedreader" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" dependencies = [ - "cssparser", - "html5ever", + "cssparser 0.29.6", + "html5ever 0.29.1", "indexmap 2.11.4", - "selectors", + "selectors 0.24.0", ] [[package]] @@ -4436,6 +4760,15 @@ version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + [[package]] name = "libfuzzer-sys" version = "0.4.8" @@ -4458,12 +4791,12 @@ dependencies = [ [[package]] name = "libloading" -version = "0.8.6" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-link 0.2.1", ] [[package]] @@ -4473,7 +4806,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60" dependencies = [ "cfg-if", - "windows-link 0.2.0", + "windows-link 0.2.1", ] [[package]] @@ -4553,9 +4886,9 @@ checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" [[package]] name = "litrs" -version = "0.4.2" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" [[package]] name = "local-ip-address" @@ -4642,9 +4975,20 @@ dependencies = [ "log", "phf 0.11.3", "phf_codegen 0.11.3", - "string_cache", - "string_cache_codegen", - "tendril", + "string_cache 0.8.7", + "string_cache_codegen 0.5.2", + "tendril 0.4.3", +] + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril 0.5.0", + "web_atoms", ] [[package]] @@ -4791,36 +5135,65 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "ml-dsa" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac4a46643af2001eafebcc37031fc459eb72d45057aac5d7a15b00046a2ad6db" +dependencies = [ + "const-oid", + "hybrid-array 0.3.1", + "num-traits", + "pkcs8", + "rand_core 0.6.4", + "sha3", + "signature", + "zeroize", +] + +[[package]] +name = "ml-kem" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de49b3df74c35498c0232031bb7e85f9389f913e2796169c8ab47a53993a18f" +dependencies = [ + "hybrid-array 0.2.3", + "kem", + "rand_core 0.6.4", + "sha3", + "zeroize", +] + [[package]] name = "muda" -version = "0.17.1" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a" +checksum = "0ae8844f63b5b118e334e205585b8c5c17b984121dbdb179d44aeb087ffad3cb" dependencies = [ "crossbeam-channel", "dpi", "gtk", - "keyboard-types", + "keyboard-types 0.7.0", "libxdo", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation 0.3.0", + "objc2-foundation 0.3.2", "once_cell", - "png", + "png 0.18.1", "serde", - "thiserror 2.0.12", - "windows-sys 0.60.2", + "thiserror 2.0.18", + "windows-sys 0.61.1", ] [[package]] name = "napi" -version = "3.0.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abf3f418eeb94d86f02f3388324017e1e21e36937e4b1d8075ea5843d980f766" +checksum = "c3a1135cfe16ca43ac82ac05858554fc39c037d8e4592f2b4a83d7ef8e822f43" dependencies = [ "bitflags 2.7.0", - "ctor 0.4.2", + "ctor 0.6.3", "napi-build", "napi-sys", "nohash-hasher", @@ -4829,18 +5202,18 @@ dependencies = [ [[package]] name = "napi-build" -version = "2.2.2" +version = "2.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fff539e61c5e3dd4d7d283610662f5d672c2aea0f158df78af694f13dbb3287b" +checksum = "3ae82775d1b06f3f07efd0666e59bbc175da8383bc372051031d7a447e94fbea" [[package]] name = "napi-derive" -version = "3.0.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce09c2a155c7c38447a6ff1711899375bdd8f9dd5a50aad700a9c765c9f8a375" +checksum = "78665d6bdf10e9a4e6b38123efb0f66962e6197c1aea2f07cff3f159a374696d" dependencies = [ "convert_case 0.8.0", - "ctor 0.4.2", + "ctor 0.6.3", "napi-derive-backend", "proc-macro2", "quote", @@ -4849,9 +5222,9 @@ dependencies = [ [[package]] name = "napi-derive-backend" -version = "2.0.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17923387d68ecfe057a04a38ee89aeb41f415c43b4c7a508a3683bf0c1511c5a" +checksum = "42d55d01423e7264de3acc13b258fa48ca7cf38a4d25db848908ec3c1304a85a" dependencies = [ "convert_case 0.8.0", "proc-macro2", @@ -4862,11 +5235,11 @@ dependencies = [ [[package]] name = "napi-sys" -version = "3.0.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e4e7135a8f97aa0f1509cce21a8a1f9dcec1b50d8dee006b48a5adb69a9d64d" +checksum = "1ed8f0e23a62a3ce0fbb6527cdc056e9282ddd9916b068c46f8923e18eed5ee6" dependencies = [ - "libloading 0.8.6", + "libloading 0.8.9", ] [[package]] @@ -4893,7 +5266,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ "bitflags 2.7.0", - "jni-sys", + "jni-sys 0.3.0", "log", "ndk-sys", "num_enum", @@ -4913,7 +5286,7 @@ version = "0.6.0+11769913" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" dependencies = [ - "jni-sys", + "jni-sys 0.3.0", ] [[package]] @@ -4981,6 +5354,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + [[package]] name = "nonmax" version = "0.5.5" @@ -5065,7 +5447,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand 0.8.5", + "rand 0.8.6", "serde", "smallvec", "zeroize", @@ -5207,9 +5589,9 @@ dependencies = [ [[package]] name = "objc2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" dependencies = [ "objc2-encode", "objc2-exception-helper", @@ -5217,75 +5599,116 @@ dependencies = [ [[package]] name = "objc2-app-kit" -version = "0.3.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5906f93257178e2f7ae069efb89fbd6ee94f0592740b5f8a1512ca498814d0fb" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ "bitflags 2.7.0", - "block2 0.6.0", - "libc", - "objc2 0.6.3", - "objc2-cloud-kit", - "objc2-core-data", + "block2 0.6.2", + "objc2 0.6.4", "objc2-core-foundation", "objc2-core-graphics", - "objc2-core-image", - "objc2-foundation 0.3.0", - "objc2-quartz-core 0.3.0", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", +] + +[[package]] +name = "objc2-application-services" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69282c2b5bc58fba07cb9de2113619532eb551e98efe3d8d695509ef45fbd53b" +dependencies = [ + "objc2-core-foundation", ] [[package]] name = "objc2-cloud-kit" -version = "0.3.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c1948a9be5f469deadbd6bcb86ad7ff9e47b4f632380139722f7d9840c0d42c" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" dependencies = [ "bitflags 2.7.0", - "objc2 0.6.3", - "objc2-foundation 0.3.0", + "objc2 0.6.4", + "objc2-foundation 0.3.2", ] [[package]] name = "objc2-core-data" -version = "0.3.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f860f8e841f6d32f754836f51e6bc7777cd7e7053cf18528233f6811d3eceb4" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" dependencies = [ - "bitflags 2.7.0", - "objc2 0.6.3", - "objc2-foundation 0.3.0", + "objc2 0.6.4", + "objc2-foundation 0.3.2", ] [[package]] name = "objc2-core-foundation" -version = "0.3.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daeaf60f25471d26948a1c2f840e3f7d86f4109e3af4e8e4b5cd70c39690d925" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags 2.7.0", - "objc2 0.6.3", + "block2 0.6.2", + "dispatch2", + "objc2 0.6.4", ] [[package]] name = "objc2-core-graphics" -version = "0.3.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dca602628b65356b6513290a21a6405b4d4027b8b250f0b98dddbb28b7de02" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ "bitflags 2.7.0", - "objc2 0.6.3", + "dispatch2", + "libc", + "objc2 0.6.4", "objc2-core-foundation", "objc2-io-surface", ] [[package]] name = "objc2-core-image" -version = "0.3.0" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.7.0", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-core-video" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ffa6bea72bf42c78b0b34e89c0bafac877d5f80bf91e159a5d96ea7f693ca56" +checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" dependencies = [ - "objc2 0.6.3", - "objc2-foundation 0.3.0", + "bitflags 2.7.0", + "objc2-core-foundation", + "objc2-core-graphics", ] [[package]] @@ -5317,25 +5740,24 @@ dependencies = [ [[package]] name = "objc2-foundation" -version = "0.3.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a21c6c9014b82c39515db5b396f91645182611c97d24637cf56ac01e5f8d998" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "bitflags 2.7.0", - "block2 0.6.0", - "libc", - "objc2 0.6.3", + "block2 0.6.2", + "objc2 0.6.4", "objc2-core-foundation", ] [[package]] name = "objc2-io-surface" -version = "0.3.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "161a8b87e32610086e1a7a9e9ec39f84459db7b3a0881c1f16ca5a2605581c19" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ "bitflags 2.7.0", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-core-foundation", ] @@ -5366,13 +5788,15 @@ dependencies = [ [[package]] name = "objc2-quartz-core" -version = "0.3.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb3794501bb1bee12f08dcad8c61f2a5875791ad1c6f47faa71a0f033f20071" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ "bitflags 2.7.0", - "objc2 0.6.3", - "objc2-foundation 0.3.0", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", ] [[package]] @@ -5382,20 +5806,39 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3126341c65c5d5728423ae95d788e1b660756486ad0592307ab87ba02d9a7268" dependencies = [ "bitflags 2.7.0", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-core-foundation", ] [[package]] name = "objc2-ui-kit" -version = "0.3.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "777a571be14a42a3990d4ebedaeb8b54cd17377ec21b92e8200ac03797b3bee1" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ "bitflags 2.7.0", - "objc2 0.6.3", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-cloud-kit", + "objc2-core-data", "objc2-core-foundation", - "objc2-foundation 0.3.0", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2 0.6.4", + "objc2-foundation 0.3.2", ] [[package]] @@ -5405,11 +5848,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b717127e4014b0f9f3e8bba3d3f2acec81f1bde01f656823036e823ed2c94dce" dependencies = [ "bitflags 2.7.0", - "block2 0.6.0", - "objc2 0.6.3", + "block2 0.6.2", + "objc2 0.6.4", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation 0.3.0", + "objc2-foundation 0.3.2", "objc2-security", ] @@ -5457,21 +5900,11 @@ dependencies = [ "asn1-rs", ] -[[package]] -name = "once-cell-regex" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3de7e389a5043420c8f2b95ed03f3f104ad6f4c41f7d7e27298f033abc253e8" -dependencies = [ - "once_cell", - "regex", -] - [[package]] name = "once_cell" -version = "1.20.2" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "opaque-debug" @@ -5481,9 +5914,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" -version = "0.10.72" +version = "0.10.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" +checksum = "f38c4372413cdaaf3cc79dd92d29d7d9f5ab09b51b10dded508fb90bb70b9222" dependencies = [ "bitflags 2.7.0", "cfg-if", @@ -5528,9 +5961,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.107" +version = "0.9.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07" +checksum = "13ce1245cd07fcc4cfdb438f7507b0c7e4f3849a69fd84d52374c66d83741bb6" dependencies = [ "cc", "libc", @@ -5545,6 +5978,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "orbclient" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5df339f526ea9a60e371768d50efc2f2508c7203290731565d1f7a6f71d21747" +dependencies = [ + "libc", + "libredox", +] + [[package]] name = "ordered-float" version = "2.10.1" @@ -5567,12 +6010,12 @@ dependencies = [ [[package]] name = "os_pipe" -version = "1.2.1" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ffd2b0a5634335b135d5728d84c5e0fd726954b87111f7506a61c502280d982" +checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.1", ] [[package]] @@ -5582,10 +6025,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4030760ffd992bef45b0ae3f10ce1aba99e33464c90d14dd7c039884963ddc7a" [[package]] -name = "owo-colors" -version = "4.1.0" +name = "owned_ttf_parser" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb37767f6569cd834a413442455e0f066d0d522de8630436e2a1761d9726ba56" +checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" +dependencies = [ + "ttf-parser", +] + +[[package]] +name = "owo-colors" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb37767f6569cd834a413442455e0f066d0d522de8630436e2a1761d9726ba56" [[package]] name = "oxc-miette" @@ -5941,7 +6393,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc" dependencies = [ "memchr", - "thiserror 2.0.12", + "thiserror 2.0.18", "ucd-trie", ] @@ -5981,31 +6433,33 @@ dependencies = [ [[package]] name = "pgp" -version = "0.14.2" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1877a97fd422433220ad272eb008ec55691944b1200e9eb204e3cb2cb69d34e9" +checksum = "eaffe1ec22db286599c30ae6be75b37493b558735d86c8e59ec5c38794415fe4" dependencies = [ + "aead", "aes", "aes-gcm", "aes-kw", "argon2", "base64 0.22.1", - "bitfield", + "bitfields", "block-padding", "blowfish", - "bstr", "buffer-redux", "byteorder", + "bytes", + "bzip2 0.6.0", "camellia", "cast5", "cfb-mode", - "chrono", "cipher", "const-oid", "crc24", "curve25519-dalek", + "cx448", "derive_builder", - "derive_more 1.0.0", + "derive_more 2.0.1", "des", "digest", "dsa", @@ -6018,11 +6472,12 @@ dependencies = [ "hex", "hkdf", "idea", - "iter-read", "k256", "log", "md-5", - "nom", + "ml-dsa", + "ml-kem", + "nom 8.0.0", "num-bigint-dig", "num-traits", "num_enum", @@ -6030,7 +6485,9 @@ dependencies = [ "p256", "p384", "p521", - "rand 0.8.5", + "rand 0.8.6", + "regex", + "replace_with", "ripemd", "rsa", "sha1", @@ -6038,11 +6495,11 @@ dependencies = [ "sha2", "sha3", "signature", + "slh-dsa", "smallvec", - "thiserror 1.0.69", + "snafu 0.8.9", "twofish", "x25519-dalek", - "x448", "zeroize", ] @@ -6076,6 +6533,17 @@ dependencies = [ "phf_shared 0.11.3", ] +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros 0.13.1", + "phf_shared 0.13.1", + "serde", +] + [[package]] name = "phf_codegen" version = "0.8.0" @@ -6096,6 +6564,16 @@ dependencies = [ "phf_shared 0.11.3", ] +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", +] + [[package]] name = "phf_generator" version = "0.8.0" @@ -6113,7 +6591,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" dependencies = [ "phf_shared 0.10.0", - "rand 0.8.5", + "rand 0.8.6", ] [[package]] @@ -6123,7 +6601,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared 0.11.3", - "rand 0.8.5", + "rand 0.8.6", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared 0.13.1", ] [[package]] @@ -6153,6 +6641,19 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "phf_shared" version = "0.8.0" @@ -6180,6 +6681,15 @@ dependencies = [ "siphasher 1.0.1", ] +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher 1.0.1", +] + [[package]] name = "pico-args" version = "0.5.0" @@ -6259,7 +6769,7 @@ checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" dependencies = [ "base64 0.22.1", "indexmap 2.11.4", - "quick-xml", + "quick-xml 0.32.0", "serde", "time", ] @@ -6277,6 +6787,33 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.7.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi 0.5.2", + "pin-project-lite", + "rustix 1.0.7", + "windows-sys 0.61.1", +] + [[package]] name = "polyval" version = "0.6.2" @@ -6395,9 +6932,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.92" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -6445,7 +6982,7 @@ dependencies = [ "bitflags 2.7.0", "lazy_static", "num-traits", - "rand 0.8.5", + "rand 0.8.6", "rand_chacha 0.3.1", "rand_xorshift", "regex-syntax", @@ -6504,6 +7041,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "quick-xml" +version = "0.39.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" +dependencies = [ + "memchr", +] + [[package]] name = "quickcheck" version = "1.0.3" @@ -6512,7 +7058,7 @@ checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" dependencies = [ "env_logger 0.8.4", "log", - "rand 0.8.5", + "rand 0.8.6", ] [[package]] @@ -6528,9 +7074,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.38" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ "proc-macro2", ] @@ -6541,6 +7087,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "radium" version = "0.7.0" @@ -6563,9 +7115,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -6574,9 +7126,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.1" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", @@ -6680,13 +7232,13 @@ dependencies = [ "either", "jzon", "konst", - "nom", + "nom 7.1.3", "num-bigint", "num-integer", "num-traits", "once_cell", "rasn-derive", - "snafu", + "snafu 0.7.5", ] [[package]] @@ -6730,7 +7282,7 @@ dependencies = [ "once_cell", "paste", "profiling", - "rand 0.8.5", + "rand 0.8.6", "rand_chacha 0.3.1", "simd_helpers", "system-deps", @@ -6817,7 +7369,7 @@ checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" dependencies = [ "getrandom 0.2.15", "libredox", - "thiserror 2.0.12", + "thiserror 2.0.18", ] [[package]] @@ -6892,6 +7444,12 @@ dependencies = [ "bytecheck", ] +[[package]] +name = "replace_with" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51743d3e274e2b18df81c4dc6caf8a5b8e15dbe799e0dca05c7617380094e884" + [[package]] name = "reqwest" version = "0.11.27" @@ -6936,9 +7494,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.13.1" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04e9018c9d814e5f30cc16a0f03271aeab3571e609612d9fe78c1aa8d11c2f62" +checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" dependencies = [ "base64 0.22.1", "bytes", @@ -7107,34 +7665,43 @@ dependencies = [ [[package]] name = "rpm" -version = "0.16.0" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1630639f4dbc1c71ad7b704cda2171584c80735c502efae94804d02763fc6f7d" +checksum = "3a5c5d43b95d73d6f0a1b8fbeab72a7ebebf618cdc99f2b9a28f269ee343766e" dependencies = [ + "base64 0.22.1", "bitflags 2.7.0", - "bzip2 0.4.4", + "bzip2 0.6.0", "chrono", - "cpio", "digest", "enum-display-derive", "enum-primitive-derive", "flate2", + "getrandom 0.4.3", "hex", - "itertools 0.13.0", + "itertools 0.14.0", "log", - "md-5", - "nom", + "memchr", + "nom 8.0.0", "num", "num-derive", "num-traits", "pgp", + "rpm-version", "sha1", "sha2", - "thiserror 2.0.12", - "xz2", + "sha3", + "thiserror 2.0.18", + "zeroize", "zstd", ] +[[package]] +name = "rpm-version" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56568fb1bf1d2c0a640aac216b3d84d65e210ca1997df922bf124f2bfaf9efd9" + [[package]] name = "rsa" version = "0.9.10" @@ -7175,7 +7742,7 @@ dependencies = [ "borsh", "bytes", "num-traits", - "rand 0.8.5", + "rand 0.8.6", "rkyv", "serde", "serde_json", @@ -7208,7 +7775,7 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" dependencies = [ - "nom", + "nom 7.1.3", ] [[package]] @@ -7287,7 +7854,7 @@ dependencies = [ "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.8", + "rustls-webpki 0.103.13", "subtle", "zeroize", ] @@ -7364,13 +7931,13 @@ checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" dependencies = [ "core-foundation 0.10.0", "core-foundation-sys", - "jni", + "jni 0.21.1", "log", "once_cell", "rustls 0.23.35", "rustls-native-certs 0.8.3", "rustls-platform-verifier-android", - "rustls-webpki 0.103.8", + "rustls-webpki 0.103.13", "security-framework 3.5.1", "security-framework-sys", "webpki-root-certs", @@ -7406,9 +7973,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.8" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "ring", "rustls-pki-types", @@ -7488,9 +8055,9 @@ dependencies = [ [[package]] name = "scc" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28e1c91382686d21b5ac7959341fcb9780fa7c03773646995a87c950fa7be640" +checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc" dependencies = [ "sdd", ] @@ -7531,6 +8098,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" @@ -7578,11 +8151,24 @@ dependencies = [ "untrusted", ] +[[package]] +name = "sctk-adwaita" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd3accc0f3f4bbaf2c9e1957a030dc582028130c67660d44c0a0345a22ca69b" +dependencies = [ + "ab_glyph", + "log", + "memmap2", + "smithay-client-toolkit", + "tiny-skia", +] + [[package]] name = "sdd" -version = "3.0.5" +version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478f121bb72bbf63c52c93011ea1791dca40140dfe13f8336c4c5ac952c33aa9" +checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" [[package]] name = "seahash" @@ -7600,6 +8186,7 @@ dependencies = [ "der", "generic-array", "pkcs8", + "serdect 0.2.0", "subtle", "zeroize", ] @@ -7647,14 +8234,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" dependencies = [ "bitflags 1.3.2", - "cssparser", + "cssparser 0.29.6", "derive_more 0.99.18", "fxhash", "log", "phf 0.8.0", "phf_codegen 0.8.0", "precomputed-hash", - "servo_arc", + "servo_arc 0.2.0", + "smallvec", +] + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.7.0", + "cssparser 0.36.0", + "derive_more 2.0.1", + "log", + "new_debug_unreachable", + "phf 0.13.1", + "phf_codegen 0.13.1", + "precomputed-hash", + "rustc-hash", + "servo_arc 0.4.3", "smallvec", ] @@ -7876,6 +8482,26 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "serdect" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" +dependencies = [ + "base16ct", + "serde", +] + +[[package]] +name = "serdect" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f42f67da2385b51a5f9652db9c93d78aeaf7610bf5ec366080b6de810604af53" +dependencies = [ + "base16ct", + "serde", +] + [[package]] name = "serial_test" version = "3.2.0" @@ -7933,6 +8559,15 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "sha1" version = "0.10.6" @@ -7984,19 +8619,20 @@ dependencies = [ [[package]] name = "shared_child" -version = "1.0.1" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09fa9338aed9a1df411814a5b2252f7cd206c55ae9bf2fa763f8de84603aa60c" +checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7" dependencies = [ "libc", - "windows-sys 0.59.0", + "sigchld", + "windows-sys 0.60.2", ] [[package]] name = "shared_thread" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7a6f98357c6bb0ebace19b22220e5543801d9de90ffe77f8abb27c056bac064" +checksum = "52b86057fcb5423f5018e331ac04623e32d6b5ce85e33300f92c79a1973928b0" [[package]] name = "shell-words" @@ -8010,6 +8646,27 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "sigchld" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1" +dependencies = [ + "libc", + "os_pipe", + "signal-hook 0.3.18", +] + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + [[package]] name = "signal-hook" version = "0.4.1" @@ -8037,7 +8694,7 @@ checksum = "e513e435a8898a0002270f29d0a708b7879708fb5c4d00e46983ca2d2d378cf0" dependencies = [ "futures-core", "libc", - "signal-hook", + "signal-hook 0.4.1", "tokio", ] @@ -8057,6 +8714,16 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + [[package]] name = "simd_helpers" version = "0.1.0" @@ -8114,6 +8781,25 @@ dependencies = [ "autocfg", ] +[[package]] +name = "slh-dsa" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd2f20f4049197e03db1104a6452f4d9e96665d79f880198dce4a7026ba5f267" +dependencies = [ + "const-oid", + "digest", + "hmac", + "hybrid-array 0.3.1", + "pkcs8", + "rand_core 0.6.4", + "sha2", + "sha3", + "signature", + "typenum", + "zerocopy", +] + [[package]] name = "slotmap" version = "1.0.7" @@ -8135,6 +8821,43 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" +[[package]] +name = "smithay-client-toolkit" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" +dependencies = [ + "bitflags 2.7.0", + "calloop", + "calloop-wayland-source", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 1.0.7", + "thiserror 2.0.18", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-experimental", + "wayland-protocols-misc", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smol_str" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa7368fcf4852a4c2dd92df0cace6a71f2091ca0a23391ce7f3a31833f1523" +dependencies = [ + "borsh", + "serde_core", +] + [[package]] name = "snafu" version = "0.7.5" @@ -8143,7 +8866,16 @@ checksum = "e4de37ad025c587a29e8f3f5605c00f70b98715ef90b9061a815b9e59e9042d6" dependencies = [ "backtrace", "doc-comment", - "snafu-derive", + "snafu-derive 0.7.5", +] + +[[package]] +name = "snafu" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e84b3f4eacbf3a1ce05eac6763b4d629d60cbc94d632e4092c54ade71f1e1a2" +dependencies = [ + "snafu-derive 0.8.9", ] [[package]] @@ -8158,6 +8890,18 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "snafu-derive" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1c97747dbf44bb1ca44a561ece23508e99cb592e862f22222dcf42f51d1e451" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "socket2" version = "0.5.8" @@ -8197,7 +8941,7 @@ checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08" dependencies = [ "bytemuck", "cfg_aliases", - "core-graphics", + "core-graphics 0.24.0", "foreign-types 0.5.0", "js-sys", "log", @@ -8223,7 +8967,7 @@ dependencies = [ "http 1.3.1", "httparse", "log", - "rand 0.8.5", + "rand 0.8.6", "sha1", ] @@ -8352,15 +9096,39 @@ dependencies = [ ] [[package]] -name = "string_cache_codegen" -version = "0.5.2" +name = "string_cache" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" dependencies = [ - "phf_generator 0.10.0", - "phf_shared 0.10.0", - "proc-macro2", - "quote", + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.13.1", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro2", + "quote", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", ] [[package]] @@ -8369,6 +9137,27 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "sublime_fuzzy" version = "0.7.0" @@ -8589,35 +9378,35 @@ dependencies = [ [[package]] name = "tao" -version = "0.34.5" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7" +checksum = "1cf65722394c2ac443e80120064987f8914ee1d4e4e36e63cdf10f2990f01159" dependencies = [ "bitflags 2.7.0", - "block2 0.6.0", + "block2 0.6.2", "core-foundation 0.10.0", - "core-graphics", + "core-graphics 0.25.0", "crossbeam-channel", - "dispatch", + "dbus", + "dispatch2", "dlopen2", "dpi", "gdkwayland-sys", "gdkx11-sys", "gtk", - "jni", - "lazy_static", + "jni 0.21.1", "libc", "log", "ndk", - "ndk-context", "ndk-sys", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-app-kit", - "objc2-foundation 0.3.0", + "objc2-foundation 0.3.2", + "objc2-ui-kit", "once_cell", "parking_lot", + "percent-encoding", "raw-window-handle", - "scopeguard", "tao-macros", "unicode-segmentation", "url", @@ -8646,9 +9435,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tar" -version = "0.4.43" +version = "0.4.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c65998313f8e17d0d553d28f91a0df93e4dbbbf770279c7bc21ca0f09ea1a1f6" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" dependencies = [ "filetime", "libc", @@ -8663,7 +9452,7 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.10.3" +version = "2.11.2" dependencies = [ "anyhow", "bytes", @@ -8680,14 +9469,14 @@ dependencies = [ "http 1.3.1", "http-range", "image", - "jni", + "jni 0.21.1", "libc", "log", "mime", "muda", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-app-kit", - "objc2-foundation 0.3.0", + "objc2-foundation 0.3.2", "objc2-ui-kit", "objc2-web-kit", "percent-encoding", @@ -8696,7 +9485,7 @@ dependencies = [ "quickcheck", "quickcheck_macros", "raw-window-handle", - "reqwest 0.13.1", + "reqwest 0.13.3", "rustls 0.23.35", "serde", "serde_json", @@ -8711,7 +9500,7 @@ dependencies = [ "tauri-runtime-cef", "tauri-runtime-wry", "tauri-utils", - "thiserror 2.0.12", + "thiserror 2.0.18", "tokio", "tracing", "tray-icon", @@ -8725,7 +9514,7 @@ dependencies = [ [[package]] name = "tauri-build" -version = "2.5.6" +version = "2.6.2" dependencies = [ "anyhow", "cargo_toml", @@ -8741,13 +9530,12 @@ dependencies = [ "tauri-codegen", "tauri-utils", "tauri-winres", - "toml 0.9.10+spec-1.1.0", "walkdir", ] [[package]] name = "tauri-bundler" -version = "2.8.1" +version = "2.9.2" dependencies = [ "anyhow", "ar", @@ -8779,7 +9567,7 @@ dependencies = [ "tauri-macos-sign", "tauri-utils", "tempfile", - "thiserror 2.0.12", + "thiserror 2.0.18", "time", "ureq", "url", @@ -8793,7 +9581,7 @@ dependencies = [ [[package]] name = "tauri-cli" -version = "2.10.1" +version = "2.11.2" dependencies = [ "ar", "axum", @@ -8807,7 +9595,7 @@ dependencies = [ "ctrlc", "dialoguer", "dirs 6.0.0", - "download-cef 2.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "download-cef", "duct", "dunce", "elf", @@ -8815,7 +9603,6 @@ dependencies = [ "glob", "handlebars", "heck 0.5.0", - "html5ever", "ignore", "image", "include_dir", @@ -8828,7 +9615,6 @@ dependencies = [ "jsonrpsee-core", "jsonrpsee-ws-client", "jsonschema", - "kuchikiki", "libc", "local-ip-address", "log", @@ -8844,10 +9630,10 @@ dependencies = [ "oxc_ast", "oxc_parser", "oxc_span", - "phf 0.11.3", + "phf 0.13.1", "plist", "pretty_assertions", - "rand 0.9.1", + "rand 0.9.4", "rayon", "regex", "resvg", @@ -8862,10 +9648,10 @@ dependencies = [ "tauri-macos-sign", "tauri-utils", "tempfile", - "thiserror 2.0.12", + "thiserror 2.0.18", "tokio", - "toml 0.9.10+spec-1.1.0", - "toml_edit 0.24.0+spec-1.1.0", + "toml 1.0.6+spec-1.1.0", + "toml_edit 0.25.4+spec-1.1.0", "ureq", "url", "uuid", @@ -8883,19 +9669,21 @@ dependencies = [ "napi", "napi-build", "napi-derive", + "napi-derive-backend", + "napi-sys", "tauri-cli", ] [[package]] name = "tauri-codegen" -version = "2.5.5" +version = "2.6.2" dependencies = [ "base64 0.22.1", "brotli", "ico", "json-patch", "plist", - "png", + "png 0.17.16", "proc-macro2", "quote", "semver", @@ -8904,7 +9692,7 @@ dependencies = [ "sha2", "syn 2.0.117", "tauri-utils", - "thiserror 2.0.12", + "thiserror 2.0.18", "time", "url", "uuid", @@ -8913,7 +9701,7 @@ dependencies = [ [[package]] name = "tauri-driver" -version = "2.0.5" +version = "2.0.6" dependencies = [ "anyhow", "futures", @@ -8924,7 +9712,7 @@ dependencies = [ "pico-args", "serde", "serde_json", - "signal-hook", + "signal-hook 0.4.1", "signal-hook-tokio", "tokio", "which", @@ -8949,12 +9737,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03b7eb4d0d43724ba9ba6a6717420ee68aee377816a3edbb45db8c18862b1431" dependencies = [ "byteorder", - "png", + "png 0.17.16", ] [[package]] name = "tauri-macos-sign" -version = "2.3.3" +version = "2.3.4" dependencies = [ "apple-codesign", "base64 0.22.1", @@ -8965,18 +9753,18 @@ dependencies = [ "os_pipe", "p12", "plist", - "rand 0.9.1", + "rand 0.9.4", "regex", "serde", "serde_json", "tempfile", - "thiserror 2.0.12", + "thiserror 2.0.18", "x509-certificate 0.23.1", ] [[package]] name = "tauri-macros" -version = "2.5.5" +version = "2.6.2" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -8988,7 +9776,7 @@ dependencies = [ [[package]] name = "tauri-plugin" -version = "2.5.4" +version = "2.6.2" dependencies = [ "anyhow", "glob", @@ -8997,7 +9785,6 @@ dependencies = [ "serde", "serde_json", "tauri-utils", - "toml 0.9.10+spec-1.1.0", "walkdir", ] @@ -9010,15 +9797,15 @@ dependencies = [ "byte-unit", "fern", "log", - "objc2 0.6.3", - "objc2-foundation 0.3.0", + "objc2 0.6.4", + "objc2-foundation 0.3.2", "serde", "serde_json", "serde_repr", "swift-rs", "tauri", "tauri-plugin", - "thiserror 2.0.12", + "thiserror 2.0.18", "time", ] @@ -9030,25 +9817,25 @@ dependencies = [ "serde", "tauri", "tauri-plugin", - "thiserror 2.0.12", + "thiserror 2.0.18", ] [[package]] name = "tauri-runtime" -version = "2.10.1" +version = "2.11.2" dependencies = [ "cookie", "dpi", "gtk", "http 1.3.1", - "jni", - "objc2 0.6.3", + "jni 0.21.1", + "objc2 0.6.4", "objc2-ui-kit", "raw-window-handle", "serde", "serde_json", "tauri-utils", - "thiserror 2.0.12", + "thiserror 2.0.18", "url", "windows 0.61.1", ] @@ -9059,16 +9846,19 @@ version = "0.1.0" dependencies = [ "base64 0.22.1", "cef", - "cef-dll-sys", "dioxus-debug-cell", "dirs 6.0.0", + "dlopen2", "gtk", - "html5ever", + "html5ever 0.29.1", "http 1.3.1", "kuchikiki", - "objc2 0.6.3", + "log", + "objc2 0.6.4", "objc2-app-kit", - "objc2-foundation 0.3.0", + "objc2-application-services", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", "raw-window-handle", "serde", "serde_json", @@ -9077,18 +9867,19 @@ dependencies = [ "tauri-utils", "url", "windows 0.61.1", + "winit", "x11-dl", ] [[package]] name = "tauri-runtime-wry" -version = "2.10.1" +version = "2.11.2" dependencies = [ "gtk", "http 1.3.1", - "jni", + "jni 0.21.1", "log", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-app-kit", "objc2-web-kit", "once_cell", @@ -9133,17 +9924,18 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "2.8.3" +version = "2.9.2" dependencies = [ "aes-gcm", "anyhow", "brotli", - "cargo_metadata", - "ctor 0.2.9", + "cargo_metadata 0.19.0", + "ctor 0.8.0", + "dom_query", "dunce", "getrandom 0.3.3", "glob", - "html5ever", + "html5ever 0.29.1", "http 1.3.1", "infer", "json-patch", @@ -9151,7 +9943,8 @@ dependencies = [ "kuchikiki", "log", "memchr", - "phf 0.11.3", + "phf 0.13.1", + "plist", "proc-macro2", "quote", "regex", @@ -9165,8 +9958,9 @@ dependencies = [ "serialize-to-javascript", "swift-rs", "tauri", - "thiserror 2.0.12", - "toml 0.9.10+spec-1.1.0", + "tempfile", + "thiserror 2.0.18", + "toml 1.0.6+spec-1.1.0", "url", "urlpattern", "uuid", @@ -9181,7 +9975,7 @@ checksum = "7c6d9028d41d4de835e3c482c677a8cb88137ac435d6ff9a71f392d4421576c9" dependencies = [ "embed-resource", "indexmap 2.11.4", - "toml 0.9.10+spec-1.1.0", + "toml 0.9.12+spec-1.1.0", ] [[package]] @@ -9220,6 +10014,16 @@ dependencies = [ "utf-8", ] +[[package]] +name = "tendril" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" +dependencies = [ + "new_debug_unreachable", + "utf-8", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -9262,11 +10066,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl 2.0.18", ] [[package]] @@ -9282,9 +10086,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -9346,7 +10150,7 @@ dependencies = [ "bytemuck", "cfg-if", "log", - "png", + "png 0.17.16", "tiny-skia-path", ] @@ -9361,19 +10165,6 @@ dependencies = [ "strict-num", ] -[[package]] -name = "tiny_http" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0d6ef4e10d23c1efb862eecad25c5054429a71958b4eeef85eb5e7170b477ca" -dependencies = [ - "ascii", - "chunked_transfer", - "log", - "time", - "url", -] - [[package]] name = "tinystr" version = "0.7.6" @@ -9510,9 +10301,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.10+spec-1.1.0" +version = "0.9.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0825052159284a1a8b4d6c0c86cbc801f2da5afd2b225fa548c72f2e74002f48" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ "indexmap 2.11.4", "serde_core", @@ -9523,6 +10314,21 @@ dependencies = [ "winnow 0.7.14", ] +[[package]] +name = "toml" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "399b1124a3c9e16766831c6bba21e50192572cdd98706ea114f9502509686ffc" +dependencies = [ + "indexmap 2.11.4", + "serde_core", + "serde_spanned 1.0.4", + "toml_datetime 1.0.0+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.14", +] + [[package]] name = "toml_datetime" version = "0.6.8" @@ -9541,6 +10347,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_datetime" +version = "1.0.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.19.15" @@ -9578,14 +10393,14 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.24.0+spec-1.1.0" +version = "0.25.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c740b185920170a6d9191122cafef7010bd6270a3824594bff6784c04d7f09e" +checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2" dependencies = [ "indexmap 2.11.4", "serde_core", "serde_spanned 1.0.4", - "toml_datetime 0.7.5+spec-1.1.0", + "toml_datetime 1.0.0+spec-1.1.0", "toml_parser", "toml_writer", "winnow 0.7.14", @@ -9593,9 +10408,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.6+spec-1.1.0" +version = "1.0.9+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" dependencies = [ "winnow 0.7.14", ] @@ -9701,24 +10516,24 @@ dependencies = [ [[package]] name = "tray-icon" -version = "0.21.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2da75ec677957aa21f6e0b361df0daab972f13a5bee3606de0638fd4ee1c666a" +checksum = "e47e6d063cfe4ad2e416fcbb310be3a37c5fd85c745b62cb562bfa4a003df674" dependencies = [ "crossbeam-channel", "dirs 6.0.0", "libappindicator", "muda", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-app-kit", "objc2-core-foundation", "objc2-core-graphics", - "objc2-foundation 0.3.0", + "objc2-foundation 0.3.2", "once_cell", - "png", + "png 0.18.1", "serde", - "thiserror 2.0.12", - "windows-sys 0.59.0", + "thiserror 2.0.18", + "windows-sys 0.61.1", ] [[package]] @@ -9748,7 +10563,7 @@ dependencies = [ "http 1.3.1", "httparse", "log", - "rand 0.8.5", + "rand 0.8.6", "rustls 0.22.4", "rustls-native-certs 0.7.3", "rustls-pki-types", @@ -9769,9 +10584,9 @@ dependencies = [ "http 1.3.1", "httparse", "log", - "rand 0.9.1", + "rand 0.9.4", "sha1", - "thiserror 2.0.12", + "thiserror 2.0.18", "utf-8", ] @@ -9802,9 +10617,9 @@ checksum = "0e13db2e0ccd5e14a544e8a246ba2312cd25223f616442d7f2cb0e3db614236e" [[package]] name = "typenum" -version = "1.17.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "typewit" @@ -10286,9 +11101,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.106" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" dependencies = [ "cfg-if", "once_cell", @@ -10299,22 +11114,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.56" +version = "0.4.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" dependencies = [ - "cfg-if", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.106" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -10322,9 +11134,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.106" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" dependencies = [ "bumpalo", "proc-macro2", @@ -10335,18 +11147,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.106" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" dependencies = [ "unicode-ident", ] [[package]] name = "wasm-streams" -version = "0.4.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" dependencies = [ "futures-util", "js-sys", @@ -10355,11 +11167,146 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wayland-backend" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d" +dependencies = [ + "cc", + "downcast-rs", + "rustix 1.0.7", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" +dependencies = [ + "bitflags 2.7.0", + "rustix 1.0.7", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.7.0", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" +dependencies = [ + "rustix 1.0.7", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "563a85523cade2429938e790815fd7319062103b9f4a2dc806e9b53b95982d8f" +dependencies = [ + "bitflags 2.7.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-experimental" +version = "20250721.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1" +dependencies = [ + "bitflags 2.7.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-misc" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9567599ef23e09b8dad6e429e5738d4509dfc46b3b21f32841a304d16b29c8" +dependencies = [ + "bitflags 2.7.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91" +dependencies = [ + "bitflags 2.7.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" +dependencies = [ + "bitflags 2.7.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" +dependencies = [ + "proc-macro2", + "quick-xml 0.39.4", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + [[package]] name = "web-sys" -version = "0.3.83" +version = "0.3.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" dependencies = [ "js-sys", "wasm-bindgen", @@ -10375,6 +11322,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web_atoms" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57a9779e9f04d2ac1ce317aee707aa2f6b773afba7b931222bff6983843b1576" +dependencies = [ + "phf 0.13.1", + "phf_codegen 0.13.1", + "string_cache 0.9.0", + "string_cache_codegen 0.6.1", +] + [[package]] name = "webkit2gtk" version = "2.0.2" @@ -10474,7 +11433,7 @@ version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c" dependencies = [ - "thiserror 2.0.12", + "thiserror 2.0.18", "windows 0.61.1", "windows-core 0.61.0", ] @@ -10549,10 +11508,10 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" dependencies = [ - "objc2 0.6.3", + "objc2 0.6.4", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation 0.3.0", + "objc2-foundation 0.3.2", "raw-window-handle", "windows-sys 0.59.0", "windows-version", @@ -10624,9 +11583,9 @@ dependencies = [ [[package]] name = "windows-implement" -version = "0.60.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", @@ -10635,9 +11594,9 @@ dependencies = [ [[package]] name = "windows-interface" -version = "0.59.1" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", @@ -10652,9 +11611,9 @@ checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" [[package]] name = "windows-link" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-numerics" @@ -10755,7 +11714,7 @@ version = "0.61.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f" dependencies = [ - "windows-link 0.2.0", + "windows-link 0.2.1", ] [[package]] @@ -11009,6 +11968,231 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +[[package]] +name = "winit" +version = "0.31.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2879d2854d1a43e48f67322d4bd097afcb6eb8f8f775c8de0260a71aea1df1aa" +dependencies = [ + "bitflags 2.7.0", + "cfg_aliases", + "cursor-icon", + "dpi", + "libc", + "raw-window-handle", + "rustix 1.0.7", + "smol_str", + "tracing", + "winit-android", + "winit-appkit", + "winit-common", + "winit-core", + "winit-orbital", + "winit-uikit", + "winit-wayland", + "winit-web", + "winit-win32", + "winit-x11", +] + +[[package]] +name = "winit-android" +version = "0.31.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d9c0d2cd93efec3a9f9ad819cfaf0834782403af7c0d248c784ec0c61761df" +dependencies = [ + "android-activity", + "bitflags 2.7.0", + "dpi", + "ndk", + "raw-window-handle", + "smol_str", + "tracing", + "winit-core", +] + +[[package]] +name = "winit-appkit" +version = "0.31.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21310ca07851a49c348e0c2cc768e36b52ca65afda2c2354d78ed4b90074d8aa" +dependencies = [ + "bitflags 2.7.0", + "block2 0.6.2", + "dispatch2", + "dpi", + "objc2 0.6.4", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-video", + "objc2-foundation 0.3.2", + "raw-window-handle", + "smol_str", + "tracing", + "winit-common", + "winit-core", +] + +[[package]] +name = "winit-common" +version = "0.31.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45375fbac4cbb77260d83a30b1f9d8105880dbac99a9ae97f56656694680ff69" +dependencies = [ + "memmap2", + "objc2 0.6.4", + "objc2-core-foundation", + "smol_str", + "tracing", + "winit-core", + "x11-dl", + "xkbcommon-dl", +] + +[[package]] +name = "winit-core" +version = "0.31.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4f0ccd7abb43740e2c6124ac7cae7d865ecec74eec63783e8922577ac232583" +dependencies = [ + "bitflags 2.7.0", + "cursor-icon", + "dpi", + "keyboard-types 0.8.3", + "raw-window-handle", + "smol_str", + "web-time", +] + +[[package]] +name = "winit-orbital" +version = "0.31.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51ea1fb262e7209f265f12bd0cc792c399b14355675e65531e9c8a87db287d46" +dependencies = [ + "bitflags 2.7.0", + "dpi", + "orbclient", + "raw-window-handle", + "redox_syscall", + "smol_str", + "tracing", + "winit-core", +] + +[[package]] +name = "winit-uikit" +version = "0.31.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680a356e798837d8eb274d4556e83bceaf81698194e31aafc5cfb8a9f2fab643" +dependencies = [ + "bitflags 2.7.0", + "block2 0.6.2", + "dispatch2", + "dpi", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-ui-kit", + "raw-window-handle", + "smol_str", + "tracing", + "winit-common", + "winit-core", +] + +[[package]] +name = "winit-wayland" +version = "0.31.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce5afb2ba07da603f84b722c95f9f9396d2cedae3944fb6c0cda4a6f88de545" +dependencies = [ + "ahash 0.8.11", + "bitflags 2.7.0", + "calloop", + "cursor-icon", + "dpi", + "libc", + "memmap2", + "raw-window-handle", + "rustix 1.0.7", + "sctk-adwaita", + "smithay-client-toolkit", + "smol_str", + "tracing", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-plasma", + "winit-common", + "winit-core", +] + +[[package]] +name = "winit-web" +version = "0.31.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c2490a953fb776fbbd5e295d54f1c3847f4f15b6c3929ec53c09acda6487a92" +dependencies = [ + "atomic-waker", + "bitflags 2.7.0", + "concurrent-queue", + "cursor-icon", + "dpi", + "js-sys", + "pin-project", + "raw-window-handle", + "smol_str", + "tracing", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "web-time", + "winit-core", +] + +[[package]] +name = "winit-win32" +version = "0.31.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "644ea78af0e858aa3b092e5d1c67c41995a98220c81813f1353b28bc8bb91eaa" +dependencies = [ + "bitflags 2.7.0", + "cursor-icon", + "dpi", + "raw-window-handle", + "smol_str", + "tracing", + "unicode-segmentation", + "windows-sys 0.59.0", + "winit-core", +] + +[[package]] +name = "winit-x11" +version = "0.31.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa5b600756534c7041aa93cd0d244d44b09fca1b89e202bd1cd80dd9f3636c46" +dependencies = [ + "bitflags 2.7.0", + "bytemuck", + "calloop", + "cursor-icon", + "dpi", + "libc", + "percent-encoding", + "raw-window-handle", + "rustix 1.0.7", + "smol_str", + "tracing", + "winit-common", + "winit-core", + "x11-dl", + "x11rb", + "xkbcommon-dl", +] + [[package]] name = "winnow" version = "0.5.40" @@ -11064,9 +12248,9 @@ dependencies = [ [[package]] name = "worker" -version = "0.7.1" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42c76c5889873a2c309365ad4503810c007d3c25fbb4e9fa9e4e23c4ceb3c7f2" +checksum = "4afd7ae4f7fcc11e0e5e64b964890b3dda90f1290b0612f7cd821b381cc18826" dependencies = [ "async-trait", "axum", @@ -11095,13 +12279,14 @@ dependencies = [ [[package]] name = "worker-macros" -version = "0.7.1" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62c62584d037bad33789a6a5d605b3fccea1c52de9251d06f9d44054170dc612" +checksum = "6371f41ac538c9f6dbe4d40cf7db58ed451eb0529a66f3e29ab8726217fc8a05" dependencies = [ "async-trait", "proc-macro2", "quote", + "strum", "syn 2.0.117", "wasm-bindgen", "wasm-bindgen-futures", @@ -11111,9 +12296,9 @@ dependencies = [ [[package]] name = "worker-sys" -version = "0.7.1" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72ddd412fd62c6eeffc1dd85e6ae5960a33b534f44a733df75b6e7519972bc74" +checksum = "4c8de95c532944cee89d63fa8d7945f3db6260ca75ee3da42f7acfeebf538e4c" dependencies = [ "cfg-if", "js-sys", @@ -11135,30 +12320,29 @@ checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" [[package]] name = "wry" -version = "0.54.0" +version = "0.55.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e456eeaf7f09413fdc16799782879b2b9f1d264dfdbce4cf7e924df0ef36afb9" +checksum = "3013fd6116aac351dd2e18f349b28b2cfef3a5ff3253a9d0ce2d7193bb1b4429" dependencies = [ "base64 0.22.1", - "block2 0.6.0", + "block2 0.6.2", "cookie", "crossbeam-channel", "dirs 6.0.0", + "dom_query", "dpi", "dunce", "gdkx11", "gtk", - "html5ever", "http 1.3.1", "javascriptcore-rs", - "jni", - "kuchikiki", + "jni 0.21.1", "libc", "ndk", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation 0.3.0", + "objc2-foundation 0.3.2", "objc2-ui-kit", "objc2-web-kit", "once_cell", @@ -11167,7 +12351,7 @@ dependencies = [ "sha2", "soup3", "tao-macros", - "thiserror 2.0.12", + "thiserror 2.0.18", "tracing", "url", "webkit2gtk", @@ -11209,6 +12393,28 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "libloading 0.8.9", + "once_cell", + "rustix 1.0.7", + "x11rb-protocol", + "xcursor", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + [[package]] name = "x25519-dalek" version = "2.0.1" @@ -11221,17 +12427,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "x448" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4cd07d4fae29e07089dbcacf7077cd52dce7760125ca9a4dd5a35ca603ffebb" -dependencies = [ - "ed448-goldilocks", - "hex", - "rand_core 0.5.1", -] - [[package]] name = "x509" version = "0.2.0" @@ -11291,6 +12486,31 @@ dependencies = [ "rustix 0.38.43", ] +[[package]] +name = "xcursor" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" + +[[package]] +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags 2.7.0", + "dlib", + "log", + "once_cell", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + [[package]] name = "xml-rs" version = "0.8.25" diff --git a/Cargo.toml b/Cargo.toml index 744d5ed1cbb3..24751061df9a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,4 +71,3 @@ tauri = { path = "./crates/tauri" } tauri-plugin = { path = "./crates/tauri-plugin" } tauri-utils = { path = "./crates/tauri-utils" } tauri-plugin-log = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "feat/cef" } -cef-dll-sys = { git = "https://github.com/tauri-apps/cef-rs", branch = "fix/146-location-windows" } diff --git a/cef-helper/Cargo.toml b/cef-helper/Cargo.toml index 45eac9bfafea..74ae3340292f 100644 --- a/cef-helper/Cargo.toml +++ b/cef-helper/Cargo.toml @@ -6,9 +6,9 @@ license = "Apache-2.0 OR MIT" publish = false [dependencies] -cef = { version = "=146.4.1", default-features = false } +cef = { version = "=148.0.0", default-features = false } # Not actually used directly, just locking it. -cef-dll-sys = { version = "=146.4.1", default-features = false } +cef-dll-sys = { version = "=148.0.0", default-features = false } [features] default = ["sandbox"] diff --git a/cef-helper/src/main.rs b/cef-helper/src/main.rs index 89002c24c431..aed6b8ebcacd 100644 --- a/cef-helper/src/main.rs +++ b/cef-helper/src/main.rs @@ -1,5 +1,122 @@ use cef::{args::Args, *}; +const IPC_MESSAGE_NAME: &str = "tauri:ipc"; +const IPC_POST_MESSAGE_FUNCTION: &str = "postMessage"; + +wrap_v8_handler! { + struct IpcPostMessageV8Handler; + + impl V8Handler { + fn execute( + &self, + name: Option<&CefString>, + _object: Option<&mut V8Value>, + arguments: Option<&[Option]>, + retval: Option<&mut Option>, + exception: Option<&mut CefString>, + ) -> std::os::raw::c_int { + let Some(name) = name else { + return 0; + }; + if name.to_string() != IPC_POST_MESSAGE_FUNCTION { + return 0; + } + + let Some(message) = arguments + .filter(|arguments| arguments.len() == 1) + .and_then(|arguments| arguments[0].as_ref()) + .filter(|argument| argument.is_string() != 0) + else { + if let Some(exception) = exception { + *exception = CefString::from("window.ipc.postMessage expects a string argument"); + } + return 1; + }; + + let Some(context) = v8_context_get_current_context() else { + return 1; + }; + let Some(frame) = context.frame() else { + return 1; + }; + + let body = CefString::from(&message.string_value()).to_string(); + let url = CefString::from(&frame.url()).to_string(); + let mut process_message = process_message_create(Some(&CefString::from(IPC_MESSAGE_NAME))); + if let Some(args) = process_message.as_ref().and_then(ProcessMessage::argument_list) { + args.set_string(0, Some(&CefString::from(url.as_str()))); + args.set_string(1, Some(&CefString::from(body.as_str()))); + frame.send_process_message(ProcessId::BROWSER, process_message.as_mut()); + } + + if let Some(retval) = retval { + *retval = v8_value_create_undefined(); + } + 1 + } + } +} + +fn install_ipc_post_message(context: Option<&mut V8Context>) { + let Some(window) = context.and_then(|context| context.global()) else { + return; + }; + + let attributes = sys::cef_v8_propertyattribute_t( + [ + sys::cef_v8_propertyattribute_t::V8_PROPERTY_ATTRIBUTE_READONLY, + sys::cef_v8_propertyattribute_t::V8_PROPERTY_ATTRIBUTE_DONTENUM, + sys::cef_v8_propertyattribute_t::V8_PROPERTY_ATTRIBUTE_DONTDELETE, + ] + .into_iter() + .fold(0, |acc, attr| acc | attr.0), + ) + .into(); + + let Some(mut ipc) = v8_value_create_object(None, None) else { + return; + }; + let mut handler = IpcPostMessageV8Handler::new(); + let post_message_name = CefString::from(IPC_POST_MESSAGE_FUNCTION); + let Some(mut post_message) = + v8_value_create_function(Some(&post_message_name), Some(&mut handler)) + else { + return; + }; + + ipc.set_value_bykey( + Some(&post_message_name), + Some(&mut post_message), + attributes, + ); + window.set_value_bykey(Some(&CefString::from("ipc")), Some(&mut ipc), attributes); +} + +wrap_render_process_handler! { + struct TauriRenderProcessHandler; + + impl RenderProcessHandler { + fn on_context_created( + &self, + _browser: Option<&mut Browser>, + _frame: Option<&mut Frame>, + context: Option<&mut V8Context>, + ) { + install_ipc_post_message(context); + } + } +} + +wrap_app! { + struct TauriRenderApp; + + impl App { + fn render_process_handler(&self) -> Option { + Some(TauriRenderProcessHandler::new()) + } + } +} + fn main() { let args = Args::new(); @@ -17,11 +134,11 @@ fn main() { loader }; + let _ = api_hash(sys::CEF_API_VERSION_LAST, 0); + let mut app = TauriRenderApp::new(); execute_process( Some(args.as_main_args()), - None::<&mut App>, + Some(&mut app), std::ptr::null_mut(), ); } - - diff --git a/crates/tauri-build/CHANGELOG.md b/crates/tauri-build/CHANGELOG.md index 121ffcf8668d..fb80c0cdf3ee 100644 --- a/crates/tauri-build/CHANGELOG.md +++ b/crates/tauri-build/CHANGELOG.md @@ -1,5 +1,40 @@ # Changelog +## \[2.6.2] + +### Dependencies + +- Upgraded to `tauri-utils@2.9.2` +- Upgraded to `tauri-codegen@2.6.2` + +## \[2.6.1] + +### Dependencies + +- Upgraded to `tauri-utils@2.9.1` +- Upgraded to `tauri-codegen@2.6.1` + +## \[2.6.0] + +### New Features + +- [`b7a0ff030`](https://www.github.com/tauri-apps/tauri/commit/b7a0ff03087a73fce6888361562faf9738afc829) ([#15263](https://www.github.com/tauri-apps/tauri/pull/15263)) Allow users to append extra `.rc` content by `append_rc_content` in `WindowsAttributes`. +- [`cc5c97602`](https://www.github.com/tauri-apps/tauri/commit/cc5c976027b0ab2431c13ec5b2e201d4414a8a6e) ([#14486](https://www.github.com/tauri-apps/tauri/pull/14486)) Implement file association for Android and iOS. + +### Enhancements + +- [`d730770bb`](https://www.github.com/tauri-apps/tauri/commit/d730770bb93d77358cfc6f1286f10187cef37362) ([#15117](https://www.github.com/tauri-apps/tauri/pull/15117)) Simplify async-sync code boundaries, no externally visible changes +- [`b3f2d12b8`](https://www.github.com/tauri-apps/tauri/commit/b3f2d12b89daefe528e562b93871db62f77973b9) ([#15289](https://www.github.com/tauri-apps/tauri/pull/15289)) Preserve a numeric semver build identifier such as `1.2.3+42` in the 4th segment of the Windows `FILEVERSION` fixed field when it fits in the Windows version format. + +### Bug Fixes + +- [`a30dca482`](https://www.github.com/tauri-apps/tauri/commit/a30dca4820d0ad681f04737a1819e6a6fab9fe84) ([#15288](https://www.github.com/tauri-apps/tauri/pull/15288)) Set the correct Windows `FileVersion` and `ProductVersion` string values using the version from the Tauri config. + +### Dependencies + +- Upgraded to `tauri-utils@2.9.0` +- Upgraded to `tauri-codegen@2.6.0` + ## \[2.5.6] ### Dependencies diff --git a/crates/tauri-build/Cargo.toml b/crates/tauri-build/Cargo.toml index c1c99bc32cc4..1d3829e3a160 100644 --- a/crates/tauri-build/Cargo.toml +++ b/crates/tauri-build/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-build" -version = "2.5.6" +version = "2.6.2" description = "build time code to pair with https://crates.io/crates/tauri" exclude = ["CHANGELOG.md", "/target"] readme = "README.md" @@ -26,9 +26,9 @@ targets = [ [dependencies] anyhow = "1" quote = { version = "1", optional = true } -tauri-codegen = { version = "2.5.5", path = "../tauri-codegen", optional = true } -tauri-utils = { version = "2.8.3", path = "../tauri-utils", features = [ - "build", +tauri-codegen = { version = "2.6.2", path = "../tauri-codegen", optional = true } +tauri-utils = { version = "2.9.2", path = "../tauri-utils", features = [ + "build-2", "resources", ] } cargo_toml = "0.22" @@ -41,7 +41,6 @@ tauri-winres = "0.3" semver = "1" dirs = "6" glob = "0.3" -toml = "0.9" schemars = { version = "1", features = ["preserve_order"] } [features] diff --git a/crates/tauri-build/src/acl.rs b/crates/tauri-build/src/acl.rs index 5e124a965e97..f536eeccd4c5 100644 --- a/crates/tauri-build/src/acl.rs +++ b/crates/tauri-build/src/acl.rs @@ -363,6 +363,11 @@ fn validate_capabilities( permission_name == "default" || manifest.permissions.contains_key(permission_name) || manifest.permission_sets.contains_key(permission_name) + // `allow-$command`/`deny-$command` permissions are materialized on demand from the commands list + // (the `allow-*`/`deny-*` wildcards are only available for the app manifest) + || manifest + .command_permission(permission_name, key == APP_ACL_KEY) + .is_some() }) .unwrap_or(false); @@ -383,6 +388,16 @@ fn validate_capabilities( for p in manifest.permission_sets.keys() { available_permissions.push(format!("{prefix}{p}")); } + for command in &manifest.commands { + let slug = command.replace('_', "-"); + available_permissions.push(format!("{prefix}allow-{slug}")); + available_permissions.push(format!("{prefix}deny-{slug}")); + } + // the `allow-*`/`deny-*` wildcards are only available for the app manifest + if key == APP_ACL_KEY && !manifest.commands.is_empty() { + available_permissions.push(format!("{prefix}allow-*")); + available_permissions.push(format!("{prefix}deny-*")); + } } anyhow::bail!( @@ -407,7 +422,8 @@ pub fn build(out_dir: &Path, target: Target, attributes: &Attributes) -> super:: )?; let has_app_manifest = app_acl.manifest.default_permission.is_some() || !app_acl.manifest.permission_sets.is_empty() - || !app_acl.manifest.permissions.is_empty(); + || !app_acl.manifest.permissions.is_empty() + || !app_acl.manifest.commands.is_empty(); if has_app_manifest { acl_manifests.insert(APP_ACL_KEY.into(), app_acl.manifest); } diff --git a/crates/tauri-build/src/lib.rs b/crates/tauri-build/src/lib.rs index 1516b337e1eb..5de4073b7f67 100644 --- a/crates/tauri-build/src/lib.rs +++ b/crates/tauri-build/src/lib.rs @@ -226,6 +226,8 @@ fn default_windows_app_manifest() -> &'static str { #[derive(Debug)] pub struct WindowsAttributes { window_icon_path: Option, + /// Whether to statically link the Visual C++ runtime into the application binary on Windows MSVC targets + static_vc_runtime: Option, /// A string containing an [application manifest] to be included with the application on Windows. /// /// Defaults to: @@ -253,6 +255,8 @@ pub struct WindowsAttributes { /// /// [application manifest]: https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests app_manifest: Option, + /// A series of strings containing additional .rc content to be appended to the generated resource file on Windows. + append_rc_content: Vec, } impl Default for WindowsAttributes { @@ -266,6 +270,8 @@ impl WindowsAttributes { pub fn new() -> Self { Self { window_icon_path: Default::default(), + static_vc_runtime: None, + append_rc_content: Vec::new(), app_manifest: Some(default_windows_app_manifest().into()), } } @@ -275,7 +281,9 @@ impl WindowsAttributes { pub fn new_without_app_manifest() -> Self { Self { app_manifest: None, + static_vc_runtime: None, window_icon_path: Default::default(), + append_rc_content: Vec::new(), } } @@ -289,6 +297,15 @@ impl WindowsAttributes { self } + /// Sets whether to statically link the Visual C++ runtime into the application binary on Windows MSVC targets. + /// + /// If unset, this is read from `build > windows > staticVCRuntime` in the Tauri configuration. + #[must_use] + pub fn static_vc_runtime(mut self, static_vc_runtime: bool) -> Self { + self.static_vc_runtime.replace(static_vc_runtime); + self + } + /// Sets the [application manifest] to be included with the application on Windows. /// /// Defaults to: @@ -345,6 +362,14 @@ impl WindowsAttributes { self.app_manifest = Some(manifest.as_ref().to_string()); self } + + /// Append additional .rc content to the generated resource file on Windows. + /// This can be called multiple times to append multiple contents. + #[must_use] + pub fn append_rc_content>(mut self, content: S) -> Self { + self.append_rc_content.push(content.into()); + self + } } /// The attributes used on the build. @@ -422,7 +447,8 @@ impl Attributes { } pub fn is_dev() -> bool { - env::var("DEP_TAURI_DEV").expect("missing `cargo:dev` instruction, please update tauri to latest") + env::var_os("DEP_TAURI_DEV") + .expect("missing `cargo:dev` instruction, please update tauri to latest") == "true" } @@ -471,7 +497,7 @@ pub fn try_build(attributes: Attributes) -> Result<()> { println!("cargo:rerun-if-env-changed=TAURI_CONFIG"); - let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap(); + let target_os = env::var_os("CARGO_CFG_TARGET_OS").unwrap(); let mobile = target_os == "ios" || target_os == "android"; cfg_alias("desktop", !mobile); cfg_alias("mobile", mobile); @@ -489,6 +515,7 @@ pub fn try_build(attributes: Attributes) -> Result<()> { json_patch::merge(&mut config, &merge_config); } let config: Config = serde_json::from_value(config)?; + let static_vc_runtime = should_static_link_vc_runtime(&config, &attributes); let s = config.identifier.split('.'); let last = s.clone().count() - 1; @@ -509,6 +536,11 @@ pub fn try_build(attributes: Attributes) -> Result<()> { if let Some(project_dir) = env::var_os("TAURI_ANDROID_PROJECT_PATH").map(PathBuf::from) { mobile::generate_gradle_files(project_dir)?; + + // Update Android manifest with file associations + if let Some(associations) = config.bundle.file_associations.as_ref() { + mobile::update_android_manifest_file_associations(associations)?; + } } cfg_alias("dev", is_dev()); @@ -516,7 +548,7 @@ pub fn try_build(attributes: Attributes) -> Result<()> { let cargo_toml_path = Path::new("Cargo.toml").canonicalize()?; let mut manifest = Manifest::::from_path_with_metadata(cargo_toml_path)?; - let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); + let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap()); manifest::check(&config, &mut manifest)?; @@ -596,8 +628,13 @@ pub fn try_build(attributes: Attributes) -> Result<()> { ); } - if !is_dev() && target_triple.contains("unknown-linux-gnu") { - // TODO: Only needed for CEF. + if target_triple.contains("unknown-linux-gnu") + && env::var("DEP_TAURI_RUNTIME").as_deref() == Ok("cef") + { + // The executable links against libcef.so, which sits next to it: the + // cef-dll-sys build script copies the CEF distribution into the cargo + // target directory for dev, and the bundler ships it alongside the binary + // in packages. `$ORIGIN` makes the loader look there in both cases. println!("cargo:rustc-link-arg=-Wl,-rpath,$ORIGIN"); } @@ -625,12 +662,18 @@ pub fn try_build(attributes: Attributes) -> Result<()> { res.set_manifest(&manifest); } + for content in attributes.windows_attributes.append_rc_content { + res.append_rc_content(&content); + } + if let Some(version_str) = &config.version && let Ok(v) = Version::parse(version_str) { - let version = (v.major << 48) | (v.minor << 32) | (v.patch << 16); + let version = to_winres_version(&v); res.set_version_info(VersionInfo::FILEVERSION, version); res.set_version_info(VersionInfo::PRODUCTVERSION, version); + res.set("FileVersion", version_str); + res.set("ProductVersion", version_str); } if let Some(product_name) = &config.product_name { @@ -699,10 +742,8 @@ pub fn try_build(attributes: Attributes) -> Result<()> { } } } - "msvc" => { - if env::var("STATIC_VCRUNTIME").is_ok_and(|v| v == "true") { - static_vcruntime::build(); - } + "msvc" if static_vc_runtime => { + static_vcruntime::build(); } _ => (), } @@ -715,3 +756,150 @@ pub fn try_build(attributes: Attributes) -> Result<()> { Ok(()) } + +fn to_winres_version(v: &semver::Version) -> u64 { + let build = v.build.parse::().map(u64::from).unwrap_or(0); + + (v.major << 48) | (v.minor << 32) | (v.patch << 16) | build +} + +fn should_static_link_vc_runtime(config: &Config, attributes: &Attributes) -> bool { + if let Some(value) = env::var_os("STATIC_VCRUNTIME") { + println!( + "cargo:warning=STATIC_VCRUNTIME is deprecated; use build.windows.staticVCRuntime in tauri.conf.json or tauri_build::WindowsAttributes::static_vc_runtime instead." + ); + value != "false" + } else { + attributes + .windows_attributes + .static_vc_runtime + .unwrap_or(config.build.windows.static_vc_runtime) + } +} + +#[cfg(test)] +mod tests { + use semver::Version; + + #[test] + fn version_uses_numeric_build_metadata() { + let version = Version::parse("1.2.3+42").unwrap(); + + assert_eq!( + crate::to_winres_version(&version), + (1 << 48) | (2 << 32) | (3 << 16) | 42 + ); + } + + #[test] + fn version_ignores_non_numeric_composite_build_metadata() { + let version = Version::parse("1.2.3+42.sha").unwrap(); + + assert_eq!( + crate::to_winres_version(&version), + (1 << 48) | (2 << 32) | (3 << 16) + ); + } + + #[test] + fn version_ignores_non_numeric_build_metadata() { + let version = Version::parse("1.2.3+abc").unwrap(); + + assert_eq!( + crate::to_winres_version(&version), + (1 << 48) | (2 << 32) | (3 << 16) + ); + } + + #[test] + fn version_ignores_build_metadata_that_does_not_fit_in_u16() { + let version = Version::parse("1.2.3+70000").unwrap(); + + assert_eq!( + crate::to_winres_version(&version), + (1 << 48) | (2 << 32) | (3 << 16) + ); + } + + #[test] + fn static_vc_runtime_chain() { + // 1. Nothing is set, should default to true + let config = tauri_utils::config::Config::default(); + let attributes = crate::Attributes::new(); + assert!(crate::should_static_link_vc_runtime(&config, &attributes)); + + // 2. Set to anything but "false" in env, should be true + unsafe { std::env::set_var("STATIC_VCRUNTIME", "qweqe") }; + let config = tauri_utils::config::Config::default(); + let attributes = crate::Attributes::new(); + assert!(crate::should_static_link_vc_runtime(&config, &attributes)); + unsafe { std::env::remove_var("STATIC_VCRUNTIME") }; + + // 3. Set to "false" in env, should be false + unsafe { std::env::set_var("STATIC_VCRUNTIME", "false") }; + let config = tauri_utils::config::Config::default(); + let attributes = crate::Attributes::new(); + assert!(!crate::should_static_link_vc_runtime(&config, &attributes)); + unsafe { std::env::remove_var("STATIC_VCRUNTIME") }; + + // 4. Set to true in attributes, should be true + let config = tauri_utils::config::Config::default(); + let attributes = crate::Attributes::new() + .windows_attributes(crate::WindowsAttributes::new().static_vc_runtime(true)); + assert!(crate::should_static_link_vc_runtime(&config, &attributes)); + + // 5. Set to false in attributes, should be false + let config = tauri_utils::config::Config::default(); + let attributes = crate::Attributes::new() + .windows_attributes(crate::WindowsAttributes::new().static_vc_runtime(false)); + assert!(!crate::should_static_link_vc_runtime(&config, &attributes)); + + // 6. Set to true in config, should be true + let config = tauri_utils::config::Config { + build: tauri_utils::config::BuildConfig { + windows: tauri_utils::config::WindowsBuildConfig { + static_vc_runtime: true, + }, + ..Default::default() + }, + ..Default::default() + }; + let attributes = crate::Attributes::new(); + assert!(crate::should_static_link_vc_runtime(&config, &attributes)); + + // 7. Set to false in config, should be false + let config = tauri_utils::config::Config { + build: tauri_utils::config::BuildConfig { + windows: tauri_utils::config::WindowsBuildConfig { + static_vc_runtime: false, + }, + ..Default::default() + }, + ..Default::default() + }; + let attributes = crate::Attributes::new(); + assert!(!crate::should_static_link_vc_runtime(&config, &attributes)); + + // 8. Set to true in config and false in attributes, should be false because attributes takes precedence over config + let config = tauri_utils::config::Config { + build: tauri_utils::config::BuildConfig { + windows: tauri_utils::config::WindowsBuildConfig { + static_vc_runtime: true, + }, + ..Default::default() + }, + ..Default::default() + }; + let attributes = crate::Attributes::new() + .windows_attributes(crate::WindowsAttributes::new().static_vc_runtime(false)); + assert!(!crate::should_static_link_vc_runtime(&config, &attributes)); + + // 9. Set to false in env and true in attributes, should be false because env takes precedence over attributes + unsafe { std::env::set_var("STATIC_VCRUNTIME", "false") }; + let config = tauri_utils::config::Config::default(); + let attributes = crate::Attributes::new() + .windows_attributes(crate::WindowsAttributes::new().static_vc_runtime(true)); + assert!(!crate::should_static_link_vc_runtime(&config, &attributes)); + unsafe { std::env::remove_var("STATIC_VCRUNTIME") }; + } +} diff --git a/crates/tauri-build/src/manifest.rs b/crates/tauri-build/src/manifest.rs index 9e3b4fa63b89..6a72e70d68dc 100644 --- a/crates/tauri-build/src/manifest.rs +++ b/crates/tauri-build/src/manifest.rs @@ -23,7 +23,7 @@ struct AllowlistedDependency { name: String, alias: Option, kind: DependencyKind, - all_cli_managed_features: Option>, + all_cli_managed_features: Vec<&'static str>, expected_features: Vec, } @@ -33,7 +33,7 @@ pub fn check(config: &Config, manifest: &mut Manifest) -> Result<()> { name: "tauri-build".into(), alias: None, kind: DependencyKind::Build, - all_cli_managed_features: Some(vec!["isolation"]), + all_cli_managed_features: vec!["isolation"], expected_features: match config.app.security.pattern { PatternKind::Isolation { .. } => vec!["isolation".to_string()], _ => vec![], @@ -43,12 +43,10 @@ pub fn check(config: &Config, manifest: &mut Manifest) -> Result<()> { name: "tauri".into(), alias: None, kind: DependencyKind::Normal, - all_cli_managed_features: Some( - AppConfig::all_features() - .into_iter() - .filter(|f| f != &"tray-icon") - .collect(), - ), + all_cli_managed_features: AppConfig::all_features() + .into_iter() + .filter(|f| f != &"tray-icon") + .collect(), expected_features: config .app .features() @@ -129,23 +127,13 @@ fn check_features(dependency: Dependency, metadata: &AllowlistedDependency) -> R Dependency::Inherited(dep) => dep.features, }; - let diff = if let Some(all_cli_managed_features) = &metadata.all_cli_managed_features { - features_diff( - &features - .into_iter() - .filter(|f| all_cli_managed_features.contains(&f.as_str())) - .collect::>(), - &metadata.expected_features, - ) - } else { - features_diff( - &features - .into_iter() - .filter(|f| f.starts_with("allow-")) - .collect::>(), - &metadata.expected_features, - ) - }; + let diff = features_diff( + &features + .into_iter() + .filter(|f| metadata.all_cli_managed_features.contains(&f.as_str())) + .collect::>(), + &metadata.expected_features, + ); let mut error_message = String::new(); if !diff.remove.is_empty() { diff --git a/crates/tauri-build/src/mobile.rs b/crates/tauri-build/src/mobile.rs index 9acef2e917be..80f0613db8e4 100644 --- a/crates/tauri-build/src/mobile.rs +++ b/crates/tauri-build/src/mobile.rs @@ -2,10 +2,147 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use std::path::PathBuf; +use std::{collections::HashSet, path::PathBuf}; use anyhow::{Context, Result}; -use tauri_utils::write_if_changed; +use tauri_utils::{config::AndroidIntentAction, write_if_changed}; + +/// Updates the Android manifest to add file association intent filters +pub fn update_android_manifest_file_associations( + associations: &[tauri_utils::config::FileAssociation], +) -> Result<()> { + if associations.is_empty() { + return Ok(()); + } + + let intent_filters = generate_file_association_intent_filters(associations); + tauri_utils::build::update_android_manifest("tauri-file-associations", "activity", intent_filters) +} + +fn generate_file_association_intent_filters( + associations: &[tauri_utils::config::FileAssociation], +) -> String { + let mut filters = String::new(); + + for association in associations { + // Get mime types - use explicit mime_type, or infer from extensions + let mut mime_types = HashSet::new(); + + if let Some(mime_type) = &association.mime_type { + mime_types.insert(( + mime_type.clone(), + association.android_intent_action_filters.clone(), + )); + } else { + // Infer mime types from extensions + for ext in &association.ext { + if let Some(mime) = extension_to_mime_type(&ext.0) { + mime_types.insert((mime, association.android_intent_action_filters.clone())); + } + } + } + + // If we have mime types, create intent filters + if !mime_types.is_empty() { + for (mime_type, actions) in &mime_types { + filters.push_str("\n"); + if let Some(actions) = actions { + for action in actions { + let action = match action { + AndroidIntentAction::Send => "SEND", + AndroidIntentAction::SendMultiple => "SEND_MULTIPLE", + AndroidIntentAction::View => "VIEW", + _ => unimplemented!(), + }; + filters.push_str(&format!( + " \n" + )); + } + } else { + filters.push_str(" \n"); + filters.push_str(" \n"); + filters.push_str(" \n"); + } + filters.push_str(" \n"); + filters.push_str(" \n"); + filters.push_str(&format!( + " \n", + mime_type + )); + + // Add file scheme and path patterns for extensions + if !association.ext.is_empty() { + // Create path patterns for each extension + // Android's pathPattern needs \\. (double backslash-dot) in XML to match a literal dot + let path_patterns: Vec = association + .ext + .iter() + .map(|ext| format!(".*\\\\.{}", ext.0)) + .collect(); + + for pattern in &path_patterns { + filters.push_str(&format!( + " \n", + pattern + )); + } + } + + filters.push_str("\n"); + } + } else if !association.ext.is_empty() { + // If no mime type but we have extensions, use a generic approach + filters.push_str("\n"); + filters.push_str(" \n"); + filters.push_str(" \n"); + filters.push_str(" \n"); + + for ext in &association.ext { + // Android's pathPattern needs \\. (double backslash-dot) in XML to match a literal dot + filters.push_str(&format!( + " \n", + ext.0 + )); + } + + filters.push_str("\n"); + } + } + + filters +} + +fn extension_to_mime_type(ext: &str) -> Option { + Some( + match ext.to_lowercase().as_str() { + "png" => "image/png", + "jpg" | "jpeg" => "image/jpeg", + "gif" => "image/gif", + "bmp" => "image/bmp", + "webp" => "image/webp", + "svg" => "image/svg+xml", + "ico" => "image/x-icon", + "tiff" | "tif" => "image/tiff", + "heic" | "heif" => "image/heic", + "mp4" => "video/mp4", + "mov" => "video/quicktime", + "avi" => "video/x-msvideo", + "mkv" => "video/x-matroska", + "mp3" => "audio/mpeg", + "wav" => "audio/wav", + "aac" => "audio/aac", + "m4a" => "audio/mp4", + "pdf" => "application/pdf", + "txt" => "text/plain", + "html" | "htm" => "text/html", + "json" => "application/json", + "xml" => "application/xml", + "rtf" => "application/rtf", + _ => return None, + } + .to_string(), + ) +} pub fn generate_gradle_files(project_dir: PathBuf) -> Result<()> { let gradle_settings_path = project_dir.join("tauri.settings.gradle"); @@ -15,7 +152,8 @@ pub fn generate_gradle_files(project_dir: PathBuf) -> Result<()> { "// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.\n".to_string(); let mut app_build_gradle = "// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. val implementation by configurations -dependencies {" +dependencies { + implementation(\"androidx.lifecycle:lifecycle-process:2.10.0\")" .to_string(); for (env, value) in std::env::vars_os() { diff --git a/crates/tauri-bundler/CHANGELOG.md b/crates/tauri-bundler/CHANGELOG.md index b2fec9ab6a18..c86a769b304b 100644 --- a/crates/tauri-bundler/CHANGELOG.md +++ b/crates/tauri-bundler/CHANGELOG.md @@ -1,5 +1,68 @@ # Changelog +## \[2.9.2] + +### Dependencies + +- Upgraded to `tauri-utils@2.9.2` + +## \[2.9.1] + +### Dependencies + +- Upgraded to `tauri-utils@2.9.1` + +## \[2.9.0] + +### New Features + +- [`926a57bb0`](https://www.github.com/tauri-apps/tauri/commit/926a57bb0851e45d47ad1ee68fc96a9c25754c7c) ([#15201](https://www.github.com/tauri-apps/tauri/pull/15201)) Added uninstaller icon and uninstaller header image support for NSIS installer. + + Notes: + + - For `tauri-bundler` lib users, the `NsisSettings` now has 2 new fields `uninstaller_icon` and `uninstaller_header_image` which can be a breaking change + - When bundling with NSIS, users can add `uninstallerIcon` and `uninstallerHeaderImage` under `bundle > windows > nsis` to configure them. +- [`cc5c97602`](https://www.github.com/tauri-apps/tauri/commit/cc5c976027b0ab2431c13ec5b2e201d4414a8a6e) ([#14486](https://www.github.com/tauri-apps/tauri/pull/14486)) Implement file association for Android and iOS. +- [`5a0ca7edb`](https://www.github.com/tauri-apps/tauri/commit/5a0ca7edbbc707199615a91845146e98b6f5e8ca) ([#14671](https://www.github.com/tauri-apps/tauri/pull/14671)) Added support to Liquid Glass icons. +- [`5dc2cee60`](https://www.github.com/tauri-apps/tauri/commit/5dc2cee60370665af88c185684432e425b1c987d) ([#14793](https://www.github.com/tauri-apps/tauri/pull/14793)) Added support for `minimumWebview2Version` option support for the MSI (Wix) installer, the old `bundle > windows > nsis > minimumWebview2Version` is now deprecated in favor of `bundle > windows > minimumWebview2Version` + + Notes: + + - For anyone relying on the `WVRTINSTALLED` `Property` tag in `main.wxs`, it is now renamed to `INSTALLED_WEBVIEW2_VERSION` + - For `tauri-bundler` lib users, the `WindowsSettings` now has a new field `minimum_webview2_version` which can be a breaking change + +### Enhancements + +- [`be0e4bd2d`](https://www.github.com/tauri-apps/tauri/commit/be0e4bd2da02eb6cc75a8dc7c81663277e64c590) ([#15218](https://www.github.com/tauri-apps/tauri/pull/15218)) Added Vietnamese translations for the NSIS installer +- [`1035f12ee`](https://www.github.com/tauri-apps/tauri/commit/1035f12eeb8b23d9780881606d442d11c786e39e) ([#14923](https://www.github.com/tauri-apps/tauri/pull/14923)) Signtool path for windows arm systems was not being properly returned which caused failure in signing of windows binaries. + + This patch addresses it. + + Previously only the following were supported: + + - PROCESSOR_ARCHITECTURE_INTEL + - PROCESSOR_ARCHITECTURE_AMD64 + + The following were added: + + - PROCESSOR_ARCHITECTURE_ARM + - PROCESSOR_ARCHITECTURE_ARM64 + +### Bug Fixes + +- [`fcb702ec4`](https://www.github.com/tauri-apps/tauri/commit/fcb702ec4d924e81943efaeebea8d3edb7289c33) ([#14954](https://www.github.com/tauri-apps/tauri/pull/14954)) Fix `build --bundles` to allow `nsis` arg in linux+macOS +- [`c8d7003b2`](https://www.github.com/tauri-apps/tauri/commit/c8d7003b23657019a547fd7cdf3164834a28849a) ([#15102](https://www.github.com/tauri-apps/tauri/pull/15102)) Correct GitHub Release URL path for Linux i686 tooling. + +### What's Changed + +- [`9979cde1c`](https://www.github.com/tauri-apps/tauri/commit/9979cde1c5534dafb1a07cc4dc2bc280d15d2f66) ([#15175](https://www.github.com/tauri-apps/tauri/pull/15175)) Update NSIS installer Italian translations + +### Dependencies + +- Upgraded to `tauri-macos-sign@2.3.4` +- Upgraded to `tauri-utils@2.9.0` +- [`373b7e677`](https://www.github.com/tauri-apps/tauri/commit/373b7e677ec498899759de9fcd35941fe792b58b) ([#15177](https://www.github.com/tauri-apps/tauri/pull/15177)) Update Specta in lockfile and upgrade dependencies using the removed `doc_auto_cfg` attribute to fix errors building documentation + ## \[2.8.1] ### Bug Fixes diff --git a/crates/tauri-bundler/Cargo.toml b/crates/tauri-bundler/Cargo.toml index b8dc1373d282..d21bc25cf312 100644 --- a/crates/tauri-bundler/Cargo.toml +++ b/crates/tauri-bundler/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-bundler" -version = "2.8.1" +version = "2.9.2" authors = [ "George Burton ", "Tauri Programme within The Commons Conservancy", @@ -15,7 +15,7 @@ rust-version = "1.88" exclude = ["CHANGELOG.md", "/target", "rustfmt.toml"] [dependencies] -tauri-utils = { version = "2.8.3", path = "../tauri-utils", features = [ +tauri-utils = { version = "2.9.2", path = "../tauri-utils", features = [ "resources", ] } image = "0.25" @@ -45,12 +45,11 @@ uuid = { version = "1", features = ["v4", "v5"] } regex = "1" goblin = "0.9" plist = "1" - +glob = "0.3" [target."cfg(target_os = \"windows\")".dependencies] bitness = "0.4" windows-registry = "0.5" -glob = "0.3" [target."cfg(target_os = \"windows\")".dependencies.windows-sys] version = "0.60" @@ -59,13 +58,20 @@ features = ["Win32_System_SystemInformation", "Win32_System_Diagnostics_Debug"] [target."cfg(target_os = \"macos\")".dependencies] icns = { package = "tauri-icns", version = "0.1" } time = { version = "0.3", features = ["formatting"] } -tauri-macos-sign = { version = "2.3.3", path = "../tauri-macos-sign" } +tauri-macos-sign = { version = "2.3.4", path = "../tauri-macos-sign" } [target."cfg(target_os = \"linux\")".dependencies] heck = "0.5" ar = "0.9" md5 = "0.8" -rpm = { version = "0.16", features = ["bzip2-compression"] } +# TODO: xz-compression's `lzma` conflicts with apple-codesign's `xz` +rpm = { version = "0.25.1", default-features = false, features = [ + "gzip-compression", + "payload", + "signature-pgp", + "zstd-compression", + "bzip2-compression", +] } [target."cfg(unix)".dependencies] which = "8" diff --git a/crates/tauri-bundler/src/bundle.rs b/crates/tauri-bundler/src/bundle.rs index d1166338cacf..10959d4787c7 100644 --- a/crates/tauri-bundler/src/bundle.rs +++ b/crates/tauri-bundler/src/bundle.rs @@ -4,7 +4,6 @@ // SPDX-License-Identifier: MIT mod category; -#[cfg(any(target_os = "linux", target_os = "windows"))] mod kmp; #[cfg(target_os = "linux")] mod linux; @@ -17,26 +16,16 @@ mod windows; use tauri_utils::{display_path, platform::Target as TargetPlatform}; -#[cfg(any(target_os = "linux", target_os = "windows"))] const BUNDLE_VAR_TOKEN: &[u8] = b"__TAURI_BUNDLE_TYPE_VAR_UNK"; /// Patch a binary with bundle type information -#[cfg(any(target_os = "linux", target_os = "windows"))] fn patch_binary(binary: &PathBuf, package_type: &PackageType) -> crate::Result<()> { - log::info!( - "Patching {} with bundle type information: {}", - display_path(binary), - package_type.short_name() - ); - - let mut file_data = std::fs::read(binary).expect("Could not read binary file."); - - let bundle_var_index = - kmp::index_of(BUNDLE_VAR_TOKEN, &file_data).ok_or(crate::Error::MissingBundleTypeVar)?; #[cfg(target_os = "linux")] let bundle_type = match package_type { crate::PackageType::Deb => b"__TAURI_BUNDLE_TYPE_VAR_DEB", crate::PackageType::Rpm => b"__TAURI_BUNDLE_TYPE_VAR_RPM", crate::PackageType::AppImage => b"__TAURI_BUNDLE_TYPE_VAR_APP", + // NSIS installers can be built in linux using cargo-xwin + crate::PackageType::Nsis => b"__TAURI_BUNDLE_TYPE_VAR_NSS", _ => { return Err(crate::Error::InvalidPackageType( package_type.short_name().to_owned(), @@ -55,7 +44,31 @@ fn patch_binary(binary: &PathBuf, package_type: &PackageType) -> crate::Result<( )); } }; + #[cfg(target_os = "macos")] + let bundle_type = match package_type { + // NSIS installers can be built in macOS using cargo-xwin + crate::PackageType::Nsis => b"__TAURI_BUNDLE_TYPE_VAR_NSS", + crate::PackageType::MacOsBundle | crate::PackageType::Dmg => { + // skip patching for macOS-native bundles + return Ok(()); + } + _ => { + return Err(crate::Error::InvalidPackageType( + package_type.short_name().to_owned(), + "macOS".to_owned(), + )); + } + }; + log::info!( + "Patching {} with bundle type information: {}", + display_path(binary), + package_type.short_name() + ); + + let mut file_data = std::fs::read(binary).expect("Could not read binary file."); + let bundle_var_index = + kmp::index_of(BUNDLE_VAR_TOKEN, &file_data).ok_or(crate::Error::MissingBundleTypeVar)?; file_data[bundle_var_index..bundle_var_index + BUNDLE_VAR_TOKEN.len()] .copy_from_slice(bundle_type); @@ -135,7 +148,6 @@ pub fn bundle_project(settings: &Settings) -> crate::Result> { continue; } - #[cfg(any(target_os = "linux", target_os = "windows"))] if let Err(e) = patch_binary(&main_binary_path, package_type) { log::warn!( "Failed to add bundler type to the binary: {e}. Updater plugin may not be able to update this package. This shouldn't normally happen, please report it to https://github.com/tauri-apps/tauri/issues" @@ -167,7 +179,7 @@ pub fn bundle_project(settings: &Settings) -> crate::Result> { #[cfg(target_os = "windows")] PackageType::WindowsMsi => windows::msi::bundle_project(settings, false)?, - // note: don't restrict to windows as NSIS installers can be built in linux using cargo-xwin + // don't restrict to windows as NSIS installers can be built in linux+macOS using cargo-xwin PackageType::Nsis => windows::nsis::bundle_project(settings, false)?, #[cfg(target_os = "linux")] diff --git a/crates/tauri-bundler/src/bundle/kmp/mod.rs b/crates/tauri-bundler/src/bundle/kmp/mod.rs index 07df0c5901fa..da54a7645102 100644 --- a/crates/tauri-bundler/src/bundle/kmp/mod.rs +++ b/crates/tauri-bundler/src/bundle/kmp/mod.rs @@ -5,7 +5,6 @@ // Knuth-Morris-Pratt algorithm // based on https://github.com/howeih/rust_kmp -#[cfg(any(target_os = "linux", target_os = "windows"))] pub fn index_of(pattern: &[u8], target: &[u8]) -> Option { let failure_function = find_failure_function(pattern); @@ -38,7 +37,6 @@ pub fn index_of(pattern: &[u8], target: &[u8]) -> Option { None } -#[cfg(any(target_os = "linux", target_os = "windows"))] fn find_failure_function(pattern: &[u8]) -> Vec { let mut i = 1; let mut j = 0; diff --git a/crates/tauri-bundler/src/bundle/linux/appimage/linuxdeploy.rs b/crates/tauri-bundler/src/bundle/linux/appimage/linuxdeploy.rs index 36883081c18b..0014525d512f 100644 --- a/crates/tauri-bundler/src/bundle/linux/appimage/linuxdeploy.rs +++ b/crates/tauri-bundler/src/bundle/linux/appimage/linuxdeploy.rs @@ -232,7 +232,7 @@ fn prepare_tools(tools_path: &Path, arch: &str, verbose: bool) -> crate::Result< write_and_make_executable(&apprun, data)?; } - let linuxdeploy_arch = if arch == "i686" { "i383" } else { arch }; + let linuxdeploy_arch = if arch == "i686" { "i386" } else { arch }; let linuxdeploy = tools_path.join(format!("linuxdeploy-{linuxdeploy_arch}.AppImage")); if !linuxdeploy.exists() { let data = download(&format!( diff --git a/crates/tauri-bundler/src/bundle/linux/appimage/sharun_cef.rs b/crates/tauri-bundler/src/bundle/linux/appimage/sharun_cef.rs index 3c64244b7f68..743e85079a59 100644 --- a/crates/tauri-bundler/src/bundle/linux/appimage/sharun_cef.rs +++ b/crates/tauri-bundler/src/bundle/linux/appimage/sharun_cef.rs @@ -45,14 +45,14 @@ pub fn bundle_project(settings: &Settings) -> crate::Result> { fs::create_dir_all(&tools_path)?; - // TODO: mirror let quick_sharun = tools_path.join("quick-sharun.sh"); - if !quick_sharun.exists() { - let data = download( - "https://raw.githubusercontent.com/pkgforge-dev/Anylinux-AppImages/refs/heads/main/useful-tools/quick-sharun.sh", - )?; - write_and_make_executable(&quick_sharun, data)?; - } + // TODO: offline build support + // github doesn't send a Last-Modified header + // if !quick_sharun.exists() {} + let data = download( + "https://raw.githubusercontent.com/pkgforge-dev/Anylinux-AppImages/refs/heads/main/useful-tools/quick-sharun.sh", + )?; + write_and_make_executable(&quick_sharun, data)?; let package_dir = settings .project_out_directory() @@ -167,6 +167,13 @@ pub fn bundle_project(settings: &Settings) -> crate::Result> { .map(|b| format!(" \"{}\"", b.to_string_lossy())) .collect::(); + // quick-sharun checks the main binary with ldd so even though we manually add the cef files, + // we'll add them to LD_LIBRARY_PATH to pass the pre-bundle checks + let mut ld_lib_path = data_dir.join("usr/lib/").to_string_lossy().to_string(); + if let Ok(ld_env) = std::env::var("LD_LIBRARY_PATH") { + ld_lib_path = format!("{}:{}", ld_lib_path, ld_env); + } + // TODO: Consider to not rely on quick-sharun when we have more time Command::new("/bin/sh") .current_dir(&output_path) @@ -178,9 +185,10 @@ pub fn bundle_project(settings: &Settings) -> crate::Result> { ) .env("ICON", &larger_icon.path) .env("OUTPUT_APPIMAGE", "1") - .env("URUNTIME2APPIMAGE_SOURCE", "https://raw.githubusercontent.com/FabianLars/Anylinux-AppImages/refs/heads/main/useful-tools/uruntime2appimage.sh") + .env("HOOKSRC", "https://raw.githubusercontent.com/FabianLars/Anylinux-AppImages/refs/heads/main/useful-tools/hooks") .env("DEPLOY_CHROMIUM", "1") .env("ADD_HOOKS", "fix-namespaces.hook") + .env("LD_LIBRARY_PATH", ld_lib_path) .args([ "-c", &format!( diff --git a/crates/tauri-bundler/src/bundle/linux/rpm.rs b/crates/tauri-bundler/src/bundle/linux/rpm.rs index 5a0eb3027778..798cefcb1c78 100644 --- a/crates/tauri-bundler/src/bundle/linux/rpm.rs +++ b/crates/tauri-bundler/src/bundle/linux/rpm.rs @@ -5,10 +5,9 @@ use crate::{Settings, bundle::settings::Arch, error::ErrorExt, utils::CommandExt}; -use rpm::{self, Dependency, FileMode, FileOptions, signature::pgp}; +use rpm::{self, Dependency, FileOptions, signature::pgp}; use std::{ - env, - fs::{self, File}, + env, fs, path::{Path, PathBuf}, process::Command, }; @@ -16,6 +15,23 @@ use tauri_utils::config::RpmCompression; use super::freedesktop; +// https://docs.fedoraproject.org/en-US/packaging-guidelines/Versioning/ +// TODO: this may not cover it perfectly yet, it's just a hotfix for prerelease semver +fn to_rpm_version(version: &str) -> String { + match semver::Version::parse(version) { + Ok(v) if !v.pre.is_empty() => { + let pre = v.pre.as_str().replace('-', "."); + let mut rpm = format!("{}.{}.{}~{}", v.major, v.minor, v.patch, pre); + if !v.build.is_empty() { + rpm.push('+'); + rpm.push_str(v.build.as_str()); + } + rpm + } + _ => version.to_string(), + } +} + /// Bundles the project. /// Returns a vector of PathBuf that shows where the RPM was created. pub fn bundle_project(settings: &Settings) -> crate::Result> { @@ -72,26 +88,28 @@ pub fn bundle_project(settings: &Settings) -> crate::Result> { RpmCompression::Bzip2 { level } => rpm::CompressionWithLevel::Bzip2(level), _ => rpm::CompressionWithLevel::None, }) - // This matches .deb compression. On a 240MB source binary the bundle will be 100KB larger than rpm's default while reducing build times by ~25%. - // TODO: Default to Zstd in v3 to match rpm-rs new default in 0.16 - .unwrap_or(rpm::CompressionWithLevel::Gzip(6)); + .unwrap_or_default(); + + let build_config = rpm::BuildConfig::default().compression(compression); - let mut builder = rpm::PackageBuilder::new(&name, version, &license, arch, summary) + let mut builder = + rpm::PackageBuilder::new(&name, &to_rpm_version(version), &license, arch, summary); + builder + .using_config(build_config) .epoch(epoch) - .release(release) - .compression(compression); + .release(release); if let Some(description) = settings.long_description() { - builder = builder.description(description); + builder.description(description); } if let Some(homepage) = settings.homepage_url() { - builder = builder.url(homepage); + builder.url(homepage); } // Add requirements for dep in settings.rpm().depends.as_ref().cloned().unwrap_or_default() { - builder = builder.requires(Dependency::any(dep)); + builder.requires(Dependency::any(dep)); } // Add provides @@ -102,7 +120,7 @@ pub fn bundle_project(settings: &Settings) -> crate::Result> { .cloned() .unwrap_or_default() { - builder = builder.provides(Dependency::any(dep)); + builder.provides(Dependency::any(dep)); } // Add recommends @@ -113,7 +131,7 @@ pub fn bundle_project(settings: &Settings) -> crate::Result> { .cloned() .unwrap_or_default() { - builder = builder.recommends(Dependency::any(dep)); + builder.recommends(Dependency::any(dep)); } // Add conflicts @@ -124,7 +142,7 @@ pub fn bundle_project(settings: &Settings) -> crate::Result> { .cloned() .unwrap_or_default() { - builder = builder.conflicts(Dependency::any(dep)); + builder.conflicts(Dependency::any(dep)); } // Add obsoletes @@ -135,7 +153,7 @@ pub fn bundle_project(settings: &Settings) -> crate::Result> { .cloned() .unwrap_or_default() { - builder = builder.obsoletes(Dependency::any(dep)); + builder.obsoletes(Dependency::any(dep)); } // Add binaries @@ -147,17 +165,16 @@ pub fn bundle_project(settings: &Settings) -> crate::Result> { let cef_bin_dest = Path::new("/usr/share") .join(settings.product_name()) .join(bin.name()); - let empty_file_path = &package_dir.join("empty"); - File::create(empty_file_path)?; - builder = builder.with_file(src, FileOptions::new(cef_bin_dest.to_string_lossy()))?; - builder = builder.with_file( - empty_file_path, - FileOptions::new(dest.to_string_lossy()) - .symlink(cef_bin_dest.to_string_lossy().replace("/usr", "..")) - .mode(0o120555), + builder.with_file(src, FileOptions::new(cef_bin_dest.to_string_lossy()))?; + builder.with_symlink( + FileOptions::symlink( + dest.to_string_lossy(), + cef_bin_dest.to_string_lossy().replace("/usr", ".."), + ) + .mode(0o120555), )?; } else { - builder = builder.with_file(src, FileOptions::new(dest.to_string_lossy()))?; + builder.with_file(src, FileOptions::new(dest.to_string_lossy()))?; } } @@ -171,47 +188,39 @@ pub fn bundle_project(settings: &Settings) -> crate::Result> { .to_string_lossy() .replace(&format!("-{}", settings.target()), ""), ); - builder = builder.with_file(&src, FileOptions::new(dest.to_string_lossy()))?; + builder.with_file(&src, FileOptions::new(dest.to_string_lossy()))?; } // Add scripts if let Some(script_path) = &settings.rpm().pre_install_script { let script = fs::read_to_string(script_path)?; - builder = builder.pre_install_script(script); + builder.pre_install_script(script); } if let Some(script_path) = &settings.rpm().post_install_script { let script = fs::read_to_string(script_path)?; - builder = builder.post_install_script(script); + builder.post_install_script(script); } if let Some(script_path) = &settings.rpm().pre_remove_script { let script = fs::read_to_string(script_path)?; - builder = builder.pre_uninstall_script(script); + builder.pre_uninstall_script(script); } if let Some(script_path) = &settings.rpm().post_remove_script { let script = fs::read_to_string(script_path)?; - builder = builder.post_uninstall_script(script); + builder.post_uninstall_script(script); } // Add resources and/or prepare for CEF files if settings.resource_files().count() > 0 || settings.bundle_settings().cef_path.is_some() { let resource_dir = Path::new("/usr/lib").join(settings.product_name()); - // Create an empty file, needed to add a directory to the RPM package - // (cf https://github.com/rpm-rs/rpm/issues/177) - let empty_file_path = &package_dir.join("empty"); - File::create(empty_file_path)?; - // Then add the resource directory `/usr/lib/` to the package. - builder = builder.with_file( - empty_file_path, - FileOptions::new(resource_dir.to_string_lossy()).mode(FileMode::Dir { permissions: 0o755 }), - )?; + builder.with_dir_entry(FileOptions::dir(resource_dir.to_string_lossy()).permissions(0o755))?; // Then add the resources files in that directory for resource in settings.resource_files().iter() { let resource = resource?; let dest = resource_dir.join(resource.target()); - builder = builder.with_file(resource.path(), FileOptions::new(dest.to_string_lossy()))?; + builder.with_file(resource.path(), FileOptions::new(dest.to_string_lossy()))?; } } // Handle CEF support if cef_path is set, @@ -261,7 +270,7 @@ pub fn bundle_project(settings: &Settings) -> crate::Result> { if f == "chrome-sandbox" { fileopts = fileopts.mode(0o104755); } - builder = builder.with_file(temp_file, fileopts).unwrap(); + builder.with_file(temp_file, fileopts).unwrap(); } let locales = [ "en-US.pak", @@ -274,7 +283,7 @@ pub fn bundle_project(settings: &Settings) -> crate::Result> { let cef_resource_dir = cef_resource_dir.join("locales"); for f in locales { - builder = builder.with_file( + builder.with_file( cef_path.join(f), FileOptions::new(cef_resource_dir.join(f).to_string_lossy()), )?; @@ -284,27 +293,26 @@ pub fn bundle_project(settings: &Settings) -> crate::Result> { // Add Desktop entry file let (desktop_src_path, desktop_dest_path) = freedesktop::generate_desktop_file(settings, &settings.rpm().desktop_template, &package_dir)?; - builder = builder.with_file( + builder.with_file( desktop_src_path, FileOptions::new(desktop_dest_path.to_string_lossy()), )?; // Add icons for (icon, src) in &freedesktop::list_icon_files(settings, &PathBuf::from("/"))? { - builder = builder.with_file(src, FileOptions::new(icon.path.to_string_lossy()))?; + builder.with_file(src, FileOptions::new(icon.path.to_string_lossy()))?; } // Add custom files for (rpm_path, src_path) in settings.rpm().files.iter() { if src_path.is_file() { - builder = builder.with_file(src_path, FileOptions::new(rpm_path.to_string_lossy()))?; + builder.with_file(src_path, FileOptions::new(rpm_path.to_string_lossy()))?; } else { for entry in walkdir::WalkDir::new(src_path) { let entry_path = entry?.into_path(); if entry_path.is_file() { let dest_path = rpm_path.join(entry_path.strip_prefix(src_path).unwrap()); - builder = - builder.with_file(&entry_path, FileOptions::new(dest_path.to_string_lossy()))?; + builder.with_file(&entry_path, FileOptions::new(dest_path.to_string_lossy()))?; } } } @@ -313,7 +321,7 @@ pub fn bundle_project(settings: &Settings) -> crate::Result> { log::info!(action = "Bundling"; "Creating .rpm file..."); let pkg = if let Ok(raw_secret_key) = env::var("TAURI_SIGNING_RPM_KEY") { - let mut signer = pgp::Signer::load_from_asc(&raw_secret_key)?; + let mut signer = pgp::Signer::from_asc(&raw_secret_key)?; if let Ok(passphrase) = env::var("TAURI_SIGNING_RPM_KEY_PASSPHRASE") { signer = signer.with_key_passphrase(passphrase); } diff --git a/crates/tauri-bundler/src/bundle/macos/app.rs b/crates/tauri-bundler/src/bundle/macos/app.rs index ff3dbdf86b43..e5eee1d14f81 100644 --- a/crates/tauri-bundler/src/bundle/macos/app.rs +++ b/crates/tauri-bundler/src/bundle/macos/app.rs @@ -115,20 +115,6 @@ pub fn bundle_project(settings: &Settings) -> crate::Result> { settings.copy_resources(&resources_dir)?; - let bin_paths = settings - .copy_binaries(&bin_dir) - .with_context(|| "Failed to copy external binaries")?; - sign_paths.extend(bin_paths.into_iter().map(|path| SignTarget { - path, - is_an_executable: true, - })); - - let bin_paths = copy_binaries_to_bundle(&bundle_directory, settings)?; - sign_paths.extend(bin_paths.into_iter().map(|path| SignTarget { - path, - is_an_executable: true, - })); - copy_custom_files_to_bundle(&bundle_directory, settings)?; // Handle CEF support if cef_path is set @@ -148,6 +134,23 @@ pub fn bundle_project(settings: &Settings) -> crate::Result> { ); } + // Sign CEF nested code first (helper apps/framework internals), then top-level binaries. + // Signing the main executable before CEF helpers causes codesign to fail with: + // "code object is not signed at all ... In subcomponent: ... Helper (Renderer).app". + let bin_paths = settings + .copy_binaries(&bin_dir) + .with_context(|| "Failed to copy external binaries")?; + sign_paths.extend(bin_paths.into_iter().map(|path| SignTarget { + path, + is_an_executable: true, + })); + + let bin_paths = copy_binaries_to_bundle(&bundle_directory, settings)?; + sign_paths.extend(bin_paths.into_iter().map(|path| SignTarget { + path, + is_an_executable: true, + })); + if settings.no_sign() { log::warn!("Skipping signing due to --no-sign flag.",); } else if let Some(keychain) = @@ -297,103 +300,14 @@ fn create_info_plist( plist.insert("LSMinimumSystemVersion".into(), version.into()); } - if let Some(associations) = settings.file_associations() { - let exported_associations = associations - .iter() - .filter_map(|association| { - association.exported_type.as_ref().map(|exported_type| { - let mut dict = plist::Dictionary::new(); - - dict.insert( - "UTTypeIdentifier".into(), - exported_type.identifier.clone().into(), - ); - if let Some(description) = &association.description { - dict.insert("UTTypeDescription".into(), description.clone().into()); - } - if let Some(conforms_to) = &exported_type.conforms_to { - dict.insert( - "UTTypeConformsTo".into(), - plist::Value::Array(conforms_to.iter().map(|s| s.clone().into()).collect()), - ); - } - - let mut specification = plist::Dictionary::new(); - specification.insert( - "public.filename-extension".into(), - plist::Value::Array( - association - .ext - .iter() - .map(|s| s.to_string().into()) - .collect(), - ), - ); - if let Some(mime_type) = &association.mime_type { - specification.insert("public.mime-type".into(), mime_type.clone().into()); - } - - dict.insert("UTTypeTagSpecification".into(), specification.into()); - - plist::Value::Dictionary(dict) - }) - }) - .collect::>(); - - if !exported_associations.is_empty() { - plist.insert( - "UTExportedTypeDeclarations".into(), - plist::Value::Array(exported_associations), - ); + if let Some(associations) = settings.file_associations() + && let Some(file_associations_plist) = + tauri_utils::config::file_associations_plist(associations) + && let Some(plist_dict) = file_associations_plist.as_dictionary() + { + for (key, value) in plist_dict { + plist.insert(key.clone(), value.clone()); } - - plist.insert( - "CFBundleDocumentTypes".into(), - plist::Value::Array( - associations - .iter() - .map(|association| { - let mut dict = plist::Dictionary::new(); - - if !association.ext.is_empty() { - dict.insert( - "CFBundleTypeExtensions".into(), - plist::Value::Array( - association - .ext - .iter() - .map(|ext| ext.to_string().into()) - .collect(), - ), - ); - } - - if let Some(content_types) = &association.content_types { - dict.insert( - "LSItemContentTypes".into(), - plist::Value::Array(content_types.iter().map(|s| s.to_string().into()).collect()), - ); - } - - dict.insert( - "CFBundleTypeName".into(), - association - .name - .as_ref() - .unwrap_or(&association.ext[0].0) - .to_string() - .into(), - ); - dict.insert( - "CFBundleTypeRole".into(), - association.role.to_string().into(), - ); - dict.insert("LSHandlerRank".into(), association.rank.to_string().into()); - plist::Value::Dictionary(dict) - }) - .collect(), - ), - ); } if let Some(path) = bundle_icon_file { diff --git a/crates/tauri-bundler/src/bundle/macos/icon.rs b/crates/tauri-bundler/src/bundle/macos/icon.rs index 63b6fe63adb0..25bff4fa1afe 100644 --- a/crates/tauri-bundler/src/bundle/macos/icon.rs +++ b/crates/tauri-bundler/src/bundle/macos/icon.rs @@ -65,7 +65,7 @@ pub fn create_icns_file(out_dir: &Path, settings: &Settings) -> crate::Result crate::Result< let icon_path = icon_path?; if icon_path .extension() - .map_or(false, |ext| ext == "png" || ext == "car") + .is_some_and(|ext| ext == "png" || ext == "car") { continue; } else if icon_path.extension() == Some(OsStr::new("icns")) { diff --git a/crates/tauri-bundler/src/bundle/settings.rs b/crates/tauri-bundler/src/bundle/settings.rs index c7447b91d54e..53dc8182000c 100644 --- a/crates/tauri-bundler/src/bundle/settings.rs +++ b/crates/tauri-bundler/src/bundle/settings.rs @@ -126,7 +126,7 @@ const ALL_PACKAGE_TYPES: &[PackageType] = &[ PackageType::IosBundle, #[cfg(target_os = "windows")] PackageType::WindowsMsi, - #[cfg(target_os = "windows")] + // NSIS installers can be built on all platforms but it's hidden in the --help output on macOS/Linux. PackageType::Nsis, #[cfg(target_os = "macos")] PackageType::MacOsBundle, @@ -470,11 +470,19 @@ pub struct NsisSettings { pub sidebar_image: Option, /// The path to an icon file used as the installer icon. pub installer_icon: Option, + /// The path to an icon file used as the uninstaller icon. + pub uninstaller_icon: Option, + /// The path to a bitmap file to display on the header of uninstallers pages. + /// Defaults to [`Self::header_image`]. If this is set but [`Self::header_image`] is not, a default image from NSIS will be applied to `header_image` + /// + /// The recommended dimensions are 150px x 57px. + pub uninstaller_header_image: Option, /// Whether the installation will be for all users or just the current user. pub install_mode: NSISInstallerMode, - /// A list of installer languages. + /// A list of installer languages. Default to `["English"]` if not set. + /// /// By default the OS language is used. If the OS language is not in the list of languages, the first language will be used. - /// To allow the user to select the language, set `display_language_selector` to `true`. + /// To allow the user to select the language, set [`Self::display_language_selector`] to `true`. /// /// See for the complete list of languages. pub languages: Option>, @@ -483,7 +491,7 @@ pub struct NsisSettings { /// /// See for an example `.nsi` file. /// - /// **Note**: the key must be a valid NSIS language and it must be added to [`NsisConfig`]languages array, + /// **Note**: the key must be a valid NSIS language and it must be added to the [`Self::languages`] array, pub custom_language_files: Option>, /// Whether to display a language selector dialog before the installer and uninstaller windows are rendered or not. /// By default the OS language is selected, with a fallback to the first language in the `languages` array. @@ -532,6 +540,10 @@ pub struct NsisSettings { /// Try to ensure that the WebView2 version is equal to or newer than this version, /// if the user's WebView2 is older than this version, /// the installer will try to trigger a WebView2 update. + #[deprecated( + since = "2.8.0", + note = "Use `WindowsSettings::minimum_webview2_version` instead." + )] pub minimum_webview2_version: Option, } @@ -587,6 +599,17 @@ pub struct WindowsSettings { /// if you are on another platform and want to cross-compile and sign you will /// need to use another tool like `osslsigncode`. pub sign_command: Option, + /// Try to ensure that the WebView2 version is equal to or newer than this version, + /// if the user's WebView2 is older than this version, + /// the installer will try to trigger a WebView2 update. + pub minimum_webview2_version: Option, + /// Whether to bundle the Visual C++ runtime DLLs alongside the application. + /// + /// This can be particularly useful when the application includes sidecars or DLLs that do not + /// statically link the Visual C++ runtime and require the runtime DLLs at runtime, and users + /// should not be required to install the Visual C++ Redistributable. This can also be useful + /// when `static_vc_runtime` is set to `false`. + pub bundle_vc_runtime: bool, } impl WindowsSettings { @@ -612,6 +635,8 @@ mod _default { webview_install_mode: Default::default(), allow_downgrades: true, sign_command: None, + minimum_webview2_version: None, + bundle_vc_runtime: false, } } } diff --git a/crates/tauri-bundler/src/bundle/windows/mod.rs b/crates/tauri-bundler/src/bundle/windows/mod.rs index b92fb5f56216..4da24df665f8 100644 --- a/crates/tauri-bundler/src/bundle/windows/mod.rs +++ b/crates/tauri-bundler/src/bundle/windows/mod.rs @@ -10,6 +10,8 @@ pub mod nsis; pub mod sign; mod util; +#[cfg(windows)] +pub use util::vswhere_path; pub use util::{ NSIS_OUTPUT_FOLDER_NAME, NSIS_UPDATER_OUTPUT_FOLDER_NAME, WIX_OUTPUT_FOLDER_NAME, WIX_UPDATER_OUTPUT_FOLDER_NAME, diff --git a/crates/tauri-bundler/src/bundle/windows/msi/main.wxs b/crates/tauri-bundler/src/bundle/windows/msi/main.wxs index a08c3b2cc5dc..dec39dadb4bc 100644 --- a/crates/tauri-bundler/src/bundle/windows/msi/main.wxs +++ b/crates/tauri-bundler/src/bundle/windows/msi/main.wxs @@ -283,38 +283,62 @@ {{#if install_webview}} - - - + + + + {{#if download_bootstrapper}} + - + {{/if}} - {{#if webview2_bootstrapper_path}} + - + {{/if}} - {{#if webview2_installer_path}} + - + + + + {{/if}} + + {{#if minimum_webview2_version}} + + + + + + + + + + + + + {{/if}} diff --git a/crates/tauri-bundler/src/bundle/windows/msi/mod.rs b/crates/tauri-bundler/src/bundle/windows/msi/mod.rs index 266a6a56036a..78248bffeae5 100644 --- a/crates/tauri-bundler/src/bundle/windows/msi/mod.rs +++ b/crates/tauri-bundler/src/bundle/windows/msi/mod.rs @@ -10,7 +10,7 @@ use crate::{ sign::{should_sign, try_sign}, util::{ WIX_OUTPUT_FOLDER_NAME, WIX_UPDATER_OUTPUT_FOLDER_NAME, download_webview2_bootstrapper, - download_webview2_offline_installer, + download_webview2_offline_installer, vc_runtime_dlls, }, }, }, @@ -536,6 +536,13 @@ pub fn build_wix_app_installer( } } + if let Some(minimum_webview2_version) = &settings.windows().minimum_webview2_version { + data.insert( + "minimum_webview2_version", + to_json(minimum_webview2_version), + ); + } + if let Some(license) = settings.license_file() { if license.ends_with(".rtf") { data.insert("license", to_json(license)); @@ -1089,6 +1096,35 @@ fn generate_resource_data(settings: &Settings) -> crate::Result { } } + let mut dlls = Vec::new(); + + if settings.windows().bundle_vc_runtime { + for dll in vc_runtime_dlls(settings.binary_arch())? { + let resource_path = dunce::simplified(&dll).to_path_buf(); + if added_resources.contains(&resource_path) { + continue; + } + added_resources.push(resource_path.clone()); + dlls.push(ResourceFile { + id: format!("I{}", Uuid::new_v4().as_simple()), + guid: Uuid::new_v4().to_string(), + path: resource_path, + }); + } + } + + if !dlls.is_empty() { + resources + .entry("".to_string()) + .and_modify(|r| r.files.append(&mut dlls)) + .or_insert(ResourceDirectory { + path: "".to_string(), + name: "".to_string(), + directories: vec![], + files: dlls, + }); + } + // Handle CEF support if cef_path is set, // using https://github.com/chromiumembedded/cef/blob/master/tools/distrib/win/README.redistrib.txt as a reference if let Some(cef_path) = settings.bundle_settings().cef_path.as_ref() { diff --git a/crates/tauri-bundler/src/bundle/windows/nsis/installer.nsi b/crates/tauri-bundler/src/bundle/windows/nsis/installer.nsi index e178c49ea12c..d372e3c39177 100644 --- a/crates/tauri-bundler/src/bundle/windows/nsis/installer.nsi +++ b/crates/tauri-bundler/src/bundle/windows/nsis/installer.nsi @@ -13,6 +13,12 @@ ManifestDPIAwareness PerMonitorV2 SetCompressor /SOLID "{{compression}}" !endif +; Keep above !include to stay ahead of any plugin command +; see https://github.com/tauri-apps/tauri/pull/15422#discussion_r3289239624 +{{#if signed_plugins_path}} +!addplugindir "{{signed_plugins_path}}" +{{/if}} + !include MUI2.nsh !include FileFunc.nsh !include x64.nsh @@ -41,6 +47,8 @@ ${StrLoc} !define INSTALLERICON "{{installer_icon}}" !define SIDEBARIMAGE "{{sidebar_image}}" !define HEADERIMAGE "{{header_image}}" +!define UNINSTALLERICON "{{uninstaller_icon}}" +!define UNINSTALLERHEADERIMAGE "{{uninstaller_header_image}}" !define MAINBINARYNAME "{{main_binary_name}}" !define MAINBINARYSRCPATH "{{main_binary_path}}" !define BUNDLEID "{{bundle_id}}" @@ -129,10 +137,26 @@ VIAddVersionKey "ProductVersion" "${VERSION}" !define MUI_WELCOMEFINISHPAGE_BITMAP "${SIDEBARIMAGE}" !endif -; Installer header image +; Enable header images for installer and uninstaller pages when either image is configured. !if "${HEADERIMAGE}" != "" !define MUI_HEADERIMAGE - !define MUI_HEADERIMAGE_BITMAP "${HEADERIMAGE}" +!else if "${UNINSTALLERHEADERIMAGE}" != "" + !define MUI_HEADERIMAGE +!endif + +; Installer header image +!if "${HEADERIMAGE}" != "" + !define MUI_HEADERIMAGE_BITMAP "${HEADERIMAGE}" +!endif + +; Uninstaller header image +!if "${UNINSTALLERHEADERIMAGE}" != "" + !define MUI_HEADERIMAGE_UNBITMAP "${UNINSTALLERHEADERIMAGE}" +!endif + +; Uninstaller icon +!if "${UNINSTALLERICON}" != "" + !define MUI_UNICON "${UNINSTALLERICON}" !endif ; Define registry key to store installer language diff --git a/crates/tauri-bundler/src/bundle/windows/nsis/languages/Italian.nsh b/crates/tauri-bundler/src/bundle/windows/nsis/languages/Italian.nsh index bca96544da58..f505e4e8ec98 100644 --- a/crates/tauri-bundler/src/bundle/windows/nsis/languages/Italian.nsh +++ b/crates/tauri-bundler/src/bundle/windows/nsis/languages/Italian.nsh @@ -1,27 +1,27 @@ -LangString addOrReinstall ${LANG_ITALIAN} "Aggiungi/Reinstalla componenti" -LangString alreadyInstalled ${LANG_ITALIAN} "Già installato" -LangString alreadyInstalledLong ${LANG_ITALIAN} "${PRODUCTNAME} ${VERSION} è già installato. Seleziona l'operazione che vuoi eseguire e clicca Avanti per continuare." -LangString appRunning ${LANG_ITALIAN} "{{product_name}} è in esecuzione! Chiudi e poi riprova." -LangString appRunningOkKill ${LANG_ITALIAN} "{{product_name}} è in esecuzione!$\nSeleziona OK per chiuderlo" -LangString chooseMaintenanceOption ${LANG_ITALIAN} "Seleziona l'operazione di manutenzione da eseguire." -LangString choowHowToInstall ${LANG_ITALIAN} "Seleziona come vuoi installare ${PRODUCTNAME}." -LangString createDesktop ${LANG_ITALIAN} "Crea scorciatoia sul Desktop" +LangString addOrReinstall ${LANG_ITALIAN} "Aggiungi/reinstalla componenti" +LangString alreadyInstalled ${LANG_ITALIAN} "Programma già installato" +LangString alreadyInstalledLong ${LANG_ITALIAN} "${PRODUCTNAME} ${VERSION} è già installato.$\nPer continuare scegli l'operazione da eseguire e seleziona 'Avanti'." +LangString appRunning ${LANG_ITALIAN} "{{product_name}} è in esecuzione!$\nChiudi {product_name}} e riprova." +LangString appRunningOkKill ${LANG_ITALIAN} "{{product_name}} è in esecuzione!$\nPer chiudere {product_name} seleziona 'OK'" +LangString chooseMaintenanceOption ${LANG_ITALIAN} "Scegli l'operazione di manutenzione da eseguire." +LangString choowHowToInstall ${LANG_ITALIAN} "Scegli come vuoi installare ${PRODUCTNAME}." +LangString createDesktop ${LANG_ITALIAN} "Crea collegamento sul desktop" LangString dontUninstall ${LANG_ITALIAN} "Non disinstallare" -LangString dontUninstallDowngrade ${LANG_ITALIAN} "Non disinstallare (Il downgrade senza la disinstallazione è disabilitato per questo installer)" -LangString failedToKillApp ${LANG_ITALIAN} "Impossibile chiudere {{product_name}}. Chiudi e poi riprova" -LangString installingWebview2 ${LANG_ITALIAN} "Installando WebView2..." -LangString newerVersionInstalled ${LANG_ITALIAN} "Una versione più recente di ${PRODUCTNAME} è già installata! Non è consigliato installare una versione più vecchia. Se vuoi comunque procedere, è meglio prima disinstallare la versione corrente. Seleziona l'operazione che vuoi eseguire e clicca Avanti per continuare." +LangString dontUninstallDowngrade ${LANG_ITALIAN} "Non disinstallare (per questo installer il downgrade senza la disinstallazione è disabilitato)" +LangString failedToKillApp ${LANG_ITALIAN} "Impossibile chiudere {{product_name}}.$\nChiudi {product_name} e poi riprova" +LangString installingWebview2 ${LANG_ITALIAN} "Installazione WebView2..." +LangString newerVersionInstalled ${LANG_ITALIAN} "È già installata una versione più recente di ${PRODUCTNAME}!$\nNon è consigliato installare una versione più vecchia.$\nSe vuoi comunque procedere, è meglio prima disinstallare la versione attuale.$\nPer continuare scegli l'operazione da eseguire e seleziona 'Avanti'." LangString older ${LANG_ITALIAN} "più vecchia" -LangString olderOrUnknownVersionInstalled ${LANG_ITALIAN} "Una versione $R4 di ${PRODUCTNAME} è installata nel tuo sistema. È consigliato che disinstalli la versione corrente prima di procedere all'installazione. Seleziona l'operazione che vuoi eseguire e clicca Avanti per continuare." -LangString silentDowngrades ${LANG_ITALIAN} "I downgrade sono disabilitati per questo installer, impossibile procedere con l'installer silenzioso, usa invece l'installer con interfaccia grafica.$\n" +LangString olderOrUnknownVersionInstalled ${LANG_ITALIAN} "Nel sistema è installata una versione $R4 di ${PRODUCTNAME}.$\nPrima di procedere all'installazione è consigliabile disinstallare la versione attuale.$\nPer continuare scegli l'operazione da eseguire e seleziona 'Avanti'." +LangString silentDowngrades ${LANG_ITALIAN} "Per questo installer i downgrade sono disabilitati , impossibile procedere con l'installer silenzioso, usa invece l'installer con interfaccia grafica.$\n" LangString unableToUninstall ${LANG_ITALIAN} "Impossibile disinstallare!" LangString uninstallApp ${LANG_ITALIAN} "Disinstalla ${PRODUCTNAME}" LangString uninstallBeforeInstalling ${LANG_ITALIAN} "Disinstalla prima di installare" LangString unknown ${LANG_ITALIAN} "sconosciuta" -LangString webview2AbortError ${LANG_ITALIAN} "Errore nell'installazione di WebView2! L'app non può funzionare senza. Prova a riavviare l'installer." -LangString webview2DownloadError ${LANG_ITALIAN} "Errore: Il download di WebView2 è fallito - $0" -LangString webview2DownloadSuccess ${LANG_ITALIAN} "Bootstrapper WebView2 scaricato con successo" -LangString webview2Downloading ${LANG_ITALIAN} "Scaricando il bootstrapper WebView2..." -LangString webview2InstallError ${LANG_ITALIAN} "Errore: L'installazione di WebView2 è fallita con il codice $1" +LangString webview2AbortError ${LANG_ITALIAN} "Errore nell'installazione di WebView2!$\nL'app non può funzionare senza.$\nProva a riavviare l'installer." +LangString webview2DownloadError ${LANG_ITALIAN} "Errore: il download di WebView2 è fallito - $0" +LangString webview2DownloadSuccess ${LANG_ITALIAN} "Download bootstrapper WebView2 completato" +LangString webview2Downloading ${LANG_ITALIAN} "Download bootstrapper WebView2..." +LangString webview2InstallError ${LANG_ITALIAN} "Errore: l'installazione di WebView2 è fallita con codice errore $1" LangString webview2InstallSuccess ${LANG_ITALIAN} "WebView2 installato correttamente" LangString deleteAppData ${LANG_ITALIAN} "Cancella i dati dell'applicazione" diff --git a/crates/tauri-bundler/src/bundle/windows/nsis/languages/Vietnamese.nsh b/crates/tauri-bundler/src/bundle/windows/nsis/languages/Vietnamese.nsh new file mode 100644 index 000000000000..00bd156ccf79 --- /dev/null +++ b/crates/tauri-bundler/src/bundle/windows/nsis/languages/Vietnamese.nsh @@ -0,0 +1,27 @@ +LangString addOrReinstall ${LANG_VIETNAMESE} "Thêm/Cài đặt lại các thành phần" +LangString alreadyInstalled ${LANG_VIETNAMESE} "Đã được cài đặt" +LangString alreadyInstalledLong ${LANG_VIETNAMESE} "${PRODUCTNAME} ${VERSION} đã được cài đặt. Hãy chọn thao tác bạn muốn thực hiện và nhấn Next để tiếp tục." +LangString appRunning ${LANG_VIETNAMESE} "{{product_name}} đang chạy! Vui lòng đóng ứng dụng trước rồi thử lại." +LangString appRunningOkKill ${LANG_VIETNAMESE} "{{product_name}} đang chạy!$\nNhấn OK để tắt ứng dụng" +LangString chooseMaintenanceOption ${LANG_VIETNAMESE} "Chọn thao tác bảo trì bạn muốn thực hiện." +LangString choowHowToInstall ${LANG_VIETNAMESE} "Chọn cách bạn muốn cài đặt ${PRODUCTNAME}." +LangString createDesktop ${LANG_VIETNAMESE} "Tạo biểu tượng ngoài màn hình" +LangString dontUninstall ${LANG_VIETNAMESE} "Không gỡ cài đặt" +LangString dontUninstallDowngrade ${LANG_VIETNAMESE} "Không gỡ cài đặt (Không hỗ trợ hạ cấp mà không gỡ cài đặt trong bộ cài này)" +LangString failedToKillApp ${LANG_VIETNAMESE} "Không thể tắt {{product_name}}. Vui lòng đóng ứng dụng trước rồi thử lại" +LangString installingWebview2 ${LANG_VIETNAMESE} "Đang cài đặt WebView2..." +LangString newerVersionInstalled ${LANG_VIETNAMESE} "Một phiên bản mới hơn của ${PRODUCTNAME} đã được cài đặt! Không khuyến nghị cài đặt phiên bản cũ hơn. Nếu bạn vẫn muốn cài phiên bản này, hãy gỡ phiên bản hiện tại trước. Chọn thao tác bạn muốn thực hiện và nhấn Next để tiếp tục." +LangString older ${LANG_VIETNAMESE} "cũ hơn" +LangString olderOrUnknownVersionInstalled ${LANG_VIETNAMESE} "Một phiên bản $R4 của ${PRODUCTNAME} đã được cài trên hệ thống. Khuyến nghị gỡ phiên bản hiện tại trước khi cài đặt. Chọn thao tác bạn muốn thực hiện và nhấn Next để tiếp tục." +LangString silentDowngrades ${LANG_VIETNAMESE} "Không hỗ trợ hạ cấp trong chế độ cài đặt im lặng, không thể tiếp tục. Vui lòng sử dụng trình cài đặt giao diện đồ họa.$\n" +LangString unableToUninstall ${LANG_VIETNAMESE} "Không thể gỡ cài đặt!" +LangString uninstallApp ${LANG_VIETNAMESE} "Gỡ cài đặt ${PRODUCTNAME}" +LangString uninstallBeforeInstalling ${LANG_VIETNAMESE} "Gỡ cài đặt trước khi cài đặt" +LangString unknown ${LANG_VIETNAMESE} "không xác định" +LangString webview2AbortError ${LANG_VIETNAMESE} "Cài đặt WebView2 thất bại! Ứng dụng không thể chạy nếu thiếu thành phần này. Hãy thử khởi động lại trình cài đặt." +LangString webview2DownloadError ${LANG_VIETNAMESE} "Lỗi: Tải WebView2 thất bại - $0" +LangString webview2DownloadSuccess ${LANG_VIETNAMESE} "Tải trình cài đặt WebView2 thành công" +LangString webview2Downloading ${LANG_VIETNAMESE} "Đang tải trình cài đặt WebView2..." +LangString webview2InstallError ${LANG_VIETNAMESE} "Lỗi: Cài đặt WebView2 thất bại với mã lỗi $1" +LangString webview2InstallSuccess ${LANG_VIETNAMESE} "Cài đặt WebView2 thành công" +LangString deleteAppData ${LANG_VIETNAMESE} "Xóa dữ liệu ứng dụng" diff --git a/crates/tauri-bundler/src/bundle/windows/nsis/mod.rs b/crates/tauri-bundler/src/bundle/windows/nsis/mod.rs index 718311e8ff0d..add775fbdd69 100644 --- a/crates/tauri-bundler/src/bundle/windows/nsis/mod.rs +++ b/crates/tauri-bundler/src/bundle/windows/nsis/mod.rs @@ -10,11 +10,11 @@ use crate::{ sign::{should_sign, sign_command, try_sign}, util::{ NSIS_OUTPUT_FOLDER_NAME, NSIS_UPDATER_OUTPUT_FOLDER_NAME, download_webview2_bootstrapper, - download_webview2_offline_installer, + download_webview2_offline_installer, vc_runtime_dlls, }, }, }, - error::ErrorExt, + error::{ErrorExt, bail}, utils::{ CommandExt, http_utils::{HashAlgorithm, download_and_verify, verify_file_hash}, @@ -281,6 +281,13 @@ fn build_nsis_app_installer( to_json(&additional_plugins_path), ); + if let Some(plugin_copy_path) = &maybe_plugin_copy_path { + data.insert( + "signed_plugins_path", + to_json(plugin_copy_path.join("x86-unicode")), + ); + } + data.insert("arch", to_json(arch)); data.insert("bundle_id", to_json(bundle_id)); data.insert("manufacturer", to_json(manufacturer)); @@ -318,7 +325,8 @@ fn build_nsis_app_installer( ); if let Some(license_file) = settings.license_file() { - let license_file = dunce::canonicalize(license_file)?; + let license_file = dunce::canonicalize(&license_file) + .fs_context("failed to resolve `bundle > licenseFile`", license_file)?; let license_file_with_bom = output_path.join("license_file"); let content = std::fs::read(license_file)?; write_utf8_with_bom(&license_file_with_bom, content)?; @@ -338,30 +346,70 @@ fn build_nsis_app_installer( if let Some(installer_icon) = &nsis.installer_icon { data.insert( "installer_icon", - to_json(dunce::canonicalize(installer_icon)?), + to_json(dunce::canonicalize(installer_icon).fs_context( + "failed to resolve `bundle > windows > nsis > installerIcon`", + installer_icon.to_owned(), + )?), ); } if let Some(header_image) = &nsis.header_image { - data.insert("header_image", to_json(dunce::canonicalize(header_image)?)); + data.insert( + "header_image", + to_json(dunce::canonicalize(header_image).fs_context( + "failed to resolve `bundle > windows > nsis > headerImage`", + header_image.to_owned(), + )?), + ); } if let Some(sidebar_image) = &nsis.sidebar_image { data.insert( "sidebar_image", - to_json(dunce::canonicalize(sidebar_image)?), + to_json(dunce::canonicalize(sidebar_image).fs_context( + "failed to resolve `bundle > windows > nsis > sidebarImage`", + sidebar_image.to_owned(), + )?), + ); + } + + if let Some(uninstaller_icon) = &nsis.uninstaller_icon { + data.insert( + "uninstaller_icon", + to_json(dunce::canonicalize(uninstaller_icon).fs_context( + "failed to resolve `bundle > windows > nsis > uninstallerIcon`", + uninstaller_icon.to_owned(), + )?), + ); + } + + if let Some(uninstaller_header_image) = &nsis.uninstaller_header_image { + data.insert( + "uninstaller_header_image", + to_json(dunce::canonicalize(uninstaller_header_image).fs_context( + "failed to resolve `bundle > windows > nsis > uninstallerHeaderImage`", + uninstaller_header_image.to_owned(), + )?), ); } if let Some(installer_hooks) = &nsis.installer_hooks { - let installer_hooks = dunce::canonicalize(installer_hooks)?; + let installer_hooks = dunce::canonicalize(installer_hooks).fs_context( + "failed to resolve `bundle > windows > nsis > installerHooks`", + installer_hooks.to_owned(), + )?; data.insert("installer_hooks", to_json(installer_hooks)); } if let Some(start_menu_folder) = &nsis.start_menu_folder { data.insert("start_menu_folder", to_json(start_menu_folder)); } - if let Some(minimum_webview2_version) = &nsis.minimum_webview2_version { + #[allow(deprecated)] + if let Some(minimum_webview2_version) = nsis + .minimum_webview2_version + .as_ref() + .or(settings.windows().minimum_webview2_version.as_ref()) + { data.insert( "minimum_webview2_version", to_json(minimum_webview2_version), @@ -610,7 +658,7 @@ fn build_nsis_app_installer( ); let nsis_output_path = output_path.join(out_file); - let nsis_installer_path = settings.project_out_directory().to_path_buf().join(format!( + let nsis_installer_path = settings.project_out_directory().join(format!( "bundle/{}/{}.exe", if updater { NSIS_UPDATER_OUTPUT_FOLDER_NAME @@ -643,11 +691,7 @@ fn build_nsis_app_installer( #[cfg(not(target_os = "windows"))] let mut nsis_cmd = Command::new("makensis"); - if let Some(plugins_path) = &maybe_plugin_copy_path { - nsis_cmd.env("NSISPLUGINS", plugins_path); - } - - nsis_cmd + let status = nsis_cmd .args(["-INPUTCHARSET", "UTF8", "-OUTPUTCHARSET", "UTF8"]) .arg(match settings.log_level() { log::Level::Error => "-V1", @@ -664,6 +708,9 @@ fn build_nsis_app_installer( command: "makensis.exe".to_string(), error, })?; + if !status.success() { + bail!("Failed to bundle app with makensis"); + } fs::rename(nsis_output_path, &nsis_installer_path)?; @@ -758,6 +805,22 @@ fn generate_resource_data(settings: &Settings) -> crate::Result { } } + if settings.windows().bundle_vc_runtime { + for dll in vc_runtime_dlls(settings.binary_arch())? { + let dll = dunce::simplified(&dll).to_path_buf(); + if added_resources.contains(&dll) { + continue; + } + let target = PathBuf::from( + dll + .file_name() + .expect("failed to extract Visual C++ runtime DLL filename"), + ); + added_resources.push(dll.clone()); + resources.insert(dll, (PathBuf::new(), target)); + } + } + // Handle CEF support if cef_path is set, // using https://github.com/chromiumembedded/cef/blob/master/tools/distrib/win/README.redistrib.txt as a reference if let Some(cef_path) = settings.bundle_settings().cef_path.as_ref() { @@ -940,6 +1003,7 @@ fn get_lang_data(lang: &str) -> Option<(String, &[u8])> { "portuguese" => include_bytes!("./languages/Portuguese.nsh"), "ukrainian" => include_bytes!("./languages/Ukrainian.nsh"), "norwegian" => include_bytes!("./languages/Norwegian.nsh"), + "vietnamese" => include_bytes!("./languages/Vietnamese.nsh"), _ => return None, }; Some((path, content)) diff --git a/crates/tauri-bundler/src/bundle/windows/sign.rs b/crates/tauri-bundler/src/bundle/windows/sign.rs index 457481a46da3..bcc5cbcee3cb 100644 --- a/crates/tauri-bundler/src/bundle/windows/sign.rs +++ b/crates/tauri-bundler/src/bundle/windows/sign.rs @@ -97,7 +97,7 @@ fn signtool() -> Option { kit_bin_paths.push(kits_root_10_bin_path); // Choose which version of SignTool to use based on OS bitness - let arch_dir = util::os_bitness().ok_or(crate::Error::UnsupportedBitness)?; + let arch_dir = util::processor_architecture().ok_or(crate::Error::UnsupportedBitness)?; /* Iterate through all bin paths, checking for existence of a SignTool executable. */ for kit_bin_path in &kit_bin_paths { diff --git a/crates/tauri-bundler/src/bundle/windows/util.rs b/crates/tauri-bundler/src/bundle/windows/util.rs index 55cfea3a968f..0f9a7f462e52 100644 --- a/crates/tauri-bundler/src/bundle/windows/util.rs +++ b/crates/tauri-bundler/src/bundle/windows/util.rs @@ -3,11 +3,14 @@ // SPDX-License-Identifier: MIT use std::{ - fs::create_dir_all, + fs, path::{Path, PathBuf}, }; +#[cfg(windows)] +use std::{io::Write, process::Command}; use ureq::ResponseExt; +use crate::bundle::settings::Arch; use crate::utils::http_utils::{base_ureq_agent, download}; pub const WEBVIEW2_BOOTSTRAPPER_URL: &str = "https://go.microsoft.com/fwlink/p/?LinkId=2124703"; @@ -22,6 +25,12 @@ pub const NSIS_UPDATER_OUTPUT_FOLDER_NAME: &str = "nsis-updater"; pub const WIX_OUTPUT_FOLDER_NAME: &str = "msi"; pub const WIX_UPDATER_OUTPUT_FOLDER_NAME: &str = "msi-updater"; +#[cfg(windows)] +const VSWHERE: &[u8] = include_bytes!("vswhere.exe"); +const VCTOOLS_REDIST_DIR_ENV_VAR: &str = "VCTOOLS_REDIST_DIR"; +#[cfg(windows)] +const VC_REDIST_COMPONENT: &str = "Microsoft.VisualStudio.Component.VC.Redist.14.Latest"; + pub fn webview2_guid_path(url: &str) -> crate::Result<(String, String)> { let agent = base_ureq_agent(); let response = agent.head(url).call().map_err(Box::new)?; @@ -57,16 +66,174 @@ pub fn download_webview2_offline_installer(base_path: &Path, arch: &str) -> crat let dir_path = base_path.join(guid); let file_path = dir_path.join(filename); if !file_path.exists() { - create_dir_all(dir_path)?; + fs::create_dir_all(dir_path)?; std::fs::write(&file_path, download(url)?)?; } Ok(file_path) } +/// Finds the Visual C++ runtime DLLs for the given architecture. +pub fn vc_runtime_dlls(arch: Arch) -> crate::Result> { + let arch = vc_runtime_arch(arch)?; + let redist_dir = vc_redist_dir()?; + let runtime_dir = vc_runtime_dir(&redist_dir, arch)?; + + let dlls = glob::glob(&glob_path(&runtime_dir, "*.dll"))?.collect::, _>>()?; + if dlls.is_empty() { + return Err(crate::Error::GenericError(format!( + "no Visual C++ runtime DLLs found in `{}`", + runtime_dir.display() + ))); + } + + Ok(dlls) +} + +#[inline(always)] +fn vc_runtime_arch(arch: Arch) -> crate::Result<&'static str> { + match arch { + Arch::X86_64 => Ok("x64"), + Arch::X86 => Ok("x86"), + Arch::AArch64 => Ok("arm64"), + _ => Err(crate::Error::GenericError( + "bundling the Visual C++ runtime is only supported for Windows x86, x64 and arm64 targets" + .into(), + )), + } +} + +#[cfg(windows)] +fn visual_studio_dir() -> crate::Result { + let Some(vswhere) = vswhere_path() else { + return Err(crate::Error::GenericError( + "failed to prepare bundled vswhere.exe".into(), + )); + }; + + let output = Command::new(vswhere) + .args([ + "-latest", + "-prerelease", + "-products", + "*", + "-requires", + VC_REDIST_COMPONENT, + "-property", + "installationPath", + "-format", + "value", + "-utf8", + ]) + .output()?; + + if !output.status.success() { + return Err(crate::Error::GenericError(format!( + "failed to locate Visual Studio with the {VC_REDIST_COMPONENT} component" + ))); + } + + let stdout = String::from_utf8_lossy(&output.stdout); + let Some(vs_dir) = stdout.lines().map(str::trim).find(|line| !line.is_empty()) else { + return Err(crate::Error::GenericError(format!( + "failed to locate Visual Studio with the {VC_REDIST_COMPONENT} component" + ))); + }; + + Ok(PathBuf::from(vs_dir)) +} + +fn vc_redist_dir() -> crate::Result { + if let Ok(redist_dir) = std::env::var(VCTOOLS_REDIST_DIR_ENV_VAR) { + return Ok(PathBuf::from(redist_dir)); + } + + #[cfg(windows)] + { + let vs_dir = visual_studio_dir()?; + Ok(vs_dir.join("VC/Redist/MSVC")) + } + + #[cfg(not(windows))] + { + Err(crate::Error::GenericError(format!( + "failed to find Visual C++ runtime redist directory; set {VCTOOLS_REDIST_DIR_ENV_VAR} when bundling the Visual C++ runtime from non-Windows hosts" + ))) + } +} + +fn vc_runtime_dir(redist_dir: &Path, arch: &str) -> crate::Result { + let Some(latest_version_dir) = latest_vc_redist_version_dir(redist_dir)? else { + return Err(crate::Error::GenericError(format!( + "failed to find Visual C++ runtime versions in `{}`", + redist_dir.display() + ))); + }; + + let arch_dir = latest_version_dir.join(arch); + let Some(runtime_dir) = glob::glob(&glob_path(&arch_dir, "Microsoft.VC*.CRT"))? + .filter_map(Result::ok) + .find(|path| path.is_dir()) + else { + return Err(crate::Error::GenericError(format!( + "failed to find Visual C++ runtime directory for `{arch}` in `{}`", + arch_dir.display() + ))); + }; + + Ok(runtime_dir) +} + +fn latest_vc_redist_version_dir(redist_dir: &Path) -> crate::Result> { + let dir = fs::read_dir(redist_dir)? + .flatten() + .map(|entry| entry.path()) + .filter(|path| path.is_dir()) + .filter_map(|path| { + let version = path + .file_name()? + .to_str()? + .parse::() + .ok()?; + Some((version, path)) + }) + .max_by(|(a, _), (b, _)| a.cmp(b)) + .map(|(_, path)| path); + Ok(dir) +} + +/// Builds a glob pattern from a literal base path and an unescaped glob suffix. +/// +/// The base path is escaped so Visual Studio paths containing glob metacharacters are treated as +/// literal directories, while `pattern` remains active glob syntax. +fn glob_path(path: &Path, pattern: &str) -> String { + PathBuf::from(glob::Pattern::escape(&path.to_string_lossy())) + .join(pattern) + .to_string_lossy() + .into_owned() +} + +/// Returns the bundled `vswhere.exe` path. +/// +/// The executable is written to a temporary file so callers do not depend on a system-installed +/// `vswhere.exe`. +#[cfg(windows)] +pub fn vswhere_path() -> Option { + let mut vswhere = std::env::temp_dir(); + vswhere.push("vswhere.exe"); + + if !vswhere.exists() { + let mut file = std::fs::File::create(&vswhere).ok()?; + file.write_all(VSWHERE).ok()?; + } + + Some(vswhere) +} + #[cfg(target_os = "windows")] -pub fn os_bitness<'a>() -> Option<&'a str> { +pub fn processor_architecture<'a>() -> Option<&'a str> { use windows_sys::Win32::System::SystemInformation::{ - GetNativeSystemInfo, PROCESSOR_ARCHITECTURE_AMD64, PROCESSOR_ARCHITECTURE_INTEL, SYSTEM_INFO, + GetNativeSystemInfo, PROCESSOR_ARCHITECTURE_AMD64, PROCESSOR_ARCHITECTURE_ARM, + PROCESSOR_ARCHITECTURE_ARM64, PROCESSOR_ARCHITECTURE_INTEL, SYSTEM_INFO, }; let mut system_info: SYSTEM_INFO = unsafe { std::mem::zeroed() }; @@ -74,6 +241,8 @@ pub fn os_bitness<'a>() -> Option<&'a str> { match unsafe { system_info.Anonymous.Anonymous.wProcessorArchitecture } { PROCESSOR_ARCHITECTURE_INTEL => Some("x86"), PROCESSOR_ARCHITECTURE_AMD64 => Some("x64"), + PROCESSOR_ARCHITECTURE_ARM => Some("arm"), + PROCESSOR_ARCHITECTURE_ARM64 => Some("arm64"), _ => None, } } diff --git a/crates/tauri-bundler/src/bundle/windows/vswhere.exe b/crates/tauri-bundler/src/bundle/windows/vswhere.exe new file mode 100644 index 000000000000..64f58cc93bd9 Binary files /dev/null and b/crates/tauri-bundler/src/bundle/windows/vswhere.exe differ diff --git a/crates/tauri-bundler/src/error.rs b/crates/tauri-bundler/src/error.rs index 7794b4fa8177..b993eb1304a9 100644 --- a/crates/tauri-bundler/src/error.rs +++ b/crates/tauri-bundler/src/error.rs @@ -79,11 +79,9 @@ pub enum Error { #[error("`{0}`")] HttpError(#[from] Box), /// Invalid glob pattern. - #[cfg(windows)] #[error("{0}")] GlobPattern(#[from] glob::PatternError), /// Failed to use glob pattern. - #[cfg(windows)] #[error("`{0}`")] Glob(#[from] glob::GlobError), /// Failed to parse the URL diff --git a/crates/tauri-cli/CHANGELOG.md b/crates/tauri-cli/CHANGELOG.md index 601cffc02c65..03b4ca37d59c 100644 --- a/crates/tauri-cli/CHANGELOG.md +++ b/crates/tauri-cli/CHANGELOG.md @@ -1,5 +1,59 @@ # Changelog +## \[2.11.2] + +### Dependencies + +- Upgraded to `tauri-utils@2.9.2` +- Upgraded to `tauri-bundler@2.9.2` + +## \[2.11.1] + +### Dependencies + +- Upgraded to `tauri-utils@2.9.1` +- Upgraded to `tauri-bundler@2.9.1` + +## \[2.11.0] + +### New Features + +- [`926a57bb0`](https://www.github.com/tauri-apps/tauri/commit/926a57bb0851e45d47ad1ee68fc96a9c25754c7c) ([#15201](https://www.github.com/tauri-apps/tauri/pull/15201)) Added uninstaller icon and uninstaller header image support for NSIS installer. + + Notes: + + - For `tauri-bundler` lib users, the `NsisSettings` now has 2 new fields `uninstaller_icon` and `uninstaller_header_image` which can be a breaking change + - When bundling with NSIS, users can add `uninstallerIcon` and `uninstallerHeaderImage` under `bundle > windows > nsis` to configure them. +- [`cc5c97602`](https://www.github.com/tauri-apps/tauri/commit/cc5c976027b0ab2431c13ec5b2e201d4414a8a6e) ([#14486](https://www.github.com/tauri-apps/tauri/pull/14486)) Implement file association for Android and iOS. +- [`4ef5797f0`](https://www.github.com/tauri-apps/tauri/commit/4ef5797f0fb27fa2df3f39f4a54e48ef319560ec) ([#15061](https://www.github.com/tauri-apps/tauri/pull/15061)) Add `--no-sign` and `--archive-only` flags to `tauri ios build`. +- [`764b9139a`](https://www.github.com/tauri-apps/tauri/commit/764b9139a32de149d8a914a6b5ec6cd1937c64eb) ([#14313](https://www.github.com/tauri-apps/tauri/pull/14313)) Prompt to restart the Android emulator if it is not connected to adb. +- [`5dc2cee60`](https://www.github.com/tauri-apps/tauri/commit/5dc2cee60370665af88c185684432e425b1c987d) ([#14793](https://www.github.com/tauri-apps/tauri/pull/14793)) Added support for `minimumWebview2Version` option support for the MSI (Wix) installer, the old `bundle > windows > nsis > minimumWebview2Version` is now deprecated in favor of `bundle > windows > minimumWebview2Version` + + Notes: + + - For anyone relying on the `WVRTINSTALLED` `Property` tag in `main.wxs`, it is now renamed to `INSTALLED_WEBVIEW2_VERSION` + - For `tauri-bundler` lib users, the `WindowsSettings` now has a new field `minimum_webview2_version` which can be a breaking change + +### Enhancements + +- [`be0e4bd2d`](https://www.github.com/tauri-apps/tauri/commit/be0e4bd2da02eb6cc75a8dc7c81663277e64c590) ([#15218](https://www.github.com/tauri-apps/tauri/pull/15218)) Added Vietnamese translations for the NSIS installer +- [`8718d0816`](https://www.github.com/tauri-apps/tauri/commit/8718d08163f074dfc53387ebd1d823f9c28280ee) ([#15033](https://www.github.com/tauri-apps/tauri/pull/15033)) Show the context before prompting for updater signing key password + +### Bug Fixes + +- [`fcb702ec4`](https://www.github.com/tauri-apps/tauri/commit/fcb702ec4d924e81943efaeebea8d3edb7289c33) ([#14954](https://www.github.com/tauri-apps/tauri/pull/14954)) Fix `build --bundles` to allow `nsis` arg in linux+macOS +- [`80c1425af`](https://www.github.com/tauri-apps/tauri/commit/80c1425af86058b1fc9489a30f778b6288d79b6b) ([#14921](https://www.github.com/tauri-apps/tauri/pull/14921)) Fix iOS build failure when `Metal Toolchain` is installed by using explicit `$(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain` path instead of `$(TOOLCHAIN_DIR)` for Swift library search paths. + +### What's Changed + +- [`9979cde1c`](https://www.github.com/tauri-apps/tauri/commit/9979cde1c5534dafb1a07cc4dc2bc280d15d2f66) ([#15175](https://www.github.com/tauri-apps/tauri/pull/15175)) Update NSIS installer Italian translations + +### Dependencies + +- Upgraded to `tauri-macos-sign@2.3.4` +- Upgraded to `tauri-bundler@2.9.0` +- Upgraded to `tauri-utils@2.9.0` + ## \[2.10.1] ### Bug Fixes diff --git a/crates/tauri-cli/Cargo.toml b/crates/tauri-cli/Cargo.toml index 2ae2d34a4ec8..2fef8d7654fb 100644 --- a/crates/tauri-cli/Cargo.toml +++ b/crates/tauri-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-cli" -version = "2.10.1" +version = "2.11.2" authors = ["Tauri Programme within The Commons Conservancy"] edition = "2024" rust-version = "1.88" @@ -36,7 +36,7 @@ name = "cargo-tauri" path = "src/main.rs" [target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\", target_os = \"windows\", target_os = \"macos\"))".dependencies] -cargo-mobile2 = { version = "0.21.1", default-features = false } +cargo-mobile2 = { version = "0.22.4", default-features = false } [dependencies] jsonrpsee = { version = "0.24", features = ["server"] } @@ -47,7 +47,7 @@ sublime_fuzzy = "0.7" clap_complete = "4" clap = { version = "4", features = ["derive", "env"] } thiserror = "2" -tauri-bundler = { version = "2.8.1", default-features = false, path = "../tauri-bundler" } +tauri-bundler = { version = "2.9.2", default-features = false, path = "../tauri-bundler" } colored = "2" serde = { version = "1", features = ["derive"] } serde_json = { version = "1", features = ["preserve_order"] } @@ -56,16 +56,16 @@ notify = "8" notify-debouncer-full = "0.6" shared_child = "1" duct = "1.0" -toml_edit = { version = "0.24", features = ["serde"] } +toml_edit = { version = "0.25", features = ["serde"] } json-patch = "3" -tauri-utils = { version = "2.8.3", path = "../tauri-utils", features = [ +tauri-utils = { version = "2.9.2", path = "../tauri-utils", features = [ "isolation", "schema", "config-json5", "config-toml", - "html-manipulation", + "html-manipulation-2", ] } -toml = "0.9" +toml = "1" jsonschema = { version = "0.33", default-features = false } handlebars = "6" include_dir = "0.7" @@ -89,8 +89,6 @@ env_logger = "0.11" icns = { package = "tauri-icns", version = "0.1" } image = { version = "0.25", default-features = false, features = ["ico"] } axum = { version = "0.8", features = ["ws"] } -html5ever = "0.29" -kuchiki = { package = "kuchikiki", version = "=0.8.8-speedreader" } tokio = { version = "1", features = ["macros", "sync"] } common-path = "1" serde-value = "0.7" @@ -105,7 +103,7 @@ oxc_span = "0.36" oxc_allocator = "0.36" oxc_ast = "0.36" magic_string = "0.3" -phf = { version = "0.11", features = ["macros"] } +phf = { version = "0.13", features = ["macros"] } walkdir = "2" elf = "0.7" memchr = "2" @@ -135,7 +133,7 @@ libc = "0.2" [target."cfg(target_os = \"macos\")".dependencies] plist = "1" -tauri-macos-sign = { version = "2.3.3", path = "../tauri-macos-sign" } +tauri-macos-sign = { version = "2.3.4", path = "../tauri-macos-sign" } object = { version = "0.36", default-features = false, features = [ "macho", "read_core", diff --git a/crates/tauri-cli/config.schema.json b/crates/tauri-cli/config.schema.json index cbde416db413..e3426ac0cce5 100644 --- a/crates/tauri-cli/config.schema.json +++ b/crates/tauri-cli/config.schema.json @@ -1,5 +1,5 @@ { - "$id": "https://schema.tauri.app/config/2.10.3", + "$id": "https://schema.tauri.app/config/2.11.2", "$schema": "http://json-schema.org/draft-07/schema#", "title": "Config", "description": "The Tauri configuration object.\nIt is read from a file where you can define your frontend assets,\nconfigure the bundler and define a tray icon.\n\nThe configuration file is generated by the\n[`tauri init`](https://v2.tauri.app/reference/cli/#init) command that lives in\nyour Tauri application source directory (src-tauri).\n\nOnce generated, you may modify it at will to customize your Tauri application.\n\n## File Formats\n\nBy default, the configuration is defined as a JSON file named `tauri.conf.json`.\n\nTauri also supports JSON5 and TOML files via the `config-json5` and `config-toml` Cargo features, respectively.\nThe JSON5 file name must be either `tauri.conf.json` or `tauri.conf.json5`.\nThe TOML file name is `Tauri.toml`.\n\n## Platform-Specific Configuration\n\nIn addition to the default configuration file, Tauri can\nread a platform-specific configuration from `tauri.linux.conf.json`,\n`tauri.windows.conf.json`, `tauri.macos.conf.json`, `tauri.android.conf.json` and `tauri.ios.conf.json`\n(or `Tauri.linux.toml`, `Tauri.windows.toml`, `Tauri.macos.toml`, `Tauri.android.toml` and `Tauri.ios.toml` if the `Tauri.toml` format is used),\nwhich gets merged with the main configuration object.\n\n## Configuration Structure\n\nThe configuration is composed of the following objects:\n\n- [`app`](#appconfig): The Tauri configuration\n- [`build`](#buildconfig): The build configuration\n- [`bundle`](#bundleconfig): The bundle configurations\n- [`plugins`](#pluginconfig): The plugins configuration\n\nExample tauri.config.json file:\n\n```json\n{\n \"productName\": \"tauri-app\",\n \"version\": \"0.1.0\",\n \"build\": {\n \"beforeBuildCommand\": \"\",\n \"beforeDevCommand\": \"\",\n \"devUrl\": \"http://localhost:3000\",\n \"frontendDist\": \"../dist\"\n },\n \"app\": {\n \"security\": {\n \"csp\": null\n },\n \"windows\": [\n {\n \"fullscreen\": false,\n \"height\": 600,\n \"resizable\": true,\n \"title\": \"Tauri App\",\n \"width\": 800\n }\n ]\n },\n \"bundle\": {},\n \"plugins\": {}\n}\n```", @@ -68,7 +68,10 @@ "description": "The build configuration.", "default": { "removeUnusedCommands": false, - "additionalWatchFolders": [] + "additionalWatchFolders": [], + "windows": { + "staticVCRuntime": true + } }, "allOf": [ { @@ -94,9 +97,11 @@ "silent": true }, "allowDowngrades": true, + "minimumWebview2Version": null, "wix": null, "nsis": null, - "signCommand": null + "signCommand": null, + "bundleVCRuntime": false }, "linux": { "appimage": { @@ -549,7 +554,7 @@ ] }, "backgroundThrottling": { - "description": "Change the default background throttling behaviour.\n\nBy default, browsers use a suspend policy that will throttle timers and even unload\nthe whole tab (view) to free resources after roughly 5 minutes when a view became\nminimized or hidden. This will pause all tasks until the documents visibility state\nchanges back from hidden to visible by bringing the view back to the foreground.\n\n## Platform-specific\n\n- **Linux / Windows / Android**: Unsupported. Workarounds like a pending WebLock transaction might suffice.\n- **iOS**: Supported since version 17.0+.\n- **macOS**: Supported since version 14.0+.\n\nsee https://github.com/tauri-apps/tauri/issues/5250#issuecomment-2569380578", + "description": "Change the default background throttling behaviour.\n\nBy default, browsers use a suspend policy that will throttle timers and even unload\nthe whole tab (view) to free resources after roughly 5 minutes when a view became\nminimized or hidden. This will pause all tasks until the documents visibility state\nchanges back from hidden to visible by bringing the view back to the foreground.\n\n## Platform-specific\n\n- **Linux / Windows / Android**: Unsupported. Workarounds like a pending WebLock transaction might suffice.\n- **iOS**: Supported since version 17.0+.\n- **macOS**: Supported since version 14.0+.\n\nsee ", "anyOf": [ { "$ref": "#/definitions/BackgroundThrottlingPolicy" @@ -604,6 +609,32 @@ "$ref": "#/definitions/ScrollBarStyle" } ] + }, + "activityName": { + "description": "The name of the Android activity to create for this window.", + "type": [ + "string", + "null" + ] + }, + "createdByActivityName": { + "description": "The name of the Android activity that is creating this webview window.\n\nThis is important to determine which stack the activity will belong to.", + "type": [ + "string", + "null" + ] + }, + "requestedBySceneIdentifier": { + "description": "Sets the identifier of the scene that is requesting the new scene,\nestablishing a relationship between the two scenes.\n\nBy default the system uses the foreground scene.", + "type": [ + "string", + "null" + ] + }, + "generalAutofillEnabled": { + "description": "Controls the WebView's browser-level general autofill behavior.\n\n**This option does not disable password or credit card autofill.**\n\nWhen set to `false`, the WebView will not automatically populate\ngeneral form fields using previously stored data such as addresses\nor contact information.\n\nIf not specified, this is `true` by default.\n\n## Platform-specific\n\n- **Windows**: Supported. WebView2's autofill feature (called\n \"Suggestions\") may not honor `autocomplete=\"off\"` on input\n elements in some cases.\n- **Linux / Android / iOS / macOS**: Unsupported and performs no\n operation.", + "type": "boolean", + "default": true } }, "additionalProperties": false @@ -1068,7 +1099,7 @@ "const": "default" }, { - "description": "Fluent UI style overlay scrollbars. **Windows Only**\n\nRequires WebView2 Runtime version 125.0.2535.41 or higher, does nothing on older versions,\nsee https://learn.microsoft.com/en-us/microsoft-edge/webview2/release-notes/?tabs=dotnetcsharp#10253541", + "description": "Fluent UI style overlay scrollbars. **Windows Only**\n\nRequires WebView2 Runtime version 125.0.2535.41 or higher, does nothing on older versions,\nsee ", "type": "string", "const": "fluentOverlay" } @@ -1863,6 +1894,17 @@ "type": "string" }, "default": [] + }, + "windows": { + "description": "Windows-specific build configuration.", + "default": { + "staticVCRuntime": true + }, + "allOf": [ + { + "$ref": "#/definitions/WindowsBuildConfig" + } + ] } }, "additionalProperties": false @@ -1990,6 +2032,18 @@ } ] }, + "WindowsBuildConfig": { + "description": "Windows-specific build configuration.", + "type": "object", + "properties": { + "staticVCRuntime": { + "description": "Whether to statically link the Visual C++ runtime into the application binary on Windows MSVC targets.", + "type": "boolean", + "default": true + } + }, + "additionalProperties": false + }, "BundleConfig": { "description": "Configuration for tauri-bundler.\n\nSee more: ", "type": "object", @@ -2129,9 +2183,11 @@ "silent": true }, "allowDowngrades": true, + "minimumWebview2Version": null, "wix": null, "nsis": null, - "signCommand": null + "signCommand": null, + "bundleVCRuntime": false }, "allOf": [ { @@ -2370,7 +2426,7 @@ ] }, "mimeType": { - "description": "The mime-type e.g. 'image/png' or 'text/plain'. Linux-only.", + "description": "The mime-type of the association, e.g. `'image/png'` or `'text/plain'`.\n\n- **Linux**: written as `MimeType=` in the `.desktop` file.\n- **macOS / iOS**: added as `public.mime-type` in the `UTTypeTagSpecification` dictionary of\n the `UTExportedTypeDeclarations` entry in `Info.plist`.\n- **Android**: used as `android:mimeType` in the `` element of an ``\n in `AndroidManifest.xml`.", "type": [ "string", "null" @@ -2395,6 +2451,16 @@ "type": "null" } ] + }, + "androidIntentActionFilters": { + "description": "Intent action filters for this file association.\n\nBy default all filters are used.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/AndroidIntentAction" + } } }, "additionalProperties": false, @@ -2485,6 +2551,26 @@ "identifier" ] }, + "AndroidIntentAction": { + "description": "Android intent action.", + "oneOf": [ + { + "description": "ACTION_SEND.\n\n", + "type": "string", + "const": "send" + }, + { + "description": "ACTION_SEND_MULTIPLE.\n\n", + "type": "string", + "const": "sendMultiple" + }, + { + "description": "ACTION_VIEW.\n\n", + "type": "string", + "const": "view" + } + ] + }, "WindowsConfig": { "description": "Windows bundler configuration.\n\nSee more: ", "type": "object", @@ -2532,6 +2618,13 @@ "type": "boolean", "default": true }, + "minimumWebview2Version": { + "description": "Try to ensure that the WebView2 version is equal to or newer than this version,\nif the user's WebView2 is older than this version,\nthe installer will try to trigger a WebView2 update.", + "type": [ + "string", + "null" + ] + }, "wix": { "description": "Configuration for the MSI generated with WiX.", "anyOf": [ @@ -2564,6 +2657,11 @@ "type": "null" } ] + }, + "bundleVCRuntime": { + "description": "Whether to bundle the Visual C++ runtime DLLs alongside the application.\n\nThis can be particularly useful when your application includes sidecars or DLLs that do\nnot statically link the Visual C++ runtime and require the runtime DLLs at runtime, and\nyou do not want to require users to install the Visual C++ Redistributable. This can also\nbe useful when `build > windows > staticVCRuntime` is set to `false`.", + "type": "boolean", + "default": false } }, "additionalProperties": false @@ -2842,6 +2940,20 @@ "null" ] }, + "uninstallerIcon": { + "description": "The path to an icon file used as the uninstaller icon.", + "type": [ + "string", + "null" + ] + }, + "uninstallerHeaderImage": { + "description": "The path to a bitmap file to display on the header of uninstallers pages.\nDefaults to [`Self::header_image`]. If this is set but [`Self::header_image`] is not, a default image from NSIS will be applied to `header_image`\n\nThe recommended dimensions are 150px x 57px.", + "type": [ + "string", + "null" + ] + }, "installMode": { "description": "Whether the installation will be for all users or just the current user.", "default": "currentUser", @@ -2852,7 +2964,7 @@ ] }, "languages": { - "description": "A list of installer languages.\nBy default the OS language is used. If the OS language is not in the list of languages, the first language will be used.\nTo allow the user to select the language, set `display_language_selector` to `true`.\n\nSee for the complete list of languages.", + "description": "A list of installer languages. Default to `[\"English\"]` if not set.\n\nBy default the OS language is used. If the OS language is not in the list of languages, the first language will be used.\nTo allow the user to select the language, set `display_language_selector` to `true`.\n\nSee for the complete list of languages.", "type": [ "array", "null" @@ -2862,7 +2974,7 @@ } }, "customLanguageFiles": { - "description": "A key-value pair where the key is the language and the\nvalue is the path to a custom `.nsh` file that holds the translated text for tauri's custom messages.\n\nSee for an example `.nsh` file.\n\n**Note**: the key must be a valid NSIS language and it must be added to [`NsisConfig`] languages array,", + "description": "A key-value pair where the key is the language and the\nvalue is the path to a custom `.nsh` file that holds the translated text for tauri's custom messages.\n\nSee for an example `.nsh` file.\n\n**Note**: the key must be a valid NSIS language and it must be added to the [`Self::languages`] array,", "type": [ "object", "null" @@ -2900,11 +3012,12 @@ ] }, "minimumWebview2Version": { - "description": "Try to ensure that the WebView2 version is equal to or newer than this version,\nif the user's WebView2 is older than this version,\nthe installer will try to trigger a WebView2 update.", + "description": "Deprecated: use [`WindowsConfig::minimum_webview2_version`] (`bundle > windows > minimumWebview2Version`) instead.\n\nTry to ensure that the WebView2 version is equal to or newer than this version,\nif the user's WebView2 is older than this version,\nthe installer will try to trigger a WebView2 update.", "type": [ "string", "null" - ] + ], + "deprecated": true } }, "additionalProperties": false @@ -3685,6 +3798,13 @@ "description": "Whether to automatically increment the `versionCode` on each build.\n\n- If `true`, the generator will try to read the last `versionCode` from\n `tauri.properties` and increment it by 1 for every build.\n- If `false` or not set, it falls back to `version_code` or semver-derived logic.\n\nNote that to use this feature, you should remove `/tauri.properties` from `src-tauri/gen/android/app/.gitignore` so the current versionCode is committed to the repository.", "type": "boolean", "default": false + }, + "debugApplicationIdSuffix": { + "description": "Application ID suffix to append for debug builds.\nThis allows installing debug and release versions side-by-side on the same device.\nExample: \".debug\" will make debug builds use \"com.example.app.debug\" as the application ID.", + "type": [ + "string", + "null" + ] } }, "additionalProperties": false diff --git a/crates/tauri-cli/metadata-v2.json b/crates/tauri-cli/metadata-v2.json index 3e034233271e..9484a7df7caa 100644 --- a/crates/tauri-cli/metadata-v2.json +++ b/crates/tauri-cli/metadata-v2.json @@ -1,9 +1,9 @@ { "cli.js": { - "version": "2.10.1", + "version": "2.11.2", "node": ">= 10.0.0" }, - "tauri": "2.10.3", - "tauri-build": "2.5.6", - "tauri-plugin": "2.5.4" + "tauri": "2.11.2", + "tauri-build": "2.6.2", + "tauri-plugin": "2.6.2" } diff --git a/crates/tauri-cli/schema.json b/crates/tauri-cli/schema.json index 6bd2b3bc87bf..903f05633e63 100644 --- a/crates/tauri-cli/schema.json +++ b/crates/tauri-cli/schema.json @@ -3095,6 +3095,13 @@ "format": "uint32", "maximum": 2100000000.0, "minimum": 1.0 + }, + "debugApplicationIdSuffix": { + "description": "Application ID suffix to append for debug builds.\n This allows installing debug and release versions side-by-side on the same device.\n Example: \".debug\" will make debug builds use \"com.example.app.debug\" as the application ID.", + "type": [ + "string", + "null" + ] } }, "additionalProperties": false diff --git a/crates/tauri-cli/src/acl/permission/ls.rs b/crates/tauri-cli/src/acl/permission/ls.rs index 927d9530f13c..1b81f3cf5113 100644 --- a/crates/tauri-cli/src/acl/permission/ls.rs +++ b/crates/tauri-cli/src/acl/permission/ls.rs @@ -145,6 +145,26 @@ pub fn command(options: Options) -> Result<()> { } } + for command in &manifest.commands { + let slug = command.replace('_', "-"); + for (id, action) in [ + (format!("allow-{slug}"), "Enables"), + (format!("deny-{slug}"), "Denies"), + ] { + if options + .filter + .as_ref() + .map(|f| id.contains(f)) + .unwrap_or(true) + { + permissions.push(format!( + "{prefix}{}\n{action} the {command} command without any pre-configured scope.", + id.cyan(), + )); + } + } + } + if !permissions.is_empty() { println!("{}\n", permissions.join("\n\n")); } diff --git a/crates/tauri-cli/src/acl/permission/new.rs b/crates/tauri-cli/src/acl/permission/new.rs index a5ab5c43c7a6..fbffe5bf4c37 100644 --- a/crates/tauri-cli/src/acl/permission/new.rs +++ b/crates/tauri-cli/src/acl/permission/new.rs @@ -113,6 +113,7 @@ pub fn command(options: Options) -> Result<()> { default: None, set: Vec::new(), permission: vec![permission], + commands: Vec::new(), }) .context("failed to serialize permission")?, ) diff --git a/crates/tauri-cli/src/bundle.rs b/crates/tauri-cli/src/bundle.rs index b09baba1e7b8..a12234056cad 100644 --- a/crates/tauri-cli/src/bundle.rs +++ b/crates/tauri-cli/src/bundle.rs @@ -43,7 +43,7 @@ impl ValueEnum for BundleFormat { } fn to_possible_value(&self) -> Option { - let hide = self.0 == PackageType::Updater; + let hide = (!cfg!(windows) && self.0 == PackageType::Nsis) || self.0 == PackageType::Updater; Some(PossibleValue::new(self.0.short_name()).hide(hide)) } } diff --git a/crates/tauri-cli/src/dev/builtin_dev_server.rs b/crates/tauri-cli/src/dev/builtin_dev_server.rs index b99c358197de..6b2d513d8db0 100644 --- a/crates/tauri-cli/src/dev/builtin_dev_server.rs +++ b/crates/tauri-cli/src/dev/builtin_dev_server.rs @@ -7,8 +7,6 @@ use axum::{ http::{StatusCode, Uri, header}, response::{IntoResponse, Response}, }; -use html5ever::{LocalName, QualName, namespace_url, ns}; -use kuchiki::{NodeRef, traits::TendrilSink}; use std::{ net::{IpAddr, SocketAddr}, path::{Path, PathBuf}, @@ -128,30 +126,14 @@ async fn ws_handler(ws: WebSocketUpgrade, state: State) -> Response } fn inject_address(html_bytes: Vec, address: &SocketAddr) -> Vec { - fn with_html_head(document: &mut NodeRef, f: F) { - if let Ok(ref node) = document.select_first("head") { - f(node.as_node()) - } else { - let node = NodeRef::new_element( - QualName::new(None, ns!(html), LocalName::from("head")), - None, - ); - f(&node); - document.prepend(node) - } - } + let document = tauri_utils::html2::parse_doc(String::from_utf8_lossy(&html_bytes).into_owned()); - let mut document = kuchiki::parse_html() - .one(String::from_utf8_lossy(&html_bytes).into_owned()) - .document_node; - with_html_head(&mut document, |head| { - let script = RELOAD_SCRIPT.replace("{{reload_url}}", &format!("ws://{address}/__tauri_cli")); - let script_el = NodeRef::new_element(QualName::new(None, ns!(html), "script".into()), None); - script_el.append(NodeRef::new_text(script)); - head.prepend(script_el); - }); + tauri_utils::html2::append_script_to_head( + &document, + &RELOAD_SCRIPT.replace("{{reload_url}}", &format!("ws://{address}/__tauri_cli")), + ); - tauri_utils::html::serialize_node(&document) + tauri_utils::html2::serialize_doc(&document) } fn fs_read_scoped(path: PathBuf, scope: &Path) -> crate::Result> { diff --git a/crates/tauri-cli/src/helpers/config.rs b/crates/tauri-cli/src/helpers/config.rs index dccedac8414c..b20b929cb666 100644 --- a/crates/tauri-cli/src/helpers/config.rs +++ b/crates/tauri-cli/src/helpers/config.rs @@ -109,6 +109,8 @@ pub fn nsis_settings(config: NsisConfig) -> tauri_bundler::NsisSettings { header_image: config.header_image, sidebar_image: config.sidebar_image, installer_icon: config.installer_icon, + uninstaller_icon: config.uninstaller_icon, + uninstaller_header_image: config.uninstaller_header_image, install_mode: config.install_mode, languages: config.languages, custom_language_files: config.custom_language_files, @@ -116,6 +118,7 @@ pub fn nsis_settings(config: NsisConfig) -> tauri_bundler::NsisSettings { compression: config.compression, start_menu_folder: config.start_menu_folder, installer_hooks: config.installer_hooks, + #[allow(deprecated)] minimum_webview2_version: config.minimum_webview2_version, } } diff --git a/crates/tauri-cli/src/interface/rust.rs b/crates/tauri-cli/src/interface/rust.rs index 14474900e612..ac3b23dfa571 100644 --- a/crates/tauri-cli/src/interface/rust.rs +++ b/crates/tauri-cli/src/interface/rust.rs @@ -187,6 +187,7 @@ impl Rust { self.app_settings.clone() } + #[allow(dead_code)] pub(crate) fn app_settings_ref(&self) -> &RustAppSettings { self.app_settings.as_ref() } @@ -713,6 +714,24 @@ impl BinarySettings { pub fn file_name(&self) -> &str { self.filename.as_ref().unwrap_or(&self.name) } + + fn required_features_enabled(&self, enabled_features: &[String]) -> bool { + match &self.required_features { + Some(req_features) => req_features + .iter() + .all(|feat| enabled_features.contains(feat)), + None => true, + } + } + + fn matches_src_bin(&self, name: &str, path: &Path) -> bool { + self.name == name + || self.file_name() == name + || self + .path + .as_ref() + .is_some_and(|src_path| path.ends_with(src_path)) + } } /// The package settings. @@ -955,6 +974,7 @@ impl AppSettings for RustAppSettings { fn get_binaries(&self, options: &Options, tauri_dir: &Path) -> crate::Result> { let mut binaries = Vec::new(); + let mut disabled_bins = Vec::new(); if let Some(bins) = &self.cargo_settings.bin { let default_run = self @@ -963,14 +983,9 @@ impl AppSettings for RustAppSettings { .clone() .unwrap_or_default(); for bin in bins { - if let Some(req_features) = &bin.required_features { - // Check if all required features are enabled. - if !req_features - .iter() - .all(|feat| options.features.contains(feat)) - { - continue; - } + if !bin.required_features_enabled(&options.features) { + disabled_bins.push(bin); + continue; } let file_name = bin.file_name(); let is_main = file_name == self.cargo_package_settings.name || file_name == default_run; @@ -1018,7 +1033,10 @@ impl AppSettings for RustAppSettings { let bin_exists = binaries .iter() .any(|bin| bin.name() == name || path.ends_with(bin.src_path().unwrap_or(&"".to_string()))); - if !bin_exists { + let bin_disabled = disabled_bins + .iter() + .any(|bin| bin.matches_src_bin(&name, &path)); + if !bin_exists && !bin_disabled { binaries.push(BundleBinary::new(name, false)) } } @@ -1710,6 +1728,8 @@ pub(crate) fn tauri_config_to_bundle_settings( webview_install_mode: config.windows.webview_install_mode, allow_downgrades: config.windows.allow_downgrades, sign_command: config.windows.sign_command.map(custom_sign_settings), + minimum_webview2_version: config.windows.minimum_webview2_version, + bundle_vc_runtime: config.windows.bundle_vc_runtime, }, license: config.license.or_else(|| { settings @@ -1843,6 +1863,44 @@ mod pkgconfig_utils { #[cfg(test)] mod tests { use super::*; + use std::fs; + + fn app_settings_with_manifest(cargo_toml: &str) -> (tempfile::TempDir, RustAppSettings) { + let temp_dir = tempfile::tempdir().unwrap(); + let tauri_dir = temp_dir.path().to_path_buf(); + fs::create_dir_all(tauri_dir.join("src/bin")).unwrap(); + fs::write(tauri_dir.join("Cargo.toml"), cargo_toml).unwrap(); + fs::write(tauri_dir.join("src/main.rs"), "").unwrap(); + fs::write(tauri_dir.join("src/bin/generate-bindings.rs"), "").unwrap(); + + let cargo_settings = CargoSettings::load(&tauri_dir).unwrap(); + let cargo_package_settings = cargo_settings.package.clone().unwrap(); + let package_settings = PackageSettings { + product_name: cargo_package_settings.name.clone(), + version: "0.1.0".into(), + description: String::new(), + homepage: None, + authors: None, + default_run: cargo_package_settings.default_run.clone(), + }; + + let target_triple = "x86_64-unknown-linux-gnu".to_string(); + + ( + temp_dir, + RustAppSettings { + manifest: Mutex::new(Manifest::default()), + cargo_settings, + cargo_package_settings, + cargo_ws_package_settings: None, + package_settings, + cargo_config: CargoConfig::default(), + target_triple: target_triple.clone(), + target_platform: TargetPlatform::from_triple(&target_triple), + workspace_dir: tauri_dir, + }, + ) + } #[test] fn parse_cargo_option() { @@ -1863,6 +1921,41 @@ mod tests { assert_eq!(get_cargo_option(&args, "--non-existent"), None); } + #[test] + fn get_binaries_ignores_src_bin_with_disabled_required_features() { + let cargo_toml = r#" + [package] + name = "app" + version = "0.1.0" + default-run = "app" + + [[bin]] + name = "generate-bindings" + path = "src/bin/generate-bindings.rs" + required-features = ["bindings"] + "#; + + let (temp_dir, app_settings) = app_settings_with_manifest(cargo_toml); + let tauri_dir = temp_dir.path(); + + let binaries = app_settings + .get_binaries(&Options::default(), tauri_dir) + .unwrap(); + assert!(binaries.iter().any(|bin| bin.name() == "app" && bin.main())); + assert!(!binaries.iter().any(|bin| bin.name() == "generate-bindings")); + + let binaries = app_settings + .get_binaries( + &Options { + features: vec!["bindings".into()], + ..Default::default() + }, + tauri_dir, + ) + .unwrap(); + assert!(binaries.iter().any(|bin| bin.name() == "generate-bindings")); + } + #[test] fn parse_profile_from_opts() { let options = Options { diff --git a/crates/tauri-cli/src/interface/rust/desktop.rs b/crates/tauri-cli/src/interface/rust/desktop.rs index 7ccfa85e4353..d21a7910ca96 100644 --- a/crates/tauri-cli/src/interface/rust/desktop.rs +++ b/crates/tauri-cli/src/interface/rust/desktop.rs @@ -194,10 +194,6 @@ pub fn build( let out_dir = app_settings.out_dir(&options, tauri_dir)?; let bin_path = app_settings.app_binary_path(&options, tauri_dir)?; - if std::env::var_os("STATIC_VCRUNTIME").is_none_or(|v| v != "false") { - unsafe { std::env::set_var("STATIC_VCRUNTIME", "true") }; - } - if options.target == Some("universal-apple-darwin".into()) { std::fs::create_dir_all(&out_dir) .fs_context("failed to create project out directory", out_dir.clone())?; diff --git a/crates/tauri-cli/src/migrate/migrations/v1/frontend.rs b/crates/tauri-cli/src/migrate/migrations/v1/frontend.rs index 33d9adbe153b..1bafe5911ae2 100644 --- a/crates/tauri-cli/src/migrate/migrations/v1/frontend.rs +++ b/crates/tauri-cli/src/migrate/migrations/v1/frontend.rs @@ -245,7 +245,7 @@ fn migrate_imports<'a>( // to: // ``` // import * as dialog from "@tauri-apps/plugin-dialog" - // import * as cli as superCli from "@tauri-apps/plugin-cli" + // import * as superCli from "@tauri-apps/plugin-cli" // ``` import if PLUGINIFIED_MODULES.contains(&import) && module == "@tauri-apps/api" => { let js_plugin: &str = MODULES_MAP[&format!("@tauri-apps/api/{import}")]; @@ -255,9 +255,7 @@ fn migrate_imports<'a>( if specifier.local.name.as_str() != import { let local = &specifier.local.name; - imports_to_add.push(format!( - "\nimport * as {import} as {local} from \"{js_plugin}\"" - )); + imports_to_add.push(format!("\nimport * as {local} from \"{js_plugin}\"")); } else { imports_to_add.push(format!("\nimport * as {import} from \"{js_plugin}\"")); }; @@ -359,6 +357,39 @@ mod tests { use super::*; use pretty_assertions::assert_eq; + fn assert_migrated_output_parses(path: &Path, source: &str) { + let has_partial_js = path + .extension() + .is_some_and(|ext| ext == "vue" || ext == "svelte"); + + let sources = if !has_partial_js { + vec![(SourceType::from_path(path).unwrap(), source.to_string())] + } else { + partial_loader::PartialLoader::parse( + path + .extension() + .unwrap_or_default() + .to_str() + .unwrap_or_default(), + source, + ) + .unwrap() + .into_iter() + .map(|s| (s.source_type, s.source_text.to_string())) + .collect() + }; + + for (source_type, script_source) in sources { + let allocator = Allocator::default(); + let ret = Parser::new(&allocator, &script_source, source_type).parse(); + assert!( + ret.errors.is_empty(), + "migrated output did not parse: {:?}", + ret.errors + ); + } + } + #[test] fn migrates_vue() { let input = r#" @@ -404,7 +435,7 @@ mod tests { import * as fs from "@tauri-apps/plugin-fs"; import "./App.css"; import * as dialog from "@tauri-apps/plugin-dialog" -import * as cli as superCli from "@tauri-apps/plugin-cli" +import * as superCli from "@tauri-apps/plugin-cli" const appWindow = getCurrentWebviewWindow() @@ -428,6 +459,7 @@ const appWindow = getCurrentWebviewWindow() .unwrap(); assert_eq!(migrated, expected); + assert_migrated_output_parses(Path::new("file.vue"), &migrated); assert_eq!( new_plugins, @@ -479,7 +511,7 @@ const appWindow = getCurrentWebviewWindow() import * as fs from "@tauri-apps/plugin-fs"; import "./App.css"; import * as dialog from "@tauri-apps/plugin-dialog" -import * as cli as superCli from "@tauri-apps/plugin-cli" +import * as superCli from "@tauri-apps/plugin-cli" const appWindow = getCurrentWebviewWindow() "#; @@ -496,6 +528,7 @@ const appWindow = getCurrentWebviewWindow() .unwrap(); assert_eq!(migrated, expected); + assert_migrated_output_parses(Path::new("file.svelte"), &migrated); assert_eq!( new_plugins, @@ -598,7 +631,7 @@ import { Store } from "@tauri-apps/plugin-store"; import Database from "@tauri-apps/plugin-sql"; import "./App.css"; import * as dialog from "@tauri-apps/plugin-dialog" -import * as cli as superCli from "@tauri-apps/plugin-cli" +import * as superCli from "@tauri-apps/plugin-cli" const appWindow = getCurrentWebviewWindow() function App() { @@ -670,6 +703,7 @@ export default App; .unwrap(); assert_eq!(migrated, expected); + assert_migrated_output_parses(Path::new("file.js"), &migrated); assert_eq!( new_plugins, diff --git a/crates/tauri-cli/src/mobile/android/android_studio_script.rs b/crates/tauri-cli/src/mobile/android/android_studio_script.rs index 189d4f8fd777..f007bb78a64b 100644 --- a/crates/tauri-cli/src/mobile/android/android_studio_script.rs +++ b/crates/tauri-cli/src/mobile/android/android_studio_script.rs @@ -13,7 +13,7 @@ use crate::{ use clap::{ArgAction, Parser}; use cargo_mobile2::{ - android::{adb, target::Target}, + android::{adb, device::ConnectionStatus, target::Target}, opts::Profile, target::{TargetTrait, call_for_targets_with_fallback}, }; @@ -192,7 +192,11 @@ fn adb_forward_port( let forward = format!("tcp:{port}"); log::info!("Forwarding port {port} with adb"); - let mut devices = adb::device_list(env).unwrap_or_default(); + let mut devices = adb::device_list(env) + .unwrap_or_default() + .into_iter() + .filter(|d| d.status() == ConnectionStatus::Connected) + .collect::>(); // if we could not detect any running device, let's wait a few seconds, it might be booting up if devices.is_empty() { log::warn!( @@ -204,7 +208,11 @@ fn adb_forward_port( loop { std::thread::sleep(std::time::Duration::from_secs(1)); - devices = adb::device_list(env).unwrap_or_default(); + devices = adb::device_list(env) + .unwrap_or_default() + .into_iter() + .filter(|d| d.status() == ConnectionStatus::Connected) + .collect::>(); if !devices.is_empty() { break; } diff --git a/crates/tauri-cli/src/mobile/android/build.rs b/crates/tauri-cli/src/mobile/android/build.rs index b0e3ce615933..030ec8eb7775 100644 --- a/crates/tauri-cli/src/mobile/android/build.rs +++ b/crates/tauri-cli/src/mobile/android/build.rs @@ -4,7 +4,7 @@ use super::{ MobileTarget, OptionsHandle, configure_cargo, delete_codegen_vars, ensure_init, env, get_app, - get_config, inject_resources, log_finished, open_and_wait, + get_config, inject_resources, log_finished, open_and_wait, sync_debug_application_id_suffix, }; use crate::{ ConfigValue, Error, Result, @@ -192,6 +192,7 @@ pub fn run( configure_cargo(&mut env, &config)?; generate_tauri_properties(&config, tauri_config, false)?; + sync_debug_application_id_suffix(&config, tauri_config)?; crate::build::setup(&interface, &mut build_options, tauri_config, dirs, true)?; diff --git a/crates/tauri-cli/src/mobile/android/dev.rs b/crates/tauri-cli/src/mobile/android/dev.rs index 3799063e1d23..7c2c6c6bf9ff 100644 --- a/crates/tauri-cli/src/mobile/android/dev.rs +++ b/crates/tauri-cli/src/mobile/android/dev.rs @@ -4,7 +4,7 @@ use super::{ MobileTarget, configure_cargo, delete_codegen_vars, device_prompt, ensure_init, env, get_app, - get_config, inject_resources, open_and_wait, + get_config, inject_resources, open_and_wait, sync_debug_application_id_suffix, }; use crate::{ ConfigValue, Error, Result, @@ -276,6 +276,7 @@ fn run_dev( configure_cargo(&mut env, config)?; generate_tauri_properties(config, &tauri_config, true)?; + sync_debug_application_id_suffix(config, &tauri_config)?; let installed_targets = crate::interface::rust::installation::installed_targets().unwrap_or_default(); @@ -341,7 +342,15 @@ fn run_dev( if open { open_and_wait(config, &env) } else if let Some(device) = &device { - match run(device, options, config, &env, metadata, noise_level) { + match run( + device, + options, + config, + &env, + metadata, + noise_level, + tauri_config, + ) { Ok(c) => Ok(Box::new(c) as Box), Err(e) => { crate::dev::kill_before_dev_process(); @@ -363,6 +372,7 @@ fn run( env: &Env, metadata: &AndroidMetadata, noise_level: NoiseLevel, + tauri_config: &tauri_utils::config::Config, ) -> crate::Result { let profile = if options.debug { Profile::Debug @@ -372,8 +382,18 @@ fn run( let build_app_bundle = metadata.asset_packs().is_some(); + let application_id_suffix = if profile == Profile::Debug { + tauri_config + .bundle + .android + .debug_application_id_suffix + .clone() + } else { + None + }; + device - .run( + .run_with_application_id_suffix( config, env, noise_level, @@ -385,7 +405,8 @@ fn run( }), build_app_bundle, false, - ".MainActivity".into(), + format!("{}.MainActivity", config.app().identifier()), + application_id_suffix, ) .map(DevChild::new) .context("failed to run Android app") diff --git a/crates/tauri-cli/src/mobile/android/mod.rs b/crates/tauri-cli/src/mobile/android/mod.rs index a372a25f488f..2c29ce2336bf 100644 --- a/crates/tauri-cli/src/mobile/android/mod.rs +++ b/crates/tauri-cli/src/mobile/android/mod.rs @@ -6,7 +6,7 @@ use cargo_mobile2::{ android::{ adb, config::{Config as AndroidConfig, Metadata as AndroidMetadata, Raw as RawAndroidConfig}, - device::Device, + device::{ConnectionStatus, Device}, emulator, env::Env, target::Target, @@ -25,6 +25,7 @@ use std::{ io::Cursor, path::{Path, PathBuf}, process::{Command, exit}, + sync::OnceLock, thread::sleep, time::Duration, }; @@ -192,6 +193,247 @@ pub fn get_config( (config, metadata) } +fn sync_debug_application_id_suffix( + config: &AndroidConfig, + tauri_config: &TauriConfig, +) -> Result<()> { + let build_gradle_path = config.project_dir().join("app").join("build.gradle.kts"); + let build_gradle = std::fs::read_to_string(&build_gradle_path).fs_context( + "failed to read Android Gradle build file", + build_gradle_path.clone(), + )?; + let Some(updated_build_gradle) = set_debug_application_id_suffix( + &build_gradle, + tauri_config + .bundle + .android + .debug_application_id_suffix + .as_deref(), + ) else { + crate::error::bail!( + "Could not find the Android debug build type in {}. Add a `getByName(\"debug\")` build type or run `tauri android init` to regenerate the Android project.", + build_gradle_path.display() + ); + }; + + if updated_build_gradle != build_gradle { + write(&build_gradle_path, updated_build_gradle).fs_context( + "failed to write Android Gradle build file", + build_gradle_path, + )?; + } + + Ok(()) +} + +fn set_debug_application_id_suffix(build_gradle: &str, suffix: Option<&str>) -> Option { + static DEBUG_BUILD_TYPE_RE: OnceLock = OnceLock::new(); + + let debug_build_type_re = DEBUG_BUILD_TYPE_RE.get_or_init(|| { + regex::Regex::new(r#"(?m)(?:\bgetByName\(\s*"debug"\s*\)|\bdebug\b)\s*\{"#) + .expect("valid debug build type regex") + }); + + for build_type_match in debug_build_type_re.find_iter(build_gradle) { + let Some(opening_brace) = build_gradle[build_type_match.start()..] + .find('{') + .map(|index| build_type_match.start() + index) + else { + continue; + }; + let Some(closing_brace) = find_matching_brace(build_gradle, opening_brace) else { + continue; + }; + + let debug_block = &build_gradle[opening_brace..closing_brace]; + let updated_debug_block = set_application_id_suffix_in_block(debug_block, suffix); + let mut updated_build_gradle = + String::with_capacity(build_gradle.len() + updated_debug_block.len()); + updated_build_gradle.push_str(&build_gradle[..opening_brace]); + updated_build_gradle.push_str(&updated_debug_block); + updated_build_gradle.push_str(&build_gradle[closing_brace..]); + return Some(updated_build_gradle); + } + + None +} + +fn set_application_id_suffix_in_block(debug_block: &str, suffix: Option<&str>) -> String { + static APPLICATION_ID_SUFFIX_RE: OnceLock = OnceLock::new(); + + let application_id_suffix_re = APPLICATION_ID_SUFFIX_RE.get_or_init(|| { + regex::Regex::new(r#"(?m)^[ \t]*applicationIdSuffix\s*=.*(?:\r?\n)?"#) + .expect("valid applicationIdSuffix regex") + }); + + if let Some(application_id_suffix_match) = application_id_suffix_re.find(debug_block) { + let mut updated_debug_block = String::with_capacity(debug_block.len()); + updated_debug_block.push_str(&debug_block[..application_id_suffix_match.start()]); + if let Some(suffix) = suffix { + let indentation = debug_block + [application_id_suffix_match.start()..application_id_suffix_match.end()] + .chars() + .take_while(|character| *character == ' ' || *character == '\t') + .collect::(); + updated_debug_block.push_str(&format!( + "{indentation}applicationIdSuffix = \"{}\"\n", + escape_kotlin_string(suffix) + )); + } + updated_debug_block.push_str(&debug_block[application_id_suffix_match.end()..]); + return updated_debug_block; + } + + let Some(suffix) = suffix else { + return debug_block.to_string(); + }; + + let indentation = debug_block_indentation(debug_block); + let application_id_suffix = format!( + "{indentation}applicationIdSuffix = \"{}\"\n", + escape_kotlin_string(suffix) + ); + + if let Some(first_newline) = debug_block.find('\n') { + let mut updated_debug_block = + String::with_capacity(debug_block.len() + application_id_suffix.len()); + updated_debug_block.push_str(&debug_block[..=first_newline]); + updated_debug_block.push_str(&application_id_suffix); + updated_debug_block.push_str(&debug_block[first_newline + 1..]); + updated_debug_block + } else { + format!("{{\n{application_id_suffix}") + } +} + +fn debug_block_indentation(debug_block: &str) -> &str { + debug_block + .lines() + .skip(1) + .find_map(|line| { + if line.trim().is_empty() { + None + } else { + Some(line.trim_end().trim_end_matches(line.trim_start())) + } + }) + .unwrap_or(" ") +} + +fn find_matching_brace(content: &str, opening_brace: usize) -> Option { + let mut depth = 0u32; + let mut in_line_comment = false; + let mut in_block_comment = false; + let mut in_string = false; + let mut in_raw_string = false; + let mut string_quote = '\0'; + let mut escaped = false; + let mut previous = '\0'; + let mut chars = content[opening_brace..].char_indices().peekable(); + + while let Some((relative_index, character)) = chars.next() { + let index = opening_brace + relative_index; + + if in_line_comment { + if character == '\n' { + in_line_comment = false; + } + previous = character; + continue; + } + + if in_block_comment { + if previous == '*' && character == '/' { + in_block_comment = false; + } + previous = character; + continue; + } + + if in_raw_string { + if content[index..].starts_with("\"\"\"") { + // Consume the remaining two quotes in the Kotlin raw string delimiter. + let _ = chars.next(); + let _ = chars.next(); + in_raw_string = false; + } + previous = character; + continue; + } + + if in_string { + if escaped { + escaped = false; + } else if character == '\\' { + escaped = true; + } else if character == string_quote { + in_string = false; + } + previous = character; + continue; + } + + if character == '/' && chars.peek().is_some_and(|(_, next)| *next == '/') { + in_line_comment = true; + previous = character; + continue; + } + + if character == '/' && chars.peek().is_some_and(|(_, next)| *next == '*') { + in_block_comment = true; + previous = character; + continue; + } + + if content[index..].starts_with("\"\"\"") { + // Consume the remaining two quotes in the Kotlin raw string delimiter. + let _ = chars.next(); + let _ = chars.next(); + in_raw_string = true; + previous = character; + continue; + } + + if character == '"' || character == '\'' { + in_string = true; + string_quote = character; + previous = character; + continue; + } + + if character == '{' { + depth = depth.saturating_add(1); + } else if character == '}' { + depth = depth.saturating_sub(1); + if depth == 0 { + return Some(index); + } + } + + previous = character; + } + + None +} + +fn escape_kotlin_string(value: &str) -> String { + let mut output = String::with_capacity(value.len()); + + for character in value.chars() { + match character { + '"' => output.push_str("\\\""), + '\\' => output.push_str("\\\\"), + '$' => output.push_str("\\$"), + '\n' => output.push_str("\\n"), + '\r' => output.push_str("\\r"), + '\t' => output.push_str("\\t"), + other => output.push(other), + } + } + + output +} + pub fn env(non_interactive: bool) -> Result { let env = super::env().context("failed to setup Android environment")?; ensure_env(non_interactive).context("failed to ensure Android environment")?; @@ -457,7 +699,11 @@ fn delete_codegen_vars() { } fn adb_device_prompt<'a>(env: &'_ Env, target: Option<&str>) -> Result> { - let device_list = adb::device_list(env).context("failed to detect connected Android devices")?; + let device_list = adb::device_list(env) + .context("failed to detect connected Android devices")? + .into_iter() + .filter(|d| d.status() == ConnectionStatus::Connected) + .collect::>(); if !device_list.is_empty() { let device = if let Some(t) = target { let (device, score) = device_list @@ -543,31 +789,174 @@ fn emulator_prompt(env: &'_ Env, target: Option<&str>) -> Result(env: &'_ Env, target: Option<&str>) -> Result> { if let Ok(device) = adb_device_prompt(env, target) { Ok(device) } else { let emulator = emulator_prompt(env, target)?; - log::info!("Starting emulator {}", emulator.name()); - emulator - .start_detached(env) - .context("failed to start emulator")?; + let emulator_status = match adb::device_list(env) { + Ok(devices) => { + // emulator might be running but disconnected from adb + devices + .iter() + .find(|d| d.name() == emulator.name()) + .and_then(|d| match d.status() { + ConnectionStatus::Offline | ConnectionStatus::Unauthorized => { + Some(EmulatorStatus::Offline { + serial_no: d.serial_no().to_string(), + }) + } + ConnectionStatus::Connected => Some(EmulatorStatus::Connected), + _ => None, + }) + } + // failed to get device information, check if the device name matches the emulator name + Err( + adb::device_list::Error::ModelFailed { + serial_no, + error: adb::get_prop::Error::CommandFailed { command: _, error }, + } + | adb::device_list::Error::AbiFailed { + serial_no, + error: adb::get_prop::Error::CommandFailed { command: _, error }, + }, + ) => { + if error.kind() == std::io::ErrorKind::TimedOut { + // if the device name matches the emulator name, the emulator is already running and marked as connected + // but we cannot connect to it + adb::device_name(env, &serial_no).map_or(None, |device_name| { + if device_name == emulator.name() { + Some(EmulatorStatus::Offline { serial_no }) + } else { + None + } + }) + } else { + None + } + } + Err(_) => None, + }; + + let emulator_already_running = emulator_status.is_some(); + match emulator_status { + Some(EmulatorStatus::Offline { serial_no }) => { + // emulator is available but not connected to adb, we must restart it + log::info!("Emulator is not connected, we need to restart it"); + restart_emulator(env, &serial_no, &emulator)?; + } + Some(EmulatorStatus::Connected) => { + // emulator is already connected to adb + // this is technically unreachable because we queried the device list with adb_device_prompt + } + None => { + log::info!("Starting emulator {}", emulator.name()); + emulator + .start_detached(env) + .context("failed to start emulator")?; + } + } + let mut tries = 0; loop { sleep(Duration::from_secs(2)); - if let Ok(device) = adb_device_prompt(env, Some(emulator.name())) { - return Ok(device); + // we do not filter for connected devices to detect emulators that are not connected to our adb anymore + match adb::device_list(env) { + Ok(devices) => { + if let Some(device) = devices.into_iter().find(|d| d.name() == emulator.name()) + && device.status() == ConnectionStatus::Connected + { + return Ok(device); + } + + if tries >= 3 { + log::info!( + "Waiting for emulator to start... (maybe the emulator is unauthorized or offline, run `adb devices` to check)" + ); + } else { + log::info!("Waiting for emulator to start..."); + } + tries += 1; + } + Err( + adb::device_list::Error::ModelFailed { + serial_no, + error: adb::get_prop::Error::CommandFailed { command: _, error }, + } + | adb::device_list::Error::AbiFailed { + serial_no, + error: adb::get_prop::Error::CommandFailed { command: _, error }, + }, + ) => { + if emulator_already_running && error.kind() == std::io::ErrorKind::TimedOut { + log::info!("Emulator is not responding, we need to restart it"); + restart_emulator(env, &serial_no, &emulator)?; + tries = 0; + } else { + log::error!("failed to get properties for device {serial_no}: {error}"); + } + } + Err(e) => { + log::error!("failed to list devices with adb: {e}"); + tries += 1; + } } - if tries >= 3 { - log::info!( - "Waiting for emulator to start... (maybe the emulator is unauthorized or offline, run `adb devices` to check)" - ); - } else { - log::info!("Waiting for emulator to start..."); + } + } +} + +fn restart_emulator(env: &Env, serial_no: &str, emulator: &emulator::Emulator) -> Result<()> { + let granted_permission_to_restart = + crate::helpers::prompts::confirm("Do you want to restart the emulator?", Some(true)) + .unwrap_or_default(); + if !granted_permission_to_restart { + crate::error::bail!( + "Cannot connect to the emulator, please restart it manually (a full boot might be required)" + ); + } + + adb::adb(env, &["-s", serial_no, "emu", "kill"]) + .run() + .context("failed to reboot emulator")?; + + log::info!("Waiting for emulator to exit..."); + loop { + let devices = adb::device_list(env).unwrap_or_default(); + if devices + .into_iter() + .find(|d| d.serial_no() == serial_no) + .is_none() + { + break; + } + sleep(Duration::from_secs(1)); + } + + log::info!("Restarting emulator with full boot"); + let mut tries = 0; + loop { + // wait a bit to make sure we can restart the emulator + sleep(Duration::from_secs(2)); + + match emulator.start_detached_with_options(env, emulator::StartOptions::new().full_boot()) { + Ok(_) => break, + Err(e) => { + tries += 1; + if tries >= 3 { + return Err(e).context("failed to start emulator"); + } else { + log::error!("failed to start emulator, retrying..."); + } } - tries += 1; } } + + Ok(()) } fn detect_target_ok<'a>(env: &Env) -> Option<&'a Target<'a>> { @@ -699,3 +1088,129 @@ fn generate_tauri_properties( Ok(()) } + +#[cfg(test)] +mod tests { + use super::{find_matching_brace, set_debug_application_id_suffix}; + + #[test] + fn writes_debug_application_id_suffix() { + let build_gradle = r#" +android { + buildTypes { + getByName("debug") { + manifestPlaceholders["usesCleartextTraffic"] = "true" + } + } +} +"#; + + let updated = set_debug_application_id_suffix(build_gradle, Some(".debug")).unwrap(); + + assert!(updated.contains( + r#" getByName("debug") { + applicationIdSuffix = ".debug" + manifestPlaceholders["usesCleartextTraffic"] = "true""# + )); + } + + #[test] + fn replaces_debug_application_id_suffix() { + let build_gradle = r#" +android { + buildTypes { + getByName("debug") { + applicationIdSuffix = ".old" + manifestPlaceholders["usesCleartextTraffic"] = "true" + } + } +} +"#; + + let updated = set_debug_application_id_suffix(build_gradle, Some(".internal")).unwrap(); + + assert!(updated.contains(r#" applicationIdSuffix = ".internal""#)); + assert!(!updated.contains(r#".old"#)); + } + + #[test] + fn removes_debug_application_id_suffix() { + let build_gradle = r#" +android { + buildTypes { + getByName("debug") { + applicationIdSuffix = ".debug" + } + getByName("release") { + applicationIdSuffix = ".release" + } + } +} +"#; + + let updated = set_debug_application_id_suffix(build_gradle, None).unwrap(); + + assert!(!updated.contains(r#"applicationIdSuffix = ".debug""#)); + assert!(updated.contains(r#"applicationIdSuffix = ".release""#)); + } + + #[test] + fn writes_debug_suffix_before_nested_blocks() { + let build_gradle = r#" +android { + buildTypes { + debug { + packaging { + jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so") + } + } + } +} +"#; + + let updated = set_debug_application_id_suffix(build_gradle, Some(".internal")).unwrap(); + + assert!(updated.contains( + r#" debug { + applicationIdSuffix = ".internal" + packaging {"# + )); + } + + #[test] + fn ignores_braces_inside_kotlin_raw_strings() { + let build_gradle = r#" +android { + buildTypes { + debug { + val proguardRules = """ + -if class ** { + public *; + } + """ + manifestPlaceholders["usesCleartextTraffic"] = "true" + } + } +} +"#; + + let opening_brace = build_gradle + .find("debug {") + .and_then(|index| build_gradle[index..].find('{').map(|brace| index + brace)) + .unwrap(); + let closing_brace = find_matching_brace(build_gradle, opening_brace).unwrap(); + + assert!( + build_gradle[opening_brace..closing_brace] + .contains(r#"manifestPlaceholders["usesCleartextTraffic"] = "true""#) + ); + + let updated = set_debug_application_id_suffix(build_gradle, Some(".debug")).unwrap(); + + assert!(updated.contains( + r#" debug { + applicationIdSuffix = ".debug" + val proguardRules = """"# + )); + } +} diff --git a/crates/tauri-cli/src/mobile/android/run.rs b/crates/tauri-cli/src/mobile/android/run.rs index 545be0025217..726d62ef8b45 100644 --- a/crates/tauri-cli/src/mobile/android/run.rs +++ b/crates/tauri-cli/src/mobile/android/run.rs @@ -10,7 +10,7 @@ use cargo_mobile2::{ use clap::{ArgAction, Parser}; use std::path::PathBuf; -use super::{configure_cargo, device_prompt, env}; +use super::{configure_cargo, device_prompt, env, sync_debug_application_id_suffix}; use crate::{ ConfigValue, Result, error::Context, @@ -127,9 +127,22 @@ pub fn command(options: Options, noise_level: NoiseLevel) -> Result<()> { if let Some(device) = device { let config = built_application.config.clone(); let release = options.release; - let runner = move |_tauri_config: &ConfigMetadata| { + + let runner = move |tauri_config: &ConfigMetadata| { + sync_debug_application_id_suffix(&config, tauri_config)?; + + let application_id_suffix = if !release { + tauri_config + .bundle + .android + .debug_application_id_suffix + .clone() + } else { + None + }; + device - .run( + .run_with_application_id_suffix( &config, &env, noise_level, @@ -145,7 +158,8 @@ pub fn command(options: Options, noise_level: NoiseLevel) -> Result<()> { }), false, false, - ".MainActivity".into(), + format!("{}.MainActivity", config.app().identifier()), + application_id_suffix, ) .map(|c| Box::new(DevChild::new(c)) as Box) .context("failed to run Android app") diff --git a/crates/tauri-cli/src/mobile/init.rs b/crates/tauri-cli/src/mobile/init.rs index 48389dbea8fc..50f8babcc65b 100644 --- a/crates/tauri-cli/src/mobile/init.rs +++ b/crates/tauri-cli/src/mobile/init.rs @@ -140,6 +140,13 @@ fn exec( let (config, metadata) = super::android::get_config(&app, &tauri_config, &[], &Default::default()); map.insert("android", &config); + + // Add application_id_suffix to the map for template access + // The template will access it via a helper or we'll modify template to use root context + if let Some(suffix) = &tauri_config.bundle.android.debug_application_id_suffix { + map.insert("android-debug-application-id-suffix", suffix); + } + super::android::project::generate( &config, &metadata, diff --git a/crates/tauri-cli/src/mobile/ios/build.rs b/crates/tauri-cli/src/mobile/ios/build.rs index 8242170350d5..06dc396cd8db 100644 --- a/crates/tauri-cli/src/mobile/ios/build.rs +++ b/crates/tauri-cli/src/mobile/ios/build.rs @@ -36,7 +36,7 @@ use rand::distr::{Alphanumeric, SampleString}; use std::{ env::{set_current_dir, var, var_os}, fs, - path::PathBuf, + path::{Path, PathBuf}, }; #[derive(Debug, Clone, Parser)] @@ -94,6 +94,12 @@ pub struct Options { /// Only use this when you are sure the mismatch is incorrectly detected as version mismatched Tauri packages can lead to unknown behavior. #[clap(long)] pub ignore_version_mismatches: bool, + /// Skip code signing when bundling the app + #[clap(long)] + pub no_sign: bool, + /// Only archive the app, skip generating the IPA. + #[clap(long)] + pub archive_only: bool, /// Target device of this build #[clap(skip)] pub target_device: Option, @@ -154,7 +160,7 @@ impl From for BuildOptions { ci: options.ci, skip_stapling: false, ignore_version_mismatches: options.ignore_version_mismatches, - no_sign: false, + no_sign: options.no_sign, } } } @@ -243,8 +249,15 @@ pub fn run(options: Options, noise_level: NoiseLevel, dirs: &Dirs) -> Result Result<()> { + let ipa_file = + fs::File::create(ipa_path).fs_context("failed to create IPA file", ipa_path.to_path_buf())?; + let mut zip = zip::ZipWriter::new(ipa_file); + let options = zip::write::SimpleFileOptions::default() + .compression_method(zip::CompressionMethod::Deflated) + .unix_permissions(0o755); + + zip + .add_directory("Payload/", options) + .context("failed to add Payload directory to zip")?; + + let mut app_files = Vec::new(); + let mut stack = vec![app_path.to_path_buf()]; + while let Some(path) = stack.pop() { + if path.is_dir() { + app_files.push(path.clone()); + for entry in fs::read_dir(&path).fs_context("failed to read directory", path.clone())? { + stack.push( + entry + .fs_context("failed to read directory entry", path.clone())? + .path(), + ); + } + } else { + app_files.push(path); + } + } + + for file_path in app_files { + let name = file_path.strip_prefix(app_path.parent().unwrap()).unwrap(); + let mut name_str = name.to_string_lossy().to_string(); + // zip expects forward slashes + if std::path::MAIN_SEPARATOR == '\\' { + name_str = name_str.replace('\\', "/"); + } + let mut name_in_zip = format!("Payload/{name_str}"); + + if file_path.is_dir() { + name_in_zip.push('/'); + zip + .add_directory(name_in_zip, options) + .context("failed to add directory to zip")?; + } else { + zip + .start_file(name_in_zip, options) + .context("failed to start file in zip")?; + let mut f = fs::File::open(&file_path).fs_context("failed to open file", file_path)?; + std::io::copy(&mut f, &mut zip).context("failed to copy file to zip")?; + } + } + + zip.finish().context("failed to finish zip")?; + Ok(()) +} + fn auth_credentials_from_env() -> Result> { match ( var("APPLE_API_KEY"), diff --git a/crates/tauri-cli/src/mobile/ios/dev.rs b/crates/tauri-cli/src/mobile/ios/dev.rs index 99f56c5ee115..8c897ec2557b 100644 --- a/crates/tauri-cli/src/mobile/ios/dev.rs +++ b/crates/tauri-cli/src/mobile/ios/dev.rs @@ -232,8 +232,15 @@ fn run_command(options: Options, noise_level: NoiseLevel, dirs: Dirs) -> Result< if dirs.tauri.join("Info.ios.plist").exists() { src_plists.push(dirs.tauri.join("Info.ios.plist").into()); } - if let Some(info_plist) = &tauri_config.bundle.ios.info_plist { - src_plists.push(info_plist.clone().into()); + { + if let Some(info_plist) = &tauri_config.bundle.ios.info_plist { + src_plists.push(info_plist.clone().into()); + } + if let Some(associations) = tauri_config.bundle.file_associations.as_ref() + && let Some(file_associations) = tauri_utils::config::file_associations_plist(associations) + { + src_plists.push(file_associations.into()); + } } let merged_info_plist = merge_plist(src_plists)?; merged_info_plist diff --git a/crates/tauri-cli/src/mobile/ios/run.rs b/crates/tauri-cli/src/mobile/ios/run.rs index bf712ed62768..a40da843764d 100644 --- a/crates/tauri-cli/src/mobile/ios/run.rs +++ b/crates/tauri-cli/src/mobile/ios/run.rs @@ -88,6 +88,8 @@ pub fn command(options: Options, noise_level: NoiseLevel) -> Result<()> { export_method: None, args: options.args, ignore_version_mismatches: options.ignore_version_mismatches, + no_sign: false, + archive_only: false, target_device: device.as_ref().map(|d| TargetDevice { id: d.id().to_string(), name: d.name().to_string(), diff --git a/crates/tauri-cli/tauri.config.schema.json b/crates/tauri-cli/tauri.config.schema.json index b6c64b09b99b..ea575a22234f 100644 --- a/crates/tauri-cli/tauri.config.schema.json +++ b/crates/tauri-cli/tauri.config.schema.json @@ -3081,7 +3081,7 @@ "additionalProperties": false }, "AndroidConfig": { - "description": "General configuration for the iOS target.", + "description": "General configuration for the Android target.", "type": "object", "properties": { "minSdkVersion": { @@ -3100,6 +3100,13 @@ "format": "uint32", "maximum": 2100000000.0, "minimum": 1.0 + }, + "debugApplicationIdSuffix": { + "description": "Application ID suffix to append for debug builds.\n This allows installing debug and release versions side-by-side on the same device.\n Example: \".debug\" will make debug builds use \"com.example.app.debug\" as the application ID.", + "type": [ + "string", + "null" + ] } }, "additionalProperties": false diff --git a/crates/tauri-cli/templates/mobile/android/.gitignore b/crates/tauri-cli/templates/mobile/android/.gitignore index b24820317285..1c636c39a750 100644 --- a/crates/tauri-cli/templates/mobile/android/.gitignore +++ b/crates/tauri-cli/templates/mobile/android/.gitignore @@ -14,6 +14,7 @@ build .cxx local.properties key.properties +keystore.properties /.tauri /tauri.settings.gradle \ No newline at end of file diff --git a/crates/tauri-cli/templates/mobile/android/app/build.gradle.kts b/crates/tauri-cli/templates/mobile/android/app/build.gradle.kts index 621f7bcaeabe..0f73195fa008 100644 --- a/crates/tauri-cli/templates/mobile/android/app/build.gradle.kts +++ b/crates/tauri-cli/templates/mobile/android/app/build.gradle.kts @@ -28,6 +28,9 @@ android { } buildTypes { getByName("debug") { + {{#if android-debug-application-id-suffix}} + applicationIdSuffix = "{{android-debug-application-id-suffix}}" + {{/if}} manifestPlaceholders["usesCleartextTraffic"] = "true" isDebuggable = true isJniDebuggable = true @@ -68,6 +71,7 @@ dependencies { implementation("androidx.appcompat:appcompat:1.7.1") implementation("androidx.activity:activity-ktx:1.10.1") implementation("com.google.android.material:material:1.12.0") + implementation("androidx.lifecycle:lifecycle-process:2.10.0") testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.4") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0") diff --git a/crates/tauri-cli/templates/mobile/ios/project.yml b/crates/tauri-cli/templates/mobile/ios/project.yml index 1994d5e5aff7..c23c1949e33d 100644 --- a/crates/tauri-cli/templates/mobile/ios/project.yml +++ b/crates/tauri-cli/templates/mobile/ios/project.yml @@ -78,8 +78,8 @@ targets: ENABLE_BITCODE: false ARCHS: [{{join ios-valid-archs}}] VALID_ARCHS: {{~#each ios-valid-archs}} {{this}} {{/each}} - LIBRARY_SEARCH_PATHS[arch=x86_64]: $(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME) - LIBRARY_SEARCH_PATHS[arch=arm64]: $(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME) + LIBRARY_SEARCH_PATHS[arch=x86_64]: $(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME) + LIBRARY_SEARCH_PATHS[arch=arm64]: $(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME) ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES: true EXCLUDED_ARCHS[sdk=iphoneos*]: x86_64 groups: [app] diff --git a/crates/tauri-cli/templates/plugin/__example-api/tauri-app/package.json b/crates/tauri-cli/templates/plugin/__example-api/tauri-app/package.json index 6217d62bd8e5..c660a10807c1 100644 --- a/crates/tauri-cli/templates/plugin/__example-api/tauri-app/package.json +++ b/crates/tauri-cli/templates/plugin/__example-api/tauri-app/package.json @@ -14,9 +14,9 @@ "tauri-plugin-{{ plugin_name }}-api": "file:../../" }, "devDependencies": { - "@sveltejs/vite-plugin-svelte": "^6.0.0", + "@sveltejs/vite-plugin-svelte": "^7.0.0", "svelte": "^5.0.0", - "vite": "^7.0.0", + "vite": "^8.0.0", "@tauri-apps/cli": "^2.0.0" } } diff --git a/crates/tauri-cli/templates/plugin/package.json b/crates/tauri-cli/templates/plugin/package.json index 1dd60abc81e3..3235e3fd18f9 100644 --- a/crates/tauri-cli/templates/plugin/package.json +++ b/crates/tauri-cli/templates/plugin/package.json @@ -27,7 +27,7 @@ "devDependencies": { "@rollup/plugin-typescript": "^12.0.0", "rollup": "^4.9.6", - "typescript": "^5.3.3", + "typescript": "^6.0.0", "tslib": "^2.6.2" } } diff --git a/crates/tauri-cli/templates/tauri.conf.json b/crates/tauri-cli/templates/tauri.conf.json index 6b8354f78464..048fc0f00d8c 100644 --- a/crates/tauri-cli/templates/tauri.conf.json +++ b/crates/tauri-cli/templates/tauri.conf.json @@ -32,6 +32,9 @@ "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico" - ] + ], + "android": { + "debugApplicationIdSuffix": ".debug" + } } } diff --git a/crates/tauri-cli/tests/fixtures/pbxproj/project.pbxproj b/crates/tauri-cli/tests/fixtures/pbxproj/project.pbxproj index a3c74da6b00d..2604ffdc40a5 100644 --- a/crates/tauri-cli/tests/fixtures/pbxproj/project.pbxproj +++ b/crates/tauri-cli/tests/fixtures/pbxproj/project.pbxproj @@ -398,8 +398,8 @@ "$(inherited)", "@executable_path/Frameworks", ); - "LIBRARY_SEARCH_PATHS[arch=arm64]" = "$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)"; - "LIBRARY_SEARCH_PATHS[arch=x86_64]" = "$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)"; + "LIBRARY_SEARCH_PATHS[arch=arm64]" = "$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)"; + "LIBRARY_SEARCH_PATHS[arch=x86_64]" = "$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)"; PRODUCT_BUNDLE_IDENTIFIER = com.tauri.api; PRODUCT_NAME = "Tauri API"; SDKROOT = iphoneos; @@ -430,8 +430,8 @@ "$(inherited)", "@executable_path/Frameworks", ); - "LIBRARY_SEARCH_PATHS[arch=arm64]" = "$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)"; - "LIBRARY_SEARCH_PATHS[arch=x86_64]" = "$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)"; + "LIBRARY_SEARCH_PATHS[arch=arm64]" = "$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)"; + "LIBRARY_SEARCH_PATHS[arch=x86_64]" = "$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)"; PRODUCT_BUNDLE_IDENTIFIER = com.tauri.api; PRODUCT_NAME = "Tauri API"; SDKROOT = iphoneos; diff --git a/crates/tauri-cli/tests/fixtures/pbxproj/snapshots/tauri_cli__helpers__pbxproj__tests__project-modified.pbxproj.snap b/crates/tauri-cli/tests/fixtures/pbxproj/snapshots/tauri_cli__helpers__pbxproj__tests__project-modified.pbxproj.snap index a3dc6bf5ed43..97ba4c6235c9 100644 --- a/crates/tauri-cli/tests/fixtures/pbxproj/snapshots/tauri_cli__helpers__pbxproj__tests__project-modified.pbxproj.snap +++ b/crates/tauri-cli/tests/fixtures/pbxproj/snapshots/tauri_cli__helpers__pbxproj__tests__project-modified.pbxproj.snap @@ -402,8 +402,8 @@ expression: pbxproj.serialize() "$(inherited)", "@executable_path/Frameworks", ); - "LIBRARY_SEARCH_PATHS[arch=arm64]" = "$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)"; - "LIBRARY_SEARCH_PATHS[arch=x86_64]" = "$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)"; + "LIBRARY_SEARCH_PATHS[arch=arm64]" = "$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)"; + "LIBRARY_SEARCH_PATHS[arch=x86_64]" = "$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)"; PRODUCT_BUNDLE_IDENTIFIER = com.tauri.api; PRODUCT_NAME = "Tauri API"; SDKROOT = iphoneos; @@ -434,8 +434,8 @@ expression: pbxproj.serialize() "$(inherited)", "@executable_path/Frameworks", ); - "LIBRARY_SEARCH_PATHS[arch=arm64]" = "$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)"; - "LIBRARY_SEARCH_PATHS[arch=x86_64]" = "$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)"; + "LIBRARY_SEARCH_PATHS[arch=arm64]" = "$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)"; + "LIBRARY_SEARCH_PATHS[arch=x86_64]" = "$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)"; PRODUCT_BUNDLE_IDENTIFIER = com.tauri.api; PRODUCT_NAME = "Tauri Test"; SDKROOT = iphoneos; diff --git a/crates/tauri-cli/tests/fixtures/pbxproj/snapshots/tauri_cli__helpers__pbxproj__tests__project.pbxproj.snap b/crates/tauri-cli/tests/fixtures/pbxproj/snapshots/tauri_cli__helpers__pbxproj__tests__project.pbxproj.snap index d84a8c0e71ba..24c4db8b9f99 100644 --- a/crates/tauri-cli/tests/fixtures/pbxproj/snapshots/tauri_cli__helpers__pbxproj__tests__project.pbxproj.snap +++ b/crates/tauri-cli/tests/fixtures/pbxproj/snapshots/tauri_cli__helpers__pbxproj__tests__project.pbxproj.snap @@ -708,13 +708,13 @@ Pbxproj { identation: "\t\t\t\t", line_number: 400, key: "\"LIBRARY_SEARCH_PATHS[arch=arm64]\"", - value: "\"$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)\"", + value: "\"$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)\"", }, BuildSettings { identation: "\t\t\t\t", line_number: 401, key: "\"LIBRARY_SEARCH_PATHS[arch=x86_64]\"", - value: "\"$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)\"", + value: "\"$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)\"", }, BuildSettings { identation: "\t\t\t\t", @@ -820,13 +820,13 @@ Pbxproj { identation: "\t\t\t\t", line_number: 432, key: "\"LIBRARY_SEARCH_PATHS[arch=arm64]\"", - value: "\"$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)\"", + value: "\"$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)\"", }, BuildSettings { identation: "\t\t\t\t", line_number: 433, key: "\"LIBRARY_SEARCH_PATHS[arch=x86_64]\"", - value: "\"$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)\"", + value: "\"$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)\"", }, BuildSettings { identation: "\t\t\t\t", diff --git a/crates/tauri-codegen/CHANGELOG.md b/crates/tauri-codegen/CHANGELOG.md index d874ccd7631f..a445a96af25a 100644 --- a/crates/tauri-codegen/CHANGELOG.md +++ b/crates/tauri-codegen/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## \[2.6.2] + +### Dependencies + +- Upgraded to `tauri-utils@2.9.2` + +## \[2.6.1] + +### Dependencies + +- Upgraded to `tauri-utils@2.9.1` + +## \[2.6.0] + +### Dependencies + +- Upgraded to `tauri-utils@2.9.0` +- [`df05c0056`](https://www.github.com/tauri-apps/tauri/commit/df05c00563a91fc936bd15c6b10dd2825472f96b) Upgraded to `tauri-utils@2.9.0` + ## \[2.5.5] ### Dependencies diff --git a/crates/tauri-codegen/Cargo.toml b/crates/tauri-codegen/Cargo.toml index e1154e67687c..9f23170ebc8a 100644 --- a/crates/tauri-codegen/Cargo.toml +++ b/crates/tauri-codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-codegen" -version = "2.5.5" +version = "2.6.2" description = "code generation meant to be consumed inside of `tauri` through `tauri-build` or `tauri-macros`" exclude = ["CHANGELOG.md", "/target"] readme = "README.md" @@ -20,8 +20,8 @@ quote = "1" syn = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" -tauri-utils = { version = "2.8.3", path = "../tauri-utils", features = [ - "build", +tauri-utils = { version = "2.9.2", path = "../tauri-utils", features = [ + "build-2", ] } thiserror = "2" walkdir = "2" diff --git a/crates/tauri-codegen/src/context.rs b/crates/tauri-codegen/src/context.rs index d3996f586135..6b907b9c7691 100644 --- a/crates/tauri-codegen/src/context.rs +++ b/crates/tauri-codegen/src/context.rs @@ -25,7 +25,7 @@ use tauri_utils::{ }, assets::AssetKey, config::{Config, FrontendDist, PatternKind}, - html::{NodeRef, inject_nonce_token, parse as parse_html, serialize_node as serialize_html_node}, + html2::{Document, inject_nonce_token, parse_doc, serialize_doc}, platform::Target, tokens::{map_lit, str_lit}, }; @@ -44,27 +44,25 @@ pub struct ContextData { pub test: bool, } -fn inject_script_hashes(document: &NodeRef, key: &AssetKey, csp_hashes: &mut CspHashes) { - if let Ok(inline_script_elements) = document.select("script:not(:empty)") { - let mut scripts = Vec::new(); - for inline_script_el in inline_script_elements { - let script = inline_script_el.as_node().text_contents(); - let mut hasher = Sha256::new(); - hasher.update(tauri_utils::html::normalize_script_for_csp( - script.as_bytes(), - )); - let hash = hasher.finalize(); - scripts.push(format!( - "'sha256-{}'", - base64::engine::general_purpose::STANDARD.encode(hash) - )); - } - csp_hashes - .inline_scripts - .entry(key.clone().into()) - .or_default() - .append(&mut scripts); - } +fn inject_script_hashes(document: &Document, key: &AssetKey, csp_hashes: &mut CspHashes) { + let script_elements = document.select("script:not(:empty)"); + + let scripts = script_elements + .iter() + .map(|element| { + let script = tauri_utils::html2::normalize_script_for_csp(element.text().as_bytes()); + let script_hash = Sha256::digest(script); + let hash_base64 = base64::engine::general_purpose::STANDARD.encode(script_hash); + + format!("'sha256-{hash_base64}'") + }) + .collect::>(); + + csp_hashes + .inline_scripts + .entry(key.clone().into()) + .or_default() + .extend(scripts); } fn map_core_assets( @@ -77,7 +75,7 @@ fn map_core_assets( if path.extension() == Some(OsStr::new("html")) { #[allow(clippy::collapsible_if)] if csp { - let document = parse_html(String::from_utf8_lossy(input).into_owned()); + let document = parse_doc(String::from_utf8_lossy(input).into_owned()); inject_nonce_token(&document, &dangerous_disable_asset_csp_modification); @@ -85,7 +83,7 @@ fn map_core_assets( inject_script_hashes(&document, key, csp_hashes); } - *input = serialize_html_node(&document); + *input = serialize_doc(&document); } } Ok(()) @@ -108,13 +106,13 @@ fn map_isolation( move |key, path, input, csp_hashes| { if path.extension() == Some(OsStr::new("html")) { - let isolation_html = parse_html(String::from_utf8_lossy(input).into_owned()); + let isolation_html = parse_doc(String::from_utf8_lossy(input).into_owned()); // this is appended, so no need to reverse order it - tauri_utils::html::inject_codegen_isolation_script(&isolation_html); + tauri_utils::html2::inject_codegen_isolation_script(&isolation_html); // temporary workaround for windows not loading assets - tauri_utils::html::inline_isolation(&isolation_html, &dir); + tauri_utils::html2::inline_isolation(&isolation_html, &dir); inject_nonce_token( &isolation_html, @@ -125,7 +123,7 @@ fn map_isolation( csp_hashes.styles.push(iframe_style_csp_hash.clone()); - *input = isolation_html.to_string().as_bytes().to_vec() + *input = serialize_doc(&isolation_html) } Ok(()) diff --git a/crates/tauri-codegen/src/embedded_assets.rs b/crates/tauri-codegen/src/embedded_assets.rs index 33a75ebe953a..a2bc7e876259 100644 --- a/crates/tauri-codegen/src/embedded_assets.rs +++ b/crates/tauri-codegen/src/embedded_assets.rs @@ -182,7 +182,7 @@ impl CspHashes { let mut hasher = Sha256::new(); hasher.update( &std::fs::read(path) - .map(|b| tauri_utils::html::normalize_script_for_csp(&b)) + .map(|b| tauri_utils::html2::normalize_script_for_csp(&b)) .map_err(|error| EmbeddedAssetsError::AssetRead { path: path.to_path_buf(), error, diff --git a/crates/tauri-driver/CHANGELOG.md b/crates/tauri-driver/CHANGELOG.md index 7dc771920114..fd67ede86919 100644 --- a/crates/tauri-driver/CHANGELOG.md +++ b/crates/tauri-driver/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## \[2.0.6] + +### What's Changed + +- [`3057eda06`](https://www.github.com/tauri-apps/tauri/commit/3057eda067b87761644209adeec077f232585c5d) ([#15324](https://www.github.com/tauri-apps/tauri/pull/15324)) Support `eq-separator` for `tauri-driver`. + ## \[2.0.5] ### Bug Fixes diff --git a/crates/tauri-driver/Cargo.toml b/crates/tauri-driver/Cargo.toml index 51613e5d9367..e0a3cb027f75 100644 --- a/crates/tauri-driver/Cargo.toml +++ b/crates/tauri-driver/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-driver" -version = "2.0.5" +version = "2.0.6" authors = ["Tauri Programme within The Commons Conservancy"] categories = ["gui", "web-programming"] license = "Apache-2.0 OR MIT" @@ -24,7 +24,7 @@ hyper-util = { version = "0.1", features = [ "server", "tokio", ] } -pico-args = "0.5" +pico-args = { version = "0.5", features = ["eq-separator"] } serde = { version = "1", features = ["derive"] } serde_json = "1" tokio = { version = "1", features = ["macros"] } diff --git a/crates/tauri-macos-sign/CHANGELOG.md b/crates/tauri-macos-sign/CHANGELOG.md index 577e23e336b4..9502ef75c3bb 100644 --- a/crates/tauri-macos-sign/CHANGELOG.md +++ b/crates/tauri-macos-sign/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## \[2.3.4] + +### Enhancements + +- [`eacd36a4e`](https://www.github.com/tauri-apps/tauri/commit/eacd36a4ea4d6a14a73f414981fb5a8af7dfdafe) ([#15038](https://www.github.com/tauri-apps/tauri/pull/15038)) Do not rely on system base64 CLI to decode certificates. + ## \[2.3.3] ### Dependencies diff --git a/crates/tauri-macos-sign/Cargo.toml b/crates/tauri-macos-sign/Cargo.toml index 0f26ea197e6b..e823400ee65b 100644 --- a/crates/tauri-macos-sign/Cargo.toml +++ b/crates/tauri-macos-sign/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-macos-sign" -version = "2.3.3" +version = "2.3.4" authors = ["Tauri Programme within The Commons Conservancy"] license = "Apache-2.0 OR MIT" keywords = ["codesign", "signing", "macos", "ios", "tauri"] diff --git a/crates/tauri-macros/CHANGELOG.md b/crates/tauri-macros/CHANGELOG.md index 2406a7af5f42..1058cdb099b1 100644 --- a/crates/tauri-macros/CHANGELOG.md +++ b/crates/tauri-macros/CHANGELOG.md @@ -1,5 +1,30 @@ # Changelog +## \[2.6.2] + +### Dependencies + +- Upgraded to `tauri-utils@2.9.2` +- Upgraded to `tauri-codegen@2.6.2` + +## \[2.6.1] + +### Dependencies + +- Upgraded to `tauri-utils@2.9.1` +- Upgraded to `tauri-codegen@2.6.1` + +## \[2.6.0] + +### New Features + +- [`c00a3dbff`](https://www.github.com/tauri-apps/tauri/commit/c00a3dbffccd6e051d3b7332f706b6c63759865d) ([#14473](https://www.github.com/tauri-apps/tauri/pull/14473)) Add support for the `rename` attribute in the `tauri::command` macro to allow renaming the command to something other than the function name. + +### Dependencies + +- Upgraded to `tauri-utils@2.9.0` +- Upgraded to `tauri-codegen@2.6.0` + ## \[2.5.5] ### Dependencies diff --git a/crates/tauri-macros/Cargo.toml b/crates/tauri-macros/Cargo.toml index 29ba5044f629..8b50579b6db1 100644 --- a/crates/tauri-macros/Cargo.toml +++ b/crates/tauri-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-macros" -version = "2.5.5" +version = "2.6.2" description = "Macros for the tauri crate." exclude = ["CHANGELOG.md", "/target"] readme = "README.md" @@ -20,8 +20,8 @@ proc-macro2 = { version = "1", features = ["span-locations"] } quote = "1" syn = { version = "2", features = ["full"] } heck = "0.5" -tauri-codegen = { version = "2.5.5", default-features = false, path = "../tauri-codegen" } -tauri-utils = { version = "2.8.3", path = "../tauri-utils" } +tauri-codegen = { version = "2.6.2", default-features = false, path = "../tauri-codegen" } +tauri-utils = { version = "2.9.2", path = "../tauri-utils" } [features] custom-protocol = [] diff --git a/crates/tauri-macros/src/command/handler.rs b/crates/tauri-macros/src/command/handler.rs index ce745839fa1b..cbbe1573592d 100644 --- a/crates/tauri-macros/src/command/handler.rs +++ b/crates/tauri-macros/src/command/handler.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use quote::format_ident; +use quote::{format_ident, quote}; use syn::{ Attribute, Ident, Path, Token, parse::{Parse, ParseBuffer, ParseStream}, @@ -117,6 +117,15 @@ fn filter_unused_commands(plugin_name: Option, command_defs: &mut Vec for proc_macro::TokenStream { ) -> Self { let cmd = format_ident!("__tauri_cmd__"); let invoke = format_ident!("__tauri_invoke__"); - let (paths, attrs): (Vec, Vec>) = command_defs - .into_iter() - .map(|def| (def.path, def.attrs)) - .unzip(); + let mut paths: Vec = Vec::new(); + let mut attrs: Vec> = Vec::new(); + let mut command_name_macros: Vec = Vec::new(); + for (def, command) in command_defs.into_iter().zip(commands) { + let path = def.path; + let attrs_vec = def.attrs; + + let mut command_name_macro_path = path.clone(); + let last = command_name_macro_path + .segments + .last_mut() + .expect("path has at least one segment"); + last.ident = format_ident!("__tauri_command_name_{command}"); + + paths.push(path); + attrs.push(attrs_vec); + // Call the macro to get the command name string literal + command_name_macros.push(quote!(#command_name_macro_path!())); + } + quote::quote!(move |#invoke| { let #cmd = #invoke.message.command(); match #cmd { - #(#(#attrs)* stringify!(#commands) => #wrappers!(#paths, #invoke),)* + #(#(#attrs)* #command_name_macros => #wrappers!(#paths, #invoke),)* _ => { return false; }, diff --git a/crates/tauri-macros/src/command/wrapper.rs b/crates/tauri-macros/src/command/wrapper.rs index f432ce3ed5e5..d8d213106a63 100644 --- a/crates/tauri-macros/src/command/wrapper.rs +++ b/crates/tauri-macros/src/command/wrapper.rs @@ -40,6 +40,7 @@ struct WrapperAttributes { root: TokenStream2, execution_context: ExecutionContext, argument_case: ArgumentCase, + rename: RenamePolicy, } impl Parse for WrapperAttributes { @@ -48,6 +49,7 @@ impl Parse for WrapperAttributes { root: quote!(::tauri), execution_context: ExecutionContext::Blocking, argument_case: ArgumentCase::Camel, + rename: RenamePolicy::Keep, }; let attrs = Punctuated::::parse_terminated(input)?; @@ -57,23 +59,29 @@ impl Parse for WrapperAttributes { return Err(syn::Error::new(input.span(), "unexpected list input")); } WrapperAttributeKind::Meta(Meta::NameValue(v)) => { - if v.path.is_ident("rename_all") { - if let Expr::Lit(ExprLit { + if v.path.is_ident("rename_all") + && let Expr::Lit(ExprLit { lit: Lit::Str(s), attrs: _, }) = v.value - { - wrapper_attributes.argument_case = match s.value().as_str() { - "snake_case" => ArgumentCase::Snake, - "camelCase" => ArgumentCase::Camel, - _ => { - return Err(syn::Error::new( - s.span(), - "expected \"camelCase\" or \"snake_case\"", - )); - } - }; - } + { + wrapper_attributes.argument_case = match s.value().as_str() { + "snake_case" => ArgumentCase::Snake, + "camelCase" => ArgumentCase::Camel, + _ => { + return Err(syn::Error::new( + s.span(), + "expected \"camelCase\" or \"snake_case\"", + )); + } + }; + } else if v.path.is_ident("rename") + && let Expr::Lit(ExprLit { + lit: Lit::Str(s), .. + }) = v.value + { + let lit = s.value(); + wrapper_attributes.rename = RenamePolicy::Rename(quote!(#lit)); } else if v.path.is_ident("root") && let Expr::Lit(ExprLit { lit: Lit::Str(s), @@ -93,7 +101,7 @@ impl Parse for WrapperAttributes { WrapperAttributeKind::Meta(Meta::Path(_)) => { return Err(syn::Error::new( input.span(), - "unexpected input, expected one of `rename_all`, `root`, `async`", + "unexpected input, expected one of `rename_all`, `rename`, `root`, `async`", )); } WrapperAttributeKind::Async => { @@ -119,6 +127,12 @@ enum ArgumentCase { Camel, } +/// The rename policy for the command. +enum RenamePolicy { + Keep, + Rename(TokenStream2), +} + /// The bindings we attach to `tauri::Invoke`. struct Invoke { message: Ident, @@ -137,9 +151,11 @@ pub fn wrapper(attributes: TokenStream, item: TokenStream) -> TokenStream { attrs.execution_context = ExecutionContext::Async; } - // macros used with `pub use my_macro;` need to be exported with `#[macro_export]` + // macros used with `pub use my_macro;` need to be exported with `#[macro_export]`. let maybe_macro_export = match &function.vis { - Visibility::Public(_) | Visibility::Restricted(_) => quote!(#[macro_export]), + Visibility::Public(_) | Visibility::Restricted(_) => { + quote!(#[macro_export]) + } _ => TokenStream2::default(), }; @@ -268,6 +284,17 @@ pub fn wrapper(attributes: TokenStream, item: TokenStream) -> TokenStream { TokenStream2::default() }; + // Always define a hidden macro that returns the externally invoked command name. + // This lets the handler match on the renamed string while the original function + // identifier remains usable in `generate_handler![original_fn_name]`. + let command_name_macro_ident = format_ident!("__tauri_command_name_{}", function.sig.ident); + let command_name_value = if let RenamePolicy::Rename(ref rename) = attrs.rename { + quote!(#rename) + } else { + let ident = &function.sig.ident; + quote!(stringify!(#ident)) + }; + // Rely on rust 2018 edition to allow importing a macro from a path. quote!( #async_command_check @@ -275,6 +302,17 @@ pub fn wrapper(attributes: TokenStream, item: TokenStream) -> TokenStream { #maybe_allow_unused #function + // Command name macro used by the handler for pattern matching. + // This macro returns the command name string literal (renamed or original). + #maybe_allow_unused + #maybe_macro_export + #[doc(hidden)] + macro_rules! #command_name_macro_ident { + () => { + #command_name_value + }; + } + #maybe_allow_unused #maybe_macro_export #[doc(hidden)] @@ -301,7 +339,7 @@ pub fn wrapper(attributes: TokenStream, item: TokenStream) -> TokenStream { // allow the macro to be resolved with the same path as the command function #[allow(unused_imports)] - #visibility use #wrapper; + #visibility use {#wrapper, #command_name_macro_ident}; ) .into() } @@ -465,11 +503,16 @@ fn parse_arg( } let root = &attributes.root; + let command_name = if let RenamePolicy::Rename(r) = &attributes.rename { + quote!(stringify!(#r)) + } else { + quote!(stringify!(#command)) + }; Ok(quote!(#root::ipc::CommandArg::from_command( #root::ipc::CommandItem { plugin: #plugin_name, - name: stringify!(#command), + name: #command_name, key: #key, message: &#message, acl: &#acl, diff --git a/crates/tauri-plugin/CHANGELOG.md b/crates/tauri-plugin/CHANGELOG.md index 93a1115aa31e..b721b377ca6c 100644 --- a/crates/tauri-plugin/CHANGELOG.md +++ b/crates/tauri-plugin/CHANGELOG.md @@ -1,5 +1,27 @@ # Changelog +## \[2.6.2] + +### Dependencies + +- Upgraded to `tauri-utils@2.9.2` + +## \[2.6.1] + +### Dependencies + +- Upgraded to `tauri-utils@2.9.1` + +## \[2.6.0] + +### New Features + +- [`cc5c97602`](https://www.github.com/tauri-apps/tauri/commit/cc5c976027b0ab2431c13ec5b2e201d4414a8a6e) ([#14486](https://www.github.com/tauri-apps/tauri/pull/14486)) Implement file association for Android and iOS. + +### Dependencies + +- Upgraded to `tauri-utils@2.9.0` + ## \[2.5.4] ### Dependencies diff --git a/crates/tauri-plugin/Cargo.toml b/crates/tauri-plugin/Cargo.toml index fe9595762eff..14755ca56ec4 100644 --- a/crates/tauri-plugin/Cargo.toml +++ b/crates/tauri-plugin/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-plugin" -version = "2.5.4" +version = "2.6.2" description = "Build script and runtime Tauri plugin definitions" authors.workspace = true homepage.workspace = true @@ -19,7 +19,6 @@ build = [ "dep:serde", "dep:serde_json", "dep:glob", - "dep:toml", "dep:plist", "dep:walkdir", ] @@ -28,12 +27,11 @@ runtime = [] [dependencies] anyhow = { version = "1", optional = true } serde = { version = "1", optional = true } -tauri-utils = { version = "2.8.3", default-features = false, features = [ - "build", +tauri-utils = { version = "2.9.2", default-features = false, features = [ + "build-2", ], path = "../tauri-utils" } serde_json = { version = "1", optional = true } glob = { version = "0.3", optional = true } -toml = { version = "0.9", optional = true } schemars = { version = "1", features = ["preserve_order"] } walkdir = { version = "2", optional = true } diff --git a/crates/tauri-plugin/src/build/mobile.rs b/crates/tauri-plugin/src/build/mobile.rs index 2ff795aca796..edccc50cc7de 100644 --- a/crates/tauri-plugin/src/build/mobile.rs +++ b/crates/tauri-plugin/src/build/mobile.rs @@ -5,8 +5,7 @@ //! Mobile-specific build utilities. use std::{ - env::var_os, - fs::{copy, create_dir, create_dir_all, read_to_string, remove_dir_all, write}, + fs::{copy, create_dir, create_dir_all, remove_dir_all}, path::{Path, PathBuf}, }; @@ -17,7 +16,7 @@ use super::{build_var, cfg_alias}; #[cfg(target_os = "macos")] pub fn update_entitlements(f: F) -> Result<()> { if let (Some(project_path), Ok(app_name)) = ( - var_os("TAURI_IOS_PROJECT_PATH").map(PathBuf::from), + std::env::var_os("TAURI_IOS_PROJECT_PATH").map(PathBuf::from), std::env::var("TAURI_IOS_APP_NAME"), ) { update_plist_file( @@ -34,7 +33,7 @@ pub fn update_entitlements(f: F) -> Result<() #[cfg(target_os = "macos")] pub fn update_info_plist(f: F) -> Result<()> { if let (Some(project_path), Ok(app_name)) = ( - var_os("TAURI_IOS_PROJECT_PATH").map(PathBuf::from), + std::env::var_os("TAURI_IOS_PROJECT_PATH").map(PathBuf::from), std::env::var("TAURI_IOS_APP_NAME"), ) { update_plist_file( @@ -48,16 +47,9 @@ pub fn update_info_plist(f: F) -> Result<()> Ok(()) } +/// Updates the Android manifest by inserting XML content into a specified parent tag. pub fn update_android_manifest(block_identifier: &str, parent: &str, insert: String) -> Result<()> { - if let Some(project_path) = var_os("TAURI_ANDROID_PROJECT_PATH").map(PathBuf::from) { - let manifest_path = project_path.join("app/src/main/AndroidManifest.xml"); - let manifest = read_to_string(&manifest_path)?; - let rewritten = insert_into_xml(&manifest, block_identifier, parent, &insert); - if rewritten != manifest { - write(manifest_path, rewritten)?; - } - } - Ok(()) + tauri_utils::build::update_android_manifest(block_identifier, parent, insert) } pub(crate) fn setup( @@ -161,7 +153,7 @@ fn update_plist_file, F: FnOnce(&mut plist::Dictionary)>( let path = path.as_ref(); if path.exists() { - let plist_str = read_to_string(path)?; + let plist_str = std::fs::read_to_string(path)?; let mut plist = plist::Value::from_reader(Cursor::new(&plist_str))?; if let Some(dict) = plist.as_dictionary_mut() { f(dict); @@ -170,7 +162,7 @@ fn update_plist_file, F: FnOnce(&mut plist::Dictionary)>( plist::to_writer_xml(writer, &plist)?; let new_plist_str = String::from_utf8(plist_buf)?; if new_plist_str != plist_str { - write(path, new_plist_str)?; + std::fs::write(path, new_plist_str)?; } } } @@ -178,72 +170,14 @@ fn update_plist_file, F: FnOnce(&mut plist::Dictionary)>( Ok(()) } -fn xml_block_comment(id: &str) -> String { - format!("") -} - -fn insert_into_xml(xml: &str, block_identifier: &str, parent_tag: &str, contents: &str) -> String { - let block_comment = xml_block_comment(block_identifier); - - let mut rewritten = Vec::new(); - let mut found_block = false; - let parent_closing_tag = format!(""); - for line in xml.split('\n') { - if line.contains(&block_comment) { - found_block = !found_block; - continue; - } - - // found previous block which should be removed - if found_block { - continue; - } - - if let Some(index) = line.find(&parent_closing_tag) { - let indentation = " ".repeat(index + 4); - rewritten.push(format!("{indentation}{block_comment}")); - for l in contents.split('\n') { - rewritten.push(format!("{indentation}{l}")); - } - rewritten.push(format!("{indentation}{block_comment}")); - } - - rewritten.push(line.to_string()); - } - - rewritten.join("\n") -} - #[cfg(test)] mod tests { #[test] - fn insert_into_xml() { - let manifest = r#" - - - - -"#; - let id = "tauritest"; - let new = super::insert_into_xml(manifest, id, "application", ""); - - let block_id_comment = super::xml_block_comment(id); - let expected = format!( - r#" - - - - {block_id_comment} - - {block_id_comment} - -"# - ); - - assert_eq!(new, expected); - - // assert it's still the same after an empty update - let new = super::insert_into_xml(&expected, id, "application", ""); - assert_eq!(new, expected); + fn update_android_manifest() { + use tauri_utils::build::update_android_manifest; + + // This test would require setting up the environment, so we just verify it compiles + // The actual implementation is tested in tauri-utils + let _result = update_android_manifest("test", "activity", "".to_string()); } } diff --git a/crates/tauri-plugin/src/lib.rs b/crates/tauri-plugin/src/lib.rs index 74c1fb941ed6..4c21a6af46ba 100644 --- a/crates/tauri-plugin/src/lib.rs +++ b/crates/tauri-plugin/src/lib.rs @@ -16,9 +16,9 @@ mod build; mod runtime; #[cfg(feature = "build")] -#[cfg_attr(docsrs, doc(feature = "build"))] +#[cfg_attr(docsrs, doc(cfg(feature = "build")))] pub use build::*; #[cfg(feature = "runtime")] -#[cfg_attr(docsrs, doc(feature = "runtime"))] +#[cfg_attr(docsrs, doc(cfg(feature = "runtime")))] #[allow(unused)] pub use runtime::*; diff --git a/crates/tauri-runtime-cef/Cargo.toml b/crates/tauri-runtime-cef/Cargo.toml index 97d2e102095a..5ab894ddcf2f 100644 --- a/crates/tauri-runtime-cef/Cargo.toml +++ b/crates/tauri-runtime-cef/Cargo.toml @@ -10,41 +10,78 @@ edition.workspace = true rust-version.workspace = true [dependencies] +base64 = "0.22" +cef = { version = "=148.0.0", features = ["build-util", "linux-x11"] } +dirs = "6" dioxus-debug-cell = "0.1" -tauri-runtime = { version = "2.9.2", path = "../tauri-runtime" } -tauri-utils = { version = "2.8.0", path = "../tauri-utils", features = [ - "html-manipulation", -] } +http = "1" +kuchiki = { package = "kuchikiki", version = "0.8.8-speedreader" } html5ever = "0.29" +log = "0.4.21" raw-window-handle = "0.6" -url = "2" -http = "1" -cef = { version = "=146.4.1", default-features = false } -# Not actually used directly, just locking it. -cef-dll-sys = "=146.4.1" serde = { version = "1", features = ["derive"] } serde_json = "1" -kuchiki = { package = "kuchikiki", version = "0.8.8-speedreader" } sha2 = "0.10" -base64 = "0.22" -dirs = "6" +tauri-runtime = { version = "2.11.2", path = "../tauri-runtime" } +tauri-utils = { version = "2.9.2", path = "../tauri-utils", features = [ + "html-manipulation", +] } +url = "2" +winit = "0.31.0-beta.2" [target."cfg(windows)".dependencies] windows = { version = "0.61", features = [ - "Win32_Graphics", + "Win32_Foundation", + "Win32_Graphics_Dwm", "Win32_Graphics_Gdi", - "Win32_UI_HiDpi", - "Win32_UI_Input", - "Win32_UI_Input_KeyboardAndMouse", + "Win32_System_Com", "Win32_System_LibraryLoader", + "Win32_UI_Input_KeyboardAndMouse", + "Win32_UI_Shell", + "Win32_UI_WindowsAndMessaging", ] } [target."cfg(target_os = \"macos\")".dependencies] objc2 = "0.6" -objc2-app-kit = { version = "0.3", features = [] } -objc2-foundation = { version = "0.3", features = ["NSNotification"] } +objc2-application-services = { version = "0.3", default-features = false, features = [ + "HIServices", + "Processes", +] } +objc2-app-kit = { version = "0.3", default-features = false, features = [ + "NSApplication", + "NSBezierPath", + "NSColor", + "NSControl", + "NSDockTile", + "NSEvent", + "NSImage", + "NSImageView", + "NSProgressIndicator", + "NSResponder", + "NSRunningApplication", + "NSScreen", + "NSView", + "NSWindow", + "block2", + "objc2-core-graphics", + "objc2-quartz-core", +] } +objc2-quartz-core = { version = "0.3", default-features = false, features = [ + "CALayer", + "objc2-core-graphics", +] } +objc2-foundation = { version = "0.3", default-features = false, features = [ + "NSArray", + "NSObject", + "NSRunLoop", + "NSString", + "NSThread", + "NSTimer", + "NSURL", +] } [target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies] +dlopen2 = { version = "0.8", features = ["derive"] } gtk = { version = "0.18", features = ["v3_24"] } x11-dl = "2.21" diff --git a/crates/tauri-runtime-cef/src/cef_impl.rs b/crates/tauri-runtime-cef/src/cef_impl.rs deleted file mode 100644 index d40da9f3606e..000000000000 --- a/crates/tauri-runtime-cef/src/cef_impl.rs +++ /dev/null @@ -1,4222 +0,0 @@ -// Copyright 2019-2024 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -use base64::Engine; -use cef::{rc::*, *}; -use cef_dll_sys::cef_runtime_style_t; -use dioxus_debug_cell::RefCell; -use sha2::{Digest, Sha256}; -use std::{ - collections::HashMap, - sync::{ - Arc, Mutex, - atomic::{AtomicBool, AtomicU32, Ordering}, - mpsc::channel, - }, -}; -use tauri_runtime::{ - ExitRequestedEventAction, RunEvent, UserEvent, - dpi::{ - LogicalPosition, LogicalSize, PhysicalPosition, PhysicalRect, PhysicalSize, Position, Rect, - Size, - }, - webview::{InitializationScript, PendingWebview, UriSchemeProtocolHandler, WebviewAttributes}, - window::{PendingWindow, WindowEvent, WindowId}, -}; -#[cfg(target_os = "macos")] -use tauri_utils::TitleBarStyle; -use tauri_utils::html::normalize_script_for_csp; - -use crate::{ - AppWebview, AppWindow, CefRuntime, CefWindowBuilder, DevToolsProtocolHandler, Message, - RuntimeStyle as CefRuntimeStyle, WebviewAtribute, WebviewMessage, WindowMessage, - cef_webview::CefWebview, -}; - -mod cookie; -mod drag_window; -pub mod request_handler; - -use cookie::{CollectAllCookiesVisitor, CollectUrlCookiesVisitor}; - -#[cfg(target_os = "linux")] -type CefOsEvent<'a> = Option<&'a mut sys::XEvent>; -#[cfg(target_os = "macos")] -type CefOsEvent<'a> = *mut u8; -#[cfg(windows)] -type CefOsEvent<'a> = Option<&'a mut sys::MSG>; -type AddressChangedHandler = dyn Fn(&url::Url) + Send + Sync; - -/// CEF transparent color value (ARGB) -const TRANSPARENT: u32 = 0x00000000; - -#[inline] -fn color_to_cef_argb(color: tauri_utils::config::Color) -> u32 { - let (r, g, b, a) = color.into(); - ((a as u32) << 24) | ((r as u32) << 16) | ((g as u32) << 8) | (b as u32) -} - -/// Convert position to the coordinate space expected by CEF. -/// On Windows, CEF uses physical coordinates; on other platforms, logical. -#[inline] -fn position_to_cef(position: Position, scale_factor: f64) -> cef::Point { - #[cfg(windows)] - let p = position.to_physical::(scale_factor); - #[cfg(not(windows))] - let p = position.to_logical::(scale_factor); - cef::Point { x: p.x, y: p.y } -} - -/// Convert size to the coordinate space expected by CEF. -/// On Windows, CEF uses physical coordinates; on other platforms, logical. -#[inline] -fn size_to_cef(size: Size, scale_factor: f64) -> cef::Size { - #[cfg(windows)] - let s = size.to_physical::(scale_factor); - #[cfg(not(windows))] - let s = size.to_logical::(scale_factor); - cef::Size { - width: s.width, - height: s.height, - } -} - -/// Convert rect to the coordinate space expected by CEF. -/// On Windows, CEF uses physical coordinates; on other platforms, logical. -#[inline] -fn rect_to_cef(rect: Rect, scale_factor: f64) -> cef::Rect { - let p = position_to_cef(rect.position, scale_factor); - let s = size_to_cef(rect.size, scale_factor); - cef::Rect { - x: p.x, - y: p.y, - width: s.width, - height: s.height, - } -} - -#[inline] -fn theme_to_color_variant(theme: Option) -> ColorVariant { - match theme { - Some(tauri_utils::Theme::Dark) => ColorVariant::DARK, - Some(tauri_utils::Theme::Light) => ColorVariant::LIGHT, - _ => ColorVariant::SYSTEM, - } -} - -#[inline] -fn color_variant_to_theme(variant: ColorVariant) -> Option { - if variant == ColorVariant::DARK { - Some(tauri_utils::Theme::Dark) - } else if variant == ColorVariant::LIGHT { - Some(tauri_utils::Theme::Light) - } else { - None - } -} - -fn set_window_theme_scheme(app_window: &AppWindow, theme: Option) { - let variant = theme_to_color_variant(theme); - for webview in &app_window.webviews { - if let Some(browser) = webview.inner.browser() - && let Some(host) = browser.host() - && let Some(request_context) = host.request_context() - { - request_context.set_chrome_color_scheme(variant, 0); - } - } -} - -fn apply_window_theme_scheme(app_window: &AppWindow, theme: Option) { - set_window_theme_scheme(app_window, theme); - if let Some(window) = app_window.window() { - // Ask CEF Views to refresh themed colors immediately. - window.theme_changed(); - } -} - -fn apply_request_context_theme_scheme( - request_context: Option<&RequestContext>, - theme: Option, -) { - if let Some(request_context) = request_context { - request_context.set_chrome_color_scheme(theme_to_color_variant(theme), 0); - } -} - -#[cfg(target_os = "macos")] -fn apply_macos_window_theme(window: Option<&cef::Window>, theme: Option) { - use objc2::rc::Retained; - use objc2_app_kit::{ - NSAppearance, NSAppearanceCustomization, NSAppearanceNameAqua, NSAppearanceNameDarkAqua, NSView, - }; - - let Some(window) = window else { - return; - }; - let ns_view = unsafe { Retained::::retain(window.window_handle() as _) }; - let Some(ns_view) = ns_view else { - return; - }; - let Some(ns_window) = ns_view.window() else { - return; - }; - let appearance = match theme { - Some(tauri_utils::Theme::Dark) => unsafe { - NSAppearance::appearanceNamed(NSAppearanceNameDarkAqua) - }, - Some(tauri_utils::Theme::Light) => unsafe { - NSAppearance::appearanceNamed(NSAppearanceNameAqua) - }, - _ => None, - }; - unsafe { ns_window.setAppearance(appearance.as_deref()) }; -} - -fn native_window_theme(app_window: &AppWindow) -> Option { - app_window.webviews.iter().find_map(|webview| { - webview - .inner - .browser() - .and_then(|browser| browser.host()) - .and_then(|host| host.request_context()) - .and_then(|request_context| { - color_variant_to_theme(request_context.chrome_color_scheme_mode()) - .or_else(|| color_variant_to_theme(request_context.chrome_color_scheme_variant())) - }) - }) -} - -/// Convert a CEF Display to a tauri Monitor -pub(crate) fn display_to_monitor(display: &cef::Display) -> tauri_runtime::monitor::Monitor { - let bounds = display.bounds(); - let work = display.work_area(); - let scale = display.device_scale_factor() as f64; - let physical_size = - LogicalSize::new(bounds.width as u32, bounds.height as u32).to_physical::(scale); - let physical_position = LogicalPosition::new(bounds.x, bounds.y).to_physical::(scale); - let work_physical_size = - LogicalSize::new(work.width as u32, work.height as u32).to_physical::(scale); - let work_physical_position = LogicalPosition::new(work.x, work.y).to_physical::(scale); - tauri_runtime::monitor::Monitor { - name: None, - size: PhysicalSize::new(physical_size.width, physical_size.height), - position: PhysicalPosition::new(physical_position.x, physical_position.y), - work_area: PhysicalRect { - position: PhysicalPosition::new(work_physical_position.x, work_physical_position.y), - size: PhysicalSize::new(work_physical_size.width, work_physical_size.height), - }, - scale_factor: display.device_scale_factor() as f64, - } -} - -/// Get the primary monitor -pub(crate) fn get_primary_monitor() -> Option { - cef::display_get_primary().map(|d| display_to_monitor(&d)) -} - -/// Get the monitor from a point -pub(crate) fn get_monitor_from_point(x: f64, y: f64) -> Option { - let rect = cef::Rect { - x: x as i32, - y: y as i32, - width: 1, - height: 1, - }; - cef::display_get_matching_bounds(Some(&rect), 1).map(|d| display_to_monitor(&d)) -} - -/// Get all available monitors -pub(crate) fn get_available_monitors() -> Vec { - let mut displays: Vec> = vec![None; cef::display_get_count()]; - cef::display_get_alls(Some(&mut displays)); - displays - .into_iter() - .flatten() - .map(|d| display_to_monitor(&d)) - .collect() -} - -/// Convert tauri Icon to CEF Image -fn icon_to_cef_image(icon: tauri_runtime::Icon<'static>) -> Option { - let rgba = icon.rgba.to_vec(); - let width = icon.width; - let height = icon.height; - - // Create a CEF Image - let image = cef::image_create()?; - - // Add bitmap data to the image - // RGBA_8888 color type, OPAQUE alpha type (for icons without transparency, or use PREMULTIPLIED for transparency) - use sys::cef_alpha_type_t; - let result = image.add_bitmap( - 1.0, // scale_factor - width as i32, - height as i32, - cef::ColorType::default(), // RGBA_8888 - cef::AlphaType::from(cef_alpha_type_t::CEF_ALPHA_TYPE_PREMULTIPLIED), // Use premultiplied for RGBA with alpha - Some(&rgba), - ); - - if result == 1 { Some(image) } else { None } -} - -/// Set window icon using CEF native API -fn set_window_icon(window: &cef::Window, icon: tauri_runtime::Icon<'static>) { - if let Some(mut cef_image) = icon_to_cef_image(icon) { - window.set_window_app_icon(Some(&mut cef_image)); - } -} - -/// Set overlay icon using CEF native API (set_window_app_icon) -fn set_overlay_icon(window: &cef::Window, icon: Option>) { - match icon { - Some(icon_data) => { - if let Some(mut cef_image) = icon_to_cef_image(icon_data) { - window.set_window_app_icon(Some(&mut cef_image)); - } - } - None => { - window.set_window_app_icon(None); - } - } -} - -#[inline] -fn apply_content_protection(window: &cef::Window, protected: bool) { - #[cfg(target_os = "linux")] - { - let _ = (window, protected); - } - #[cfg(windows)] - { - use windows::Win32::Foundation::HWND; - use windows::Win32::UI::WindowsAndMessaging::{ - SetWindowDisplayAffinity, WDA_EXCLUDEFROMCAPTURE, WDA_NONE, - }; - let hwnd = window.window_handle(); - unsafe { - let _ = SetWindowDisplayAffinity( - HWND(hwnd.0 as _), - if protected { - WDA_EXCLUDEFROMCAPTURE - } else { - WDA_NONE - }, - ); - } - } - - #[cfg(target_os = "macos")] - { - // Set NSWindow sharing type to NSWindowSharingNone/NSWindowSharingReadOnly - // Safety: must be called on main thread; CEF window APIs run on main thread. - unsafe { - use objc2::rc::Retained; - use objc2_app_kit::{NSView, NSWindowSharingType}; - let ns_view = Retained::::retain(window.window_handle() as _); - let ns_window = ns_view.as_ref().and_then(|v| v.window()); - let sharing = if protected { - NSWindowSharingType::None - } else { - NSWindowSharingType::ReadOnly - }; - if let Some(ns_window) = ns_window { - ns_window.setSharingType(sharing); - } - } - } -} - -#[derive(Clone)] -pub struct CefInitScript { - pub script: InitializationScript, - pub hash: String, -} - -impl CefInitScript { - pub fn new(script: InitializationScript) -> Self { - let hash = hash_script(script.script.as_str()); - Self { script, hash } - } -} - -fn hash_script(script: &str) -> String { - let normalized = normalize_script_for_csp(script.as_bytes()); - let mut hasher = Sha256::new(); - hasher.update(&normalized); - let hash = hasher.finalize(); - format!( - "'sha256-{}'", - base64::engine::general_purpose::STANDARD.encode(hash) - ) -} - -pub type SchemeHandlerRegistry = Arc< - Mutex< - HashMap< - (i32, String), - ( - String, - Arc>, - Arc>, - ), - >, - >, ->; - -pub type RunEventCallback = Arc)>>>; - -#[derive(Clone)] -pub struct Context { - pub windows: Arc>>, - pub callback: RunEventCallback, - pub next_window_id: Arc, - pub next_webview_id: Arc, - pub next_window_event_id: Arc, - pub next_webview_event_id: Arc, - pub scheme_handler_registry: SchemeHandlerRegistry, -} - -impl Context { - pub fn next_window_id(&self) -> WindowId { - self.next_window_id.fetch_add(1, Ordering::Relaxed).into() - } - - pub fn next_webview_id(&self) -> u32 { - self.next_webview_id.fetch_add(1, Ordering::Relaxed) - } - - pub fn next_window_event_id(&self) -> u32 { - self.next_window_event_id.fetch_add(1, Ordering::Relaxed) - } - - pub fn next_webview_event_id(&self) -> u32 { - self.next_webview_event_id.fetch_add(1, Ordering::Relaxed) - } -} - -wrap_app! { - pub struct TauriApp { - context: Context, - custom_schemes: Vec, - deep_link_schemes: Vec, - command_line_args: Vec<(String, Option)>, - } - - impl App { - fn browser_process_handler(&self) -> Option { - Some(AppBrowserProcessHandler::new( - self.context.clone(), - self.deep_link_schemes.clone(), - )) - } - - fn on_before_command_line_processing( - &self, - _process_type: Option<&CefString>, - command_line: Option<&mut CommandLine>, - ) { - if let Some(command_line) = command_line { - for (arg, value) in &self.command_line_args { - if let Some(value) = value { - command_line.append_switch_with_value( - Some(&CefString::from(arg.as_str())), - Some(&CefString::from(value.as_str())), - ); - } else if arg.starts_with("-") { - command_line.append_switch(Some(&CefString::from(arg.as_str()))); - } else { - command_line.append_argument(Some(&CefString::from(arg.as_str()))); - } - } - } - } - } -} - -wrap_browser_process_handler! { - struct AppBrowserProcessHandler { - context: Context, - deep_link_schemes: Vec, - } - - impl BrowserProcessHandler { - fn on_context_initialized(&self) { - (self.context.callback.borrow())(RunEvent::Ready); - } - - fn on_already_running_app_relaunch( - &self, - command_line: Option<&mut CommandLine>, - _current_directory: Option<&CefString>, - ) -> std::os::raw::c_int { - let Some(command_line) = command_line else { - return 0; - }; - let mut list = CefStringList::new(); - command_line.arguments(Some(&mut list)); - let args: Vec = list.into_iter().collect(); - if args.len() == 1 - && let Ok(url) = url::Url::parse(&args[0]) { - let scheme = url.scheme().to_string(); - if self.deep_link_schemes.iter().any(|s| s == &scheme) { - (self.context.callback.borrow())(RunEvent::Opened { - urls: vec![url], - }); - return 1; - } - } - // TODO: add event - 1 - } - } -} - -wrap_load_handler! { - struct BrowserLoadHandler { - initialization_scripts: Arc>, - on_page_load_handler: Option>, - custom_scheme_domain_names: Vec, - custom_protocol_scheme: String, - } - - impl LoadHandler { - fn on_load_start( - &self, - _browser: Option<&mut Browser>, - frame: Option<&mut Frame>, - _transition_type: TransitionType, - ) { - let Some(handler) = &self.on_page_load_handler else { return }; - let Some(frame) = frame else { return }; - - let is_main_frame = frame.is_main() == 1; - if !is_main_frame { - return; - } - - let url = frame.url(); - let url_str = cef::CefString::from(&url).to_string(); - if let Ok(url) = url::Url::parse(&url_str) { - handler(url, tauri_runtime::webview::PageLoadEvent::Started); - } - } - - fn on_load_end( - &self, - _browser: Option<&mut Browser>, - frame: Option<&mut Frame>, - http_status_code: ::std::os::raw::c_int, - ) { - let Some(frame) = frame else { return }; - - if let Some(handler) = &self.on_page_load_handler - && frame.is_main() == 1 { - let url = frame.url(); - let url_str = cef::CefString::from(&url).to_string(); - if let Ok(url) = url::Url::parse(&url_str) { - handler(url, tauri_runtime::webview::PageLoadEvent::Finished); - } - } - - // run init scripts for http/https pages that are not custom schemes - // custom schemes are handled by the request handler - // where we inject scripts directly in the html - - if !(200..300).contains(&http_status_code) { - return; - } - - let url = frame.url(); - let url_str = cef::CefString::from(&url).to_string(); - let url_obj = url::Url::parse(&url_str).ok(); - - let is_custom_scheme_url = url_obj - .as_ref() - .map(|u| { - let scheme = u.scheme(); - if scheme == self.custom_protocol_scheme { - let host_str = u.host_str().unwrap_or("").to_string(); - scheme == self.custom_protocol_scheme && self.custom_scheme_domain_names.contains(&host_str) - } else { - false - } - }); - // if we can't parse the URL, also return - if is_custom_scheme_url.unwrap_or(true) { return; } - - let is_main_frame = frame.is_main() == 1; - - let scripts_to_execute = if is_main_frame { - Box::new(self.initialization_scripts.iter().map(|s| &s.script.script)) as Box> - } else { - Box::new(self.initialization_scripts - .iter() - .filter(|s| !s.script.for_main_frame_only) - .map(|s| &s.script.script)) as Box> - }; - - for script in scripts_to_execute { - let script_url = format!("{}://__tauri_init_script__", url_obj.as_ref().map(|u| u.scheme()).unwrap_or("http")); - - frame.execute_java_script( - Some(&cef::CefString::from(script.as_str())), - Some(&cef::CefString::from(script_url.as_str())), - 0, - ); - } - } - } -} - -wrap_display_handler! { - struct BrowserDisplayHandler { - document_title_changed_handler: Option>, - address_changed_handler: Option>, - } - - impl DisplayHandler { - fn on_title_change( - &self, - _browser: Option<&mut Browser>, - title: Option<&CefString>, - ) { - let Some(handler) = &self.document_title_changed_handler else { return }; - let Some(title) = title else { return }; - let title_str = title.to_string(); - handler(title_str); - } - - fn on_address_change( - &self, - _browser: Option<&mut Browser>, - frame: Option<&mut Frame>, - url: Option<&CefString>, - ) { - // Only fire for main frame URL changes (matches on_before_browse behavior) - if let Some(frame) = frame - && frame.is_main() == 0 { - return; - } - let Some(handler) = &self.address_changed_handler else { return }; - let Some(url) = url else { return }; - let url_str = url.to_string(); - let Ok(parsed) = url::Url::parse(&url_str) else { return }; - handler(&parsed); - } - } -} - -wrap_context_menu_handler! { - struct BrowserContextMenuHandler { - devtools_enabled: bool, - } - - impl ContextMenuHandler { - fn on_before_context_menu( - &self, - _browser: Option<&mut Browser>, - _frame: Option<&mut Frame>, - _params: Option<&mut ContextMenuParams>, - model: Option<&mut MenuModel>, - ) { - if !self.devtools_enabled - && let Some(model) = model { - model.remove_at(model.count() - 1); - } - } - } -} - -cef::wrap_dev_tools_message_observer! { - struct TauriDevToolsProtocolObserver { - handlers: Arc>>>, - } - - impl DevToolsMessageObserver { - fn on_dev_tools_message( - &self, - _browser: Option<&mut cef::Browser>, - message: Option<&[u8]>, - ) -> std::os::raw::c_int { - if let Some(msg) = message { - let protocol = crate::DevToolsProtocol::Message(msg.to_vec()); - if let Ok(handlers) = self.handlers.lock() { - for handler in handlers.iter() { - handler(protocol.clone()); - } - } - } - 0 - } - - fn on_dev_tools_method_result( - &self, - _browser: Option<&mut Browser>, - message_id: std::os::raw::c_int, - success: std::os::raw::c_int, - result: Option<&[u8]>, - ) { - let protocol = crate::DevToolsProtocol::MethodResult { - message_id, - success: success != 0, - result: result.map(|r| r.to_vec()).unwrap_or_default(), - }; - if let Ok(handlers) = self.handlers.lock() { - for handler in handlers.iter() { - handler(protocol.clone()); - } - } - } - - fn on_dev_tools_event( - &self, - _browser: Option<&mut Browser>, - method: Option<&CefString>, - params: Option<&[u8]>, - ) { - let protocol = crate::DevToolsProtocol::Event { - method: method - .map(|m| format!("{m}")) - .unwrap_or_default(), - params: params.map(|p| p.to_vec()).unwrap_or_default(), - }; - if let Ok(handlers) = self.handlers.lock() { - for handler in handlers.iter() { - handler(protocol.clone()); - } - } - } - } -} - -/// Registers a DevTools protocol observer. Returns the [`cef::Registration`] which must be -/// kept alive for the observer to stay registered. The observer is unregistered when -/// the Registration is dropped. -fn add_dev_tools_observer( - browser: &cef::Browser, - handlers: Arc>>>, -) -> Option { - browser.host().and_then(|host| { - let mut observer = TauriDevToolsProtocolObserver::new(handlers); - host.add_dev_tools_message_observer(Some(&mut observer)) - }) -} - -wrap_keyboard_handler! { - struct BrowserKeyboardHandler { - devtools_enabled: bool, - } - - impl KeyboardHandler { - fn on_pre_key_event( - &self, - _browser: Option<&mut Browser>, - event: Option<&KeyEvent>, - _os_event: CefOsEvent, - _is_keyboard_shortcut: Option<&mut ::std::os::raw::c_int>, - ) -> ::std::os::raw::c_int { - // If devtools is disabled, block devtools keyboard shortcuts - if !self.devtools_enabled { - let Some(event) = event else { return 0; }; - - // Check if this is a keydown event - use cef::sys::cef_key_event_type_t; - let keydown_type: cef::KeyEventType = cef_key_event_type_t::KEYEVENT_RAWKEYDOWN.into(); - if event.type_ != keydown_type { - return 0; - } - - // Get modifier keys - use cef::sys::cef_event_flags_t; - #[cfg(windows)] - let modifiers = event.modifiers as i32; - #[cfg(not(windows))] - let modifiers = event.modifiers; - - #[cfg(not(target_os = "macos"))] - let ctrl = (modifiers & (cef_event_flags_t::EVENTFLAG_CONTROL_DOWN.0)) != 0; - #[cfg(not(target_os = "macos"))] - let shift = (modifiers & (cef_event_flags_t::EVENTFLAG_SHIFT_DOWN.0)) != 0; - - let key_code = event.windows_key_code; - - // Block F12 (key code 123) - if key_code == 123 { - if let Some(is_keyboard_shortcut) = _is_keyboard_shortcut { - *is_keyboard_shortcut = 1; - } - return 1; - } - - // Block Ctrl+Shift+I (key code 73 = 'I') on Linux/Windows - #[cfg(not(target_os = "macos"))] - if key_code == 73 && ctrl && shift { - if let Some(is_keyboard_shortcut) = _is_keyboard_shortcut { - *is_keyboard_shortcut = 1; - } - return 1; - } - - // Block Cmd+Opt+I on macOS - #[cfg(target_os = "macos")] - { - let meta = (modifiers & cef_event_flags_t::EVENTFLAG_COMMAND_DOWN.0) != 0; - let alt = (modifiers & cef_event_flags_t::EVENTFLAG_ALT_DOWN.0) != 0; - if key_code == 73 && meta && alt { - if let Some(is_keyboard_shortcut) = _is_keyboard_shortcut { - *is_keyboard_shortcut = 1; - } - return 1; - } - } - } - - 0 - } - } -} - -wrap_permission_handler! { - struct BrowserPermissionHandler {} - - impl PermissionHandler { - fn on_request_media_access_permission( - &self, - _browser: Option<&mut Browser>, - _frame: Option<&mut Frame>, - _requesting_origin: Option<&CefString>, - requested_permissions: u32, - callback: Option<&mut MediaAccessCallback>, - ) -> ::std::os::raw::c_int { - let Some(callback) = callback else { - return 0; - }; - // Allow microphone and camera when requested - let allowed = requested_permissions & (sys::cef_media_access_permission_types_t::CEF_MEDIA_PERMISSION_DEVICE_AUDIO_CAPTURE as u32 | sys::cef_media_access_permission_types_t::CEF_MEDIA_PERMISSION_DEVICE_VIDEO_CAPTURE as u32); - if allowed != 0 { - callback.cont(requested_permissions); - return 1; - } - 0 - } - - fn on_show_permission_prompt( - &self, - _browser: Option<&mut Browser>, - _prompt_id: u64, - _requesting_origin: Option<&CefString>, - requested_permissions: u32, - callback: Option<&mut PermissionPromptCallback>, - ) -> ::std::os::raw::c_int { - let Some(callback) = callback else { - return 0; - }; - // Allow permission prompt (e.g. microphone/camera) - callback.cont(PermissionRequestResult::from( - cef::sys::cef_permission_request_result_t::CEF_PERMISSION_RESULT_ACCEPT, - )); - 1 - } - } -} - -wrap_download_handler! { - struct BrowserDownloadHandler { - download_handler: Arc, - } - - impl DownloadHandler { - fn can_download( - &self, - _browser: Option<&mut Browser>, - _url: Option<&CefStringUtf16>, - _request_method: Option<&CefStringUtf16>, - ) -> ::std::os::raw::c_int { - // on_before_download is the one that actually validates the download - // so we return 1 to allow the download here - 1 - } - - fn on_before_download( - &self, - _browser: Option<&mut Browser>, - download_item: Option<&mut DownloadItem>, - suggested_name: Option<&CefStringUtf16>, - callback: Option<&mut BeforeDownloadCallback>, - ) -> ::std::os::raw::c_int { - let Some(download_item) = download_item else { return 0; }; - let Some(callback) = callback else { return 0; }; - - let url_str = CefString::from(&download_item.url()).to_string(); - let Ok(url) = url::Url::parse(&url_str) else { return 0; }; - - let suggested_path = suggested_name - .map(|s| s.to_string()) - .map(std::path::PathBuf::from) - .unwrap_or_default(); - - let mut destination = suggested_path.clone(); - - // Call handler with Requested event - let should_allow = (self.download_handler)(tauri_runtime::webview::DownloadEvent::Requested { - url: url.clone(), - destination: &mut destination, - }); - - if should_allow { - // Set the download path - let destination_cef = CefStringUtf16::from(destination.to_string_lossy().as_ref()); - - // if the user callback did not modify the destination, show the dialog - let show_dialog = destination == suggested_path; - callback.cont(Some(&destination_cef), show_dialog as ::std::os::raw::c_int); - } - 1 - } - - fn on_download_updated( - &self, - _browser: Option<&mut Browser>, - download_item: Option<&mut DownloadItem>, - _callback: Option<&mut DownloadItemCallback>, - ) { - let Some(download_item) = download_item else { return; }; - - // Get download URL - let url_str = CefString::from(&download_item.url()).to_string(); - let Ok(url) = url::Url::parse(&url_str) else { return; }; - - // Check download state - CEF returns i32 where 0 is false, non-zero is true - let is_complete = download_item.is_complete() != 0; - let is_canceled = download_item.is_canceled() != 0; - let success = is_complete && !is_canceled; - - // Get full path if available - full_path() returns CefStringUserfreeUtf16 - let full_path = if is_complete || is_canceled { - let path_cef = download_item.full_path(); - let path_str = CefString::from(&path_cef).to_string(); - if !path_str.is_empty() { - Some(std::path::PathBuf::from(path_str)) - } else { - None - } - } else { - None - }; - - // Only call handler when download is finished (complete or canceled) - if is_complete || is_canceled { - // Call handler with Finished event - (self.download_handler)(tauri_runtime::webview::DownloadEvent::Finished { - url, - path: full_path, - success, - }); - } - } - } -} - -#[derive(Clone, Copy, PartialEq, Eq)] -enum WindowKind { - /// Full browser window created with browser_host_create_browser_sync - Browser, - /// Tauri window created with window_create_top_level - Tauri, -} - -wrap_life_span_handler! { - struct BrowserLifeSpanHandler { - window_kind: WindowKind, - window_id: WindowId, - context: Context, - new_window_handler: Option>>>, - initial_url: Option, - } - - impl LifeSpanHandler { - fn on_after_created(&self, browser: Option<&mut Browser>) { - if let Some(browser) = browser - && let Some(initial_url) = &self.initial_url { - check_and_reload_if_blank(browser.clone(), initial_url.clone()); - } - } - - fn on_before_close(&self, browser: Option<&mut Browser>) { - match self.window_kind { - WindowKind::Browser => { - on_window_destroyed(self.window_id, &self.context); - } - WindowKind::Tauri => { - let Some(browser) = browser else { - return; - }; - let browser_id = browser.identifier(); - - let (webview, is_last_in_window) = { - let mut windows = self.context.windows.borrow_mut(); - let Some(app_window) = windows.get_mut(&self.window_id) else { - return; - }; - let webview_index = app_window - .webviews - .iter() - .position(|w| *w.browser_id.borrow() == browser_id); - let Some(index) = webview_index else { - return; - }; - let webview = app_window.webviews.remove(index); - let webview_id = webview.webview_id; - app_window - .webview_event_listeners - .lock() - .unwrap() - .remove(&webview_id); - let is_last = app_window.webviews.is_empty(); - (webview, is_last) - }; - - { - let mut registry = self.context.scheme_handler_registry.lock().unwrap(); - let schemes: Vec<_> = webview - .uri_scheme_protocols - .keys() - .cloned() - .collect(); - for scheme in schemes { - registry.remove(&(browser_id, scheme)); - } - } - - // safe to drop - CEF callbacks can borrow windows - drop(webview); - - if is_last_in_window { - on_window_destroyed(self.window_id, &self.context); - } - } - } - } - - fn on_before_popup( - &self, - _browser: Option<&mut Browser>, - _frame: Option<&mut Frame>, - _popup_id: std::os::raw::c_int, - target_url: Option<&CefString>, - _target_frame_name: Option<&CefString>, - _target_disposition: WindowOpenDisposition, - _user_gesture: std::os::raw::c_int, - popup_features: Option<&PopupFeatures>, - _window_info: Option<&mut WindowInfo>, - _client: Option<&mut Option>, - _settings: Option<&mut BrowserSettings>, - _extra_info: Option<&mut Option>, - _no_javascript_access: Option<&mut i32>, - ) -> std::os::raw::c_int { - let Some(handler) = &self.new_window_handler else { - // No handler, allow default behavior - return 0; - }; - - let Some(target_url) = target_url else { - // No URL, deny - return 1; - }; - - let url_str = target_url.to_string(); - let Ok(url) = url::Url::parse(&url_str) else { - // Invalid URL, deny - return 1; - }; - - // Extract size and position from popup_features - // Note: PopupFeatures fields may vary by CEF version, so we handle them defensively - let size = popup_features.and({ - // Try to access width/height fields - structure may vary - // For now, we'll use None if we can't determine the size - None // TODO: Implement proper PopupFeatures field access when CEF API is available - }); - - let position = popup_features.and({ - // Try to access x/y fields - structure may vary - // For now, we'll use None if we can't determine the position - None // TODO: Implement proper PopupFeatures field access when CEF API is available - }); - - let features = tauri_runtime::webview::NewWindowFeatures::new( - size, - position, - crate::NewWindowOpener {}, - ); - - let response = handler(url, features); - - match response { - tauri_runtime::webview::NewWindowResponse::Allow => { - // Allow CEF to handle the popup with default behavior - 0 - } - tauri_runtime::webview::NewWindowResponse::Create { window_id: _window_id } => { - // We need to create a window and associate it with the popup - // For now, we'll deny the popup and let the handler create the window - // The window creation should happen via the message system - // This is a limitation - CEF doesn't easily support creating a window - // and associating it with a popup in the callback - // We return 1 to cancel the popup, and the handler should create the window - 1 - } - tauri_runtime::webview::NewWindowResponse::Deny => { - // Deny the popup - 1 - } - } - } - } -} - -wrap_client! { - struct BrowserClient { - window_kind: WindowKind, - window_id: WindowId, - initialization_scripts: Arc>, - on_page_load_handler: Option>, - document_title_changed_handler: Option>, - navigation_handler: Option>, - address_changed_handler: Option>, - new_window_handler: Option>>>, - download_handler: Option>, - devtools_enabled: bool, - custom_scheme_domain_names: Vec, - custom_protocol_scheme: String, - context: Context, - initial_url: Option, - } - - impl Client { - fn request_handler(&self) -> Option { - Some(request_handler::WebRequestHandler::new( - self.initialization_scripts.clone(), - self.navigation_handler.clone(), - )) - } - - fn life_span_handler(&self) -> Option { - Some(BrowserLifeSpanHandler::new( - self.window_kind, - self.window_id, - self.context.clone(), - self.new_window_handler.clone(), - self.initial_url.clone(), - )) - } - - fn load_handler(&self) -> Option { - Some(BrowserLoadHandler::new( - self.initialization_scripts.clone(), - self.on_page_load_handler.clone(), - self.custom_scheme_domain_names.clone(), - self.custom_protocol_scheme.clone(), - )) - } - - fn display_handler(&self) -> Option { - Some(BrowserDisplayHandler::new( - self.document_title_changed_handler.clone(), - self.address_changed_handler.clone(), - )) - } - - fn download_handler(&self) -> Option { - self.download_handler.clone().map(|handler| BrowserDownloadHandler::new(handler)) - } - - fn context_menu_handler(&self) -> Option { - Some(BrowserContextMenuHandler::new(self.devtools_enabled)) - } - - fn keyboard_handler(&self) -> Option { - Some(BrowserKeyboardHandler::new(self.devtools_enabled)) - } - - fn permission_handler(&self) -> Option { - Some(BrowserPermissionHandler::new()) - } - } -} - -wrap_browser_view_delegate! { - struct BrowserViewDelegateImpl { - browser_id: Arc>, - browser_runtime_style: CefRuntimeStyle, - scheme_handler_registry: SchemeHandlerRegistry, - webview_label: String, - uri_scheme_protocols: Arc>>>, - initialization_scripts: Arc>, - devtools_protocol_handlers: Arc>>>, - devtools_observer_registration: Arc>>, - webview_attributes: Arc>, - } - - impl ViewDelegate { - fn on_theme_changed(&self, view: Option<&mut View>) { - #[cfg(any(not(target_os = "macos"), feature = "macos-private-api"))] - { - let Some(view) = view else { return; }; - - let webview_attributes = self.webview_attributes.borrow(); - - if webview_attributes.transparent { - view.set_background_color(TRANSPARENT); - } else if let Some(color) = webview_attributes.background_color { - let color = color_to_cef_argb(color); - view.set_background_color(color); - } - } - } - } - - impl BrowserViewDelegate { - fn on_browser_created(&self, _browser_view: Option<&mut BrowserView>, browser: Option<&mut Browser>) { - if let Some(browser) = browser { - let real_id = browser.identifier(); - let _ = std::mem::replace(&mut *self.browser_id.borrow_mut(), real_id); - - // Only add the observer when at least one listener is registered - if !self.devtools_protocol_handlers.lock().unwrap().is_empty() - && let Some(registration) = add_dev_tools_observer(browser, self.devtools_protocol_handlers.clone()) { - self.devtools_observer_registration.lock().unwrap().replace(registration); - } - - let mut registry = self.scheme_handler_registry.lock().unwrap(); - for (scheme, handler) in self.uri_scheme_protocols.iter() { - registry.insert( - (real_id, scheme.clone()), - ( - self.webview_label.clone(), - handler.clone(), - self.initialization_scripts.clone(), - ), - ); - } - } - } - - fn browser_runtime_style(&self) -> RuntimeStyle { - use cef::sys::cef_runtime_style_t; - - match self.browser_runtime_style { - CefRuntimeStyle::Alloy => RuntimeStyle::from(cef_runtime_style_t::CEF_RUNTIME_STYLE_ALLOY), - CefRuntimeStyle::Chrome => RuntimeStyle::from(cef_runtime_style_t::CEF_RUNTIME_STYLE_CHROME), - } - } - } -} - -wrap_window_delegate! { - struct AppWindowDelegate { - window_id: WindowId, - callback: RunEventCallback, - force_close: Arc, - windows: Arc>>, - attributes: Arc>, - last_emitted_position: RefCell>, - last_emitted_size: RefCell>, - suppress_next_theme_changed: RefCell, - context: Context - } - - impl ViewDelegate { - fn minimum_size(&self, view: Option<&mut View>) -> cef::Size { - let window = view.and_then(|v| v.window()); - let scale = window - .and_then(|w| w.display()) - .map(|d| d.device_scale_factor() as f64) - .unwrap_or(1.0); - let mut min_w: i32 = 0; - let mut min_h: i32 = 0; - let Ok(attributes) = self.attributes.try_borrow() else { - return cef::Size { width: 0, height: 0 }; - }; - if let Some(min_size) = attributes.min_inner_size { - let logical = min_size.to_logical::(scale); - min_w = min_w.max(logical.width as i32); - min_h = min_h.max(logical.height as i32); - } - if let Some(constraints) = attributes.inner_size_constraints.as_ref() { - if let Some(w) = constraints.min_width { - let w_lg = i32::from(w.to_logical::(scale)); - min_w = min_w.max(w_lg); - } - if let Some(h) = constraints.min_height { - let h_lg = i32::from(h.to_logical::(scale)); - min_h = min_h.max(h_lg); - } - } - - if min_w != 0 || min_h != 0 { - cef::Size { width: min_w, height: min_h } - } else { - cef::Size { width: 0, height: 0 } - } - } - - fn maximum_size(&self, view: Option<&mut View>) -> cef::Size { - let window = view.and_then(|v| v.window()); - let scale = window - .and_then(|w| w.display()) - .map(|d| d.device_scale_factor() as f64) - .unwrap_or(1.0); - let mut max_w: Option = None; - let mut max_h: Option = None; - let Ok(attributes) = self.attributes.try_borrow() else { - return cef::Size { width: 0, height: 0 }; - }; - - if let Some(max_size) = attributes.max_inner_size { - let logical = max_size.to_logical::(scale); - max_w = Some(logical.width as i32); - max_h = Some(logical.height as i32); - } - if let Some(constraints) = attributes.inner_size_constraints.as_ref() { - if let Some(w) = constraints.max_width { - let w_lg = i32::from(w.to_logical::(scale)); - max_w = Some(match max_w { Some(v) => v.min(w_lg), None => w_lg }); - } - if let Some(h) = constraints.max_height { - let h_lg = i32::from(h.to_logical::(scale)); - max_h = Some(match max_h { Some(v) => v.min(h_lg), None => h_lg }); - } - } - - if max_w.is_some() || max_h.is_some() { - cef::Size { - width: max_w.unwrap_or(0), - height: max_h.unwrap_or(0), - } - } else { - cef::Size { width: 0, height: 0 } - } - } - - fn on_theme_changed(&self, view: Option<&mut View>) { - let Some(view) = view else { return; }; - - let attrs = self.attributes.borrow(); - - #[cfg(any(not(target_os = "macos"), feature = "macos-private-api"))] - if attrs.transparent.unwrap_or_default() { - view.set_background_color(TRANSPARENT); - } else if let Some(color) = attrs.background_color { - let color = color_to_cef_argb(color); - view.set_background_color(color); - } - - // macOS resets traffic light button positions during the layout pass - // that follows an appearance change, so we must defer the reapply - // to run after that layout completes. - #[cfg(target_os = "macos")] - if let Some(position) = attrs.traffic_light_position { - send_message_task( - &self.context, - Message::Window { - window_id: self.window_id, - message: WindowMessage::SetTrafficLightPosition(position), - }, - ); - } - - if std::mem::take(&mut *self.suppress_next_theme_changed.borrow_mut()) { - return; - } - - let (system_theme, explicit_theme) = { - let windows = self.windows.borrow(); - let Some(app_window) = windows.get(&self.window_id) else { - return; - }; - - let Some(system_theme) = native_window_theme(app_window) else { - return; - }; - - let explicit_theme = app_window.attributes.borrow().theme; - (system_theme, explicit_theme) - }; - - if let Some(explicit_theme) = explicit_theme - && let Some(app_window) = self.windows.borrow().get(&self.window_id) - { - #[cfg(target_os = "macos")] - { - *self.suppress_next_theme_changed.borrow_mut() = true; - send_message_task( - &self.context, - Message::Window { - window_id: self.window_id, - message: WindowMessage::SetTheme(Some(explicit_theme)), - }, - ); - } - set_window_theme_scheme(app_window, Some(explicit_theme)); - } - - send_window_event( - self.window_id, - &self.windows, - &self.callback, - WindowEvent::ThemeChanged(system_theme), - ); - } - } - - impl PanelDelegate {} - - impl WindowDelegate { - fn on_window_created(&self, window: Option<&mut Window>) { - if let Some(window) = window { - // Setup necessary handling for `start_window_dragging` to work on Windows - #[cfg(windows)] - drag_window::windows::subclass_window_for_dragging(window); - - let a = self.attributes.borrow(); - #[cfg(target_os = "macos")] - apply_macos_window_theme(Some(window), a.theme); - if let Some(icon) = a.icon.clone() { - set_window_icon(window, icon); - } - - #[cfg(target_os = "macos")] - { - let decorations = a.decorations.unwrap_or(true); - - // default to transparent title bar if decorations are disabled, otherwise use visible title bar - let default_style = if decorations { - TitleBarStyle::Visible - } else { - TitleBarStyle::Transparent - }; - let style = a.title_bar_style.unwrap_or(default_style); - - // default to hidden title if decorations are disabled, otherwise show title - let hidden_title = a.hidden_title.unwrap_or(!decorations); - - apply_titlebar_style(window, style, hidden_title); - } - - if let Some(title) = &a.title { - window.set_title(Some(&CefString::from(title.as_str()))); - } - - if let Some(inner_size) = a.inner_size - - && let Some(display) = window.display() { - let scale = display.device_scale_factor() as f64; - let size = size_to_cef(inner_size, scale); - - // On Windows, the size set via CEF APIs is the outer size (including borders), - // so we need to adjust it to set the correct inner size. - #[cfg(windows)] - let size = crate::utils::windows::adjust_size(window.window_handle(), size); - - window.set_size(Some(&size)); - } - - if let Some(position) = &a.position - && let Some(display) = window.display() { - let device_scale_factor = display.device_scale_factor() as f64; - let position = position_to_cef(*position, device_scale_factor); - window.set_position(Some(&position)); - } - - if a.center { - // Use CEF's native centering API - window.center_window(Some(&window.size())); - } - - if let Some(focused) = a.focused - && focused { - window.request_focus(); - } - - if let Some(maximized) = a.maximized - && maximized { - window.maximize(); - } - - if let Some(fullscreen) = a.fullscreen - && fullscreen { - window.set_fullscreen(1); - } - - if let Some(always_on_top) = a.always_on_top - && always_on_top { - window.set_always_on_top(1); - } - - if let Some(_always_on_bottom) = a.always_on_bottom { - // TODO: Implement always on bottom for CEF - } - - if let Some(visible_on_all_workspaces) = a.visible_on_all_workspaces - && visible_on_all_workspaces { - // TODO: Implement visible on all workspaces for CEF - } - - if let Some(content_protected) = a.content_protected { - apply_content_protection(window, content_protected); - } - - if let Some(skip_taskbar) = a.skip_taskbar - && skip_taskbar { - // TODO: Implement skip taskbar for CEF - } - - if let Some(shadow) = a.shadow - && !shadow { - // TODO: Implement shadow control for CEF - } - - if let Some(focusable) = a.focusable { - window.set_focusable(if focusable { 1 } else { 0 }); - } - - if a.visible.unwrap_or(true) { - window.show(); - } - - // Set traffic light position on macOS after window is fully created - // by posting a task to the UI thread to avoid issues with early setting - #[cfg(target_os = "macos")] - if let Some(pos) = a.traffic_light_position { - let window_message = WindowMessage::SetTrafficLightPosition(pos); - let message = Message::Window { - window_id: self.window_id, - message: window_message, - }; - - send_message_task(&self.context, message); - } - } - } - - fn is_frameless(&self, _window: Option<&mut Window>) -> ::std::os::raw::c_int { - // Map `decorations: false` to frameless window - let decorated = self - .attributes - .borrow() - .decorations - .unwrap_or(true); - (!decorated) as i32 - } - - fn with_standard_window_buttons(&self, _window: Option<&mut Window>) -> ::std::os::raw::c_int { - 1 - } - - fn on_window_destroyed(&self, _window: Option<&mut Window>) { - on_window_destroyed(self.window_id, &self.context); - } - - fn can_resize(&self, _window: Option<&mut Window>) -> ::std::os::raw::c_int { - self - .attributes - .borrow() - .resizable - .unwrap_or(true) as i32 - } - - fn can_maximize(&self, _window: Option<&mut Window>) -> ::std::os::raw::c_int { - // Can maximize if maximizable is true and resizable is true (or not set, defaulting to true) - let a = self.attributes.borrow(); - let resizable = a.resizable.unwrap_or(true); - let maximizable = a.maximizable.unwrap_or(true); - (resizable && maximizable) as i32 - } - - fn can_minimize(&self, _window: Option<&mut Window>) -> ::std::os::raw::c_int { - self - .attributes - .borrow() - .minimizable - .unwrap_or(true) as i32 - } - - fn can_close(&self, _window: Option<&mut Window>) -> ::std::os::raw::c_int { - if self.force_close.load(Ordering::SeqCst) { - close_window_browsers(self.window_id, &self.windows); - return 1; - } - let closable = self - .attributes - .borrow() - .closable - .unwrap_or(true); - - if !closable { - return 0; - } - - let (tx, rx) = channel(); - let event = WindowEvent::CloseRequested { signal_tx: tx }; - - send_window_event(self.window_id, &self.windows, &self.callback, event.clone()); - - let should_prevent = matches!(rx.try_recv(), Ok(true)); - - if should_prevent { - 0 - } else { - close_window_browsers(self.window_id, &self.windows) as i32 - } - } - - fn on_window_bounds_changed( - &self, - window: Option<&mut Window>, - bounds: Option<&cef::Rect>, - ) { - let (Some(window), Some(bounds)) = (window, bounds) else { return; }; - - #[cfg(target_os = "macos")] - if let Some(pos) = &self.attributes.borrow().traffic_light_position { - apply_traffic_light_position(window.window_handle(), pos); - } - - #[cfg(not(windows))] - let size = LogicalSize::new(bounds.width as u32, bounds.height as u32); - - // On Windows, we need to get the inner size because the bounds include the window borders. - #[cfg(windows)] - let size = crate::utils::windows::inner_size(window.window_handle()); - - // Update autoresize overlay bounds - let bounds_updates: Vec<(CefWebview, cef::Rect)> = - if let Ok(windows_ref) = self.windows.try_borrow() { - if let Some(app_window) = windows_ref.get(&self.window_id) { - app_window - .webviews - .iter() - .filter_map(|wrapper| { - if wrapper.inner.is_browser() { - wrapper.bounds.lock().unwrap().as_ref().map(|b| { - let new_rect = cef::Rect { - x: (size.width as f32 * b.x_rate) as i32, - y: (size.height as f32 * b.y_rate) as i32, - width: (size.width as f32 * b.width_rate) as i32, - height: (size.height as f32 * b.height_rate) as i32, - }; - (wrapper.inner.clone(), new_rect) - }) - } else { - None - } - }) - .collect() - } else { - Vec::new() - } - } else { - Vec::new() - }; - - for (inner, rect) in bounds_updates { - inner.set_bounds(Some(&rect)); - } - - let scale = window - .display() - .map(|d| d.device_scale_factor() as f64) - .unwrap_or(1.0); - - let physical_position = LogicalPosition::new(bounds.x, bounds.y) - .to_physical::(scale); - let position_changed = { - let mut emitted_pos = self.last_emitted_position.borrow_mut(); - let changed = *emitted_pos != physical_position; - if changed { - *emitted_pos = physical_position; - } - changed - }; - if position_changed { - send_window_event( - self.window_id, - &self.windows, - &self.callback, - WindowEvent::Moved(physical_position), - ); - } - - let physical_size = LogicalSize::new( - bounds.width as u32, - bounds.height as u32, - ).to_physical::(scale); - let size_changed = { - let mut emitted_size = self.last_emitted_size.borrow_mut(); - let changed = *emitted_size != physical_size; - if changed { - *emitted_size = physical_size; - } - changed - }; - if size_changed { - send_window_event( - self.window_id, - &self.windows, - &self.callback, - WindowEvent::Resized(physical_size), - ); - } - } - - fn on_window_activation_changed( - &self, - _window: Option<&mut Window>, - active: ::std::os::raw::c_int, - ) { - send_window_event( - self.window_id, - &self.windows, - &self.callback, - WindowEvent::Focused(active == 1), - ); - } - } -} - -fn get_webview( - context: &Context, - window_id: WindowId, - webview_id: u32, -) -> Option { - context - .windows - .borrow() - .get(&window_id) - .and_then(|app_window| { - app_window - .webviews - .iter() - .find(|w| w.webview_id == webview_id) - .cloned() - }) -} - -fn get_main_frame( - context: &Context, - window_id: WindowId, - webview_id: u32, -) -> Option { - get_webview(context, window_id, webview_id) - .and_then(|bv| bv.inner.browser()) - .and_then(|b| b.main_frame()) -} - -fn get_browser( - context: &Context, - window_id: WindowId, - webview_id: u32, -) -> Option { - get_webview(context, window_id, webview_id).and_then(|bv| bv.inner.browser()) -} - -fn handle_webview_message( - context: &Context, - window_id: WindowId, - webview_id: u32, - message: WebviewMessage, -) { - match message { - WebviewMessage::AddEventListener(event_id, handler) => { - if let Some(app_window) = context.windows.borrow().get(&window_id) { - let listeners = app_window.webview_event_listeners.clone(); - let mut listeners_map = listeners.lock().unwrap(); - let webview_listeners = listeners_map - .entry(webview_id) - .or_insert_with(|| Arc::new(Mutex::new(HashMap::new()))); - webview_listeners.lock().unwrap().insert(event_id, handler); - } - } - WebviewMessage::EvaluateScript(script) => { - if let Some(frame) = get_main_frame(context, window_id, webview_id) { - frame.execute_java_script( - Some(&cef::CefString::from(script.as_str())), - Some(&cef::CefString::from("")), - 0, - ); - } - } - WebviewMessage::Navigate(url) => { - if let Some(frame) = get_main_frame(context, window_id, webview_id) { - frame.load_url(Some(&cef::CefString::from(url.as_str()))) - } - } - WebviewMessage::Reload => { - if let Some(browser) = get_browser(context, window_id, webview_id) { - browser.reload() - } - } - WebviewMessage::GoBack => { - if let Some(browser) = get_browser(context, window_id, webview_id) { - browser.go_back() - } - } - WebviewMessage::CanGoBack(tx) => { - if let Some(browser) = get_browser(context, window_id, webview_id) { - let _ = tx.send(Ok(browser.can_go_back() != 0)); - } else { - let _ = tx.send(Err(tauri_runtime::Error::FailedToSendMessage)); - } - } - WebviewMessage::GoForward => { - if let Some(browser) = get_browser(context, window_id, webview_id) { - browser.go_forward() - } - } - WebviewMessage::CanGoForward(tx) => { - if let Some(browser) = get_browser(context, window_id, webview_id) { - let _ = tx.send(Ok(browser.can_go_forward() != 0)); - } else { - let _ = tx.send(Err(tauri_runtime::Error::FailedToSendMessage)); - } - } - WebviewMessage::Print => { - if let Some(host) = get_browser(context, window_id, webview_id).and_then(|b| b.host()) { - host.print() - } - } - WebviewMessage::Close => { - let webview_to_close = { - let mut windows = context.windows.borrow_mut(); - if let Some(app_window) = windows.get_mut(&window_id) { - let webview_index = app_window - .webviews - .iter() - .position(|w| w.webview_id == webview_id); - - if let Some(index) = webview_index { - let wrapper = app_window.webviews.remove(index); - app_window - .webview_event_listeners - .lock() - .unwrap() - .remove(&webview_id); - Some(wrapper) - } else { - None - } - } else { - None - } - }; - - if let Some(wrapper) = webview_to_close { - let browser_id = *wrapper.browser_id.borrow(); - { - let mut registry = context.scheme_handler_registry.lock().unwrap(); - for scheme in wrapper.uri_scheme_protocols.keys() { - registry.remove(&(browser_id, scheme.clone())); - } - } - wrapper.inner.close(); - } - } - WebviewMessage::Show => { - if let Some(wrapper) = get_webview(context, window_id, webview_id) { - wrapper.inner.set_visible(1) - } - } - WebviewMessage::Hide => { - if let Some(wrapper) = get_webview(context, window_id, webview_id) { - wrapper.inner.set_visible(0) - } - } - WebviewMessage::SetPosition(position) => { - let data = context - .windows - .borrow() - .get(&window_id) - .and_then(|app_window| { - let device_scale_factor = app_window - .window() - .and_then(|window| window.display()) - .map(|d| d.device_scale_factor() as f64) - .unwrap_or(1.0); - - let position = position_to_cef(position, device_scale_factor); - - app_window - .webviews - .iter() - .find(|w| w.webview_id == webview_id) - .map(|wrapper| { - let current_bounds = wrapper.inner.bounds(); - let new_bounds = cef::Rect { - x: position.x, - y: position.y, - width: current_bounds.width, - height: current_bounds.height, - }; - let inner = wrapper.inner.clone(); - let bounds_arc = wrapper.bounds.clone(); - let is_browser = wrapper.inner.is_browser(); - let window_bounds = if is_browser { - app_window.window().map(|w| w.bounds()) - } else { - None - }; - (inner, new_bounds, is_browser, bounds_arc, window_bounds) - }) - }); - - if let Some((inner, new_bounds, is_browser, bounds_arc, window_bounds)) = data { - inner.set_bounds(Some(&new_bounds)); - if is_browser - && let Some(b) = &mut *bounds_arc.lock().unwrap() - && let Some(wb) = window_bounds - { - let window_size = LogicalSize::new(wb.width as u32, wb.height as u32); - b.x_rate = new_bounds.x as f32 / window_size.width as f32; - b.y_rate = new_bounds.y as f32 / window_size.height as f32; - } - } - } - WebviewMessage::SetSize(size) => { - let data = context - .windows - .borrow() - .get(&window_id) - .and_then(|app_window| { - let device_scale_factor = app_window - .window() - .and_then(|window| window.display()) - .map(|d| d.device_scale_factor() as f64) - .unwrap_or(1.0); - - let size = size_to_cef(size, device_scale_factor); - - app_window - .webviews - .iter() - .find(|w| w.webview_id == webview_id) - .map(|wrapper| { - let current_bounds = wrapper.inner.bounds(); - let new_bounds = cef::Rect { - x: current_bounds.x, - y: current_bounds.y, - width: size.width, - height: size.height, - }; - let inner = wrapper.inner.clone(); - let bounds_arc = wrapper.bounds.clone(); - let is_browser = wrapper.inner.is_browser(); - let window_bounds = if is_browser { - app_window.window().map(|w| w.bounds()) - } else { - None - }; - (inner, new_bounds, is_browser, bounds_arc, window_bounds) - }) - }); - - if let Some((inner, new_bounds, is_browser, bounds_arc, window_bounds)) = data { - inner.set_bounds(Some(&new_bounds)); - if is_browser - && let Some(b) = &mut *bounds_arc.lock().unwrap() - && let Some(wb) = window_bounds - { - let window_size = LogicalSize::new(wb.width as u32, wb.height as u32); - b.width_rate = new_bounds.width as f32 / window_size.width as f32; - b.height_rate = new_bounds.height as f32 / window_size.height as f32; - } - } - } - WebviewMessage::SetBounds(bounds) => { - let data = context - .windows - .borrow() - .get(&window_id) - .and_then(|app_window| { - let device_scale_factor = app_window - .window() - .and_then(|window| window.display()) - .map(|d| d.device_scale_factor() as f64) - .unwrap_or(1.0); - - let new_bounds = rect_to_cef(bounds, device_scale_factor); - app_window - .webviews - .iter() - .find(|w| w.webview_id == webview_id) - .map(|wrapper| { - let inner = wrapper.inner.clone(); - let bounds_arc = wrapper.bounds.clone(); - let is_browser = wrapper.inner.is_browser(); - let window_bounds = if is_browser { - app_window.window().map(|w| w.bounds()) - } else { - None - }; - (inner, new_bounds, is_browser, bounds_arc, window_bounds) - }) - }); - - if let Some((inner, new_bounds, is_browser, bounds_arc, window_bounds)) = data { - inner.set_bounds(Some(&new_bounds)); - if is_browser - && let Some(b) = &mut *bounds_arc.lock().unwrap() - && let Some(wb) = window_bounds - { - let window_size = LogicalSize::new(wb.width as u32, wb.height as u32); - b.x_rate = new_bounds.x as f32 / window_size.width as f32; - b.y_rate = new_bounds.y as f32 / window_size.height as f32; - b.width_rate = new_bounds.width as f32 / window_size.width as f32; - b.height_rate = new_bounds.height as f32 / window_size.height as f32; - } - } - } - WebviewMessage::SetFocus => { - if let Some(host) = get_webview(context, window_id, webview_id) - .and_then(|bv| bv.inner.browser()) - .and_then(|b| b.host()) - { - host.set_focus(1) - } - } - WebviewMessage::Reparent(target_window_id, tx) => { - let reparent_data = { - let mut windows = context.windows.borrow_mut(); - - if !windows.contains_key(&target_window_id) { - let _ = tx.send(Err(tauri_runtime::Error::FailedToSendMessage)); - return; - }; - - let Some(webview_wrapper) = windows.get_mut(&window_id).and_then(|app_window| { - app_window - .webviews - .iter() - .position(|w| w.webview_id == webview_id) - .map(|index| app_window.webviews.remove(index)) - }) else { - let _ = tx.send(Err(tauri_runtime::Error::FailedToSendMessage)); - return; - }; - - let target_cef_window = match windows.get(&target_window_id) { - Some(tw) => match &tw.window { - crate::AppWindowKind::Window(window) => window.clone(), - crate::AppWindowKind::BrowserWindow => { - let _ = tx.send(Err(tauri_runtime::Error::FailedToSendMessage)); - return; - } - }, - None => { - let _ = tx.send(Err(tauri_runtime::Error::FailedToSendMessage)); - return; - } - }; - - (webview_wrapper, target_cef_window) - }; - - let (webview_wrapper, target_cef_window) = reparent_data; - - let bounds = webview_wrapper.inner.bounds(); - webview_wrapper.inner.set_parent(&target_cef_window); - webview_wrapper.inner.set_bounds(Some(&bounds)); - - { - let mut windows = context.windows.borrow_mut(); - if let Some(target_window) = windows.get_mut(&target_window_id) { - target_window.webviews.push(webview_wrapper); - let _ = tx.send(Ok(())); - } else { - let _ = tx.send(Err(tauri_runtime::Error::FailedToSendMessage)); - } - } - } - WebviewMessage::SetAutoResize(auto_resize) => { - if let Some(app_window) = context.windows.borrow().get(&window_id) - && let Some(wrapper) = app_window - .webviews - .iter() - .find(|w| w.webview_id == webview_id) - && wrapper.inner.is_browser() - { - if auto_resize { - if let Some(window) = app_window.window() { - let window_bounds = window.bounds(); - let window_size = - LogicalSize::new(window_bounds.width as u32, window_bounds.height as u32); - - let ob = wrapper.inner.bounds(); - let pos = LogicalPosition::new(ob.x, ob.y); - let size = LogicalSize::new(ob.width as u32, ob.height as u32); - - *wrapper.bounds.lock().unwrap() = Some(crate::WebviewBounds { - x_rate: pos.x as f32 / window_size.width as f32, - y_rate: pos.y as f32 / window_size.height as f32, - width_rate: size.width as f32 / window_size.width as f32, - height_rate: size.height as f32 / window_size.height as f32, - }); - } - } else { - *wrapper.bounds.lock().unwrap() = None; - } - } - } - WebviewMessage::SetZoom(scale_factor) => { - if let Some(host) = get_webview(context, window_id, webview_id) - .and_then(|bv| bv.inner.browser()) - .and_then(|b| b.host()) - { - // CEF uses a logarithmic zoom level where percentage = 1.2^level - // (Chromium's kTextSizeMultiplierRatio). Convert from Tauri linear - // scale factor (1.0 = 100%) to CEF's level (0.0 = 100%) - const CEF_ZOOM_BASE: f64 = 1.2; - let zoom_level = if scale_factor > 0.0 { - scale_factor.ln() / CEF_ZOOM_BASE.ln() - } else { - 0.0 - }; - host.set_zoom_level(zoom_level) - } - } - WebviewMessage::SetBackgroundColor(color) => { - if let Some(bv) = context - .windows - .borrow() - .get(&window_id) - .and_then(|app_window| { - app_window - .webviews - .iter() - .find(|w| w.webview_id == webview_id) - }) - { - bv.webview_attributes.borrow_mut().background_color = color; - - bv.inner.set_background_color(color.map(color_to_cef_argb)); - } - } - WebviewMessage::ClearAllBrowsingData => { - // TODO: Implement clear browsing data - } - // Getters - WebviewMessage::Url(tx) => { - let result = get_main_frame(context, window_id, webview_id) - .map(|frame| cef::CefString::from(&frame.url()).to_string()) - .ok_or(tauri_runtime::Error::FailedToSendMessage); - let _ = tx.send(result); - } - WebviewMessage::Bounds(tx) => { - let result = get_webview(context, window_id, webview_id) - .map(|webview| { - let bounds = webview.inner.bounds(); - let scale = webview.inner.scale_factor(); - let logical_position = LogicalPosition::new(bounds.x, bounds.y); - let logical_size = LogicalSize::new(bounds.width as u32, bounds.height as u32); - let physical_position = logical_position.to_physical::(scale); - let physical_size = logical_size.to_physical::(scale); - tauri_runtime::dpi::Rect { - position: Position::Physical(physical_position), - size: Size::Physical(physical_size), - } - }) - .ok_or(tauri_runtime::Error::FailedToSendMessage); - let _ = tx.send(result); - } - WebviewMessage::Position(tx) => { - let result = get_webview(context, window_id, webview_id) - .map(|webview| { - let bounds = webview.inner.bounds(); - let scale = webview.inner.scale_factor(); - LogicalPosition::new(bounds.x, bounds.y).to_physical::(scale) - }) - .ok_or(tauri_runtime::Error::FailedToSendMessage); - let _ = tx.send(result); - } - WebviewMessage::Size(tx) => { - let result = get_webview(context, window_id, webview_id) - .map(|webview| { - let bounds = webview.inner.bounds(); - let scale = webview.inner.scale_factor(); - let size = LogicalSize::new(bounds.width as u32, bounds.height as u32); - size.to_physical::(scale) - }) - .ok_or(tauri_runtime::Error::FailedToSendMessage); - let _ = tx.send(result); - } - WebviewMessage::WithWebview(f) => { - if let Some(browser_view) = get_browser(context, window_id, webview_id) { - f(Box::new(browser_view)); - } - } - // Devtools - #[cfg(any(debug_assertions, feature = "devtools"))] - WebviewMessage::OpenDevTools => { - if let Some(host) = get_browser(context, window_id, webview_id).and_then(|b| b.host()) { - let window_info = cef::WindowInfo::default(); - let settings = cef::BrowserSettings::default(); - let inspect_at = cef::Point { x: 0, y: 0 }; - host.show_dev_tools( - Some(&window_info), - Option::<&mut cef::Client>::None, - Some(&settings), - Some(&inspect_at), - ); - } - } - #[cfg(any(debug_assertions, feature = "devtools"))] - WebviewMessage::CloseDevTools => { - if let Some(host) = get_browser(context, window_id, webview_id).and_then(|b| b.host()) { - host.close_dev_tools() - } - } - #[cfg(any(debug_assertions, feature = "devtools"))] - WebviewMessage::IsDevToolsOpen(tx) => { - let result = get_browser(context, window_id, webview_id) - .and_then(|b| b.host()) - .map(|host| host.has_dev_tools() != 0) - .unwrap_or(false); - let _ = tx.send(result); - } - WebviewMessage::SendDevToolsMessage(message, tx) => { - let result = get_browser(context, window_id, webview_id) - .and_then(|b| b.host()) - .map(|host| { - let result = host.send_dev_tools_message(Some(&message)); - if result == 1 { - Ok(()) - } else { - Err(tauri_runtime::Error::FailedToSendMessage) - } - }) - .unwrap_or(Err(tauri_runtime::Error::FailedToSendMessage)); - let _ = tx.send(result); - } - WebviewMessage::OnDevToolsProtocol(handler, tx) => { - let result = match get_webview(context, window_id, webview_id) { - Some(webview) => { - let mut handlers = webview.devtools_protocol_handlers.lock().unwrap(); - handlers.push(handler); - // Add the observer when the first listener is registered - if handlers.len() == 1 - && let Some(browser) = get_browser(context, window_id, webview_id) - && let Some(registration) = - add_dev_tools_observer(&browser, webview.devtools_protocol_handlers.clone()) - { - *webview.devtools_observer_registration.lock().unwrap() = Some(registration); - } - Ok(()) - } - None => Err(tauri_runtime::Error::FailedToSendMessage), - }; - let _ = tx.send(result); - } - WebviewMessage::CookiesForUrl(url, tx) => { - // Collect cookies for a specific URL - let url_str = url.as_str().to_string(); - - cef::cookie_manager_get_global_manager(None) - .map(|manager| { - let collected: Arc>>> = - Arc::new(Mutex::new(Vec::new())); - let tx_ = tx.clone(); - - let mut visitor = CollectUrlCookiesVisitor::new(tx_, collected.clone()); - let url_cef = cef::CefString::from(url_str.as_str()); - manager.visit_url_cookies(Some(&url_cef), 1, Some(&mut visitor)); - }) - .or_else(|| { - let _ = tx.send(Ok(Vec::new())); - None - }); - } - WebviewMessage::Cookies(tx) => { - // Collect all cookies - cef::cookie_manager_get_global_manager(None) - .map(|manager| { - let collected: Arc>>> = - Arc::new(Mutex::new(Vec::new())); - let tx_ = tx.clone(); - - let mut visitor = CollectAllCookiesVisitor::new(tx_, collected.clone()); - manager.visit_all_cookies(Some(&mut visitor)); - }) - .or_else(|| { - let _ = tx.send(Ok(Vec::new())); - None - }); - } - WebviewMessage::SetCookie(cookie) => { - if let Some(manager) = cef::cookie_manager_get_global_manager(None) { - // Try to infer a URL for the cookie scope using the currently loaded URL - let url = get_main_frame(context, window_id, webview_id) - .map(|frame| cef::CefString::from(&frame.url()).to_string()) - .unwrap_or_default(); - - let mut cef_cookie = cef::Cookie { - name: cef::CefString::from(cookie.name()), - value: cef::CefString::from(cookie.value()), - ..Default::default() - }; - if let Some(d) = cookie.domain() { - cef_cookie.domain = cef::CefString::from(d); - } - if let Some(p) = cookie.path() { - cef_cookie.path = cef::CefString::from(p); - } - if cookie.secure().unwrap_or(false) { - cef_cookie.secure = 1; - } - if cookie.http_only().unwrap_or(false) { - cef_cookie.httponly = 1; - } - - let url_cef = if url.is_empty() { - None - } else { - Some(cef::CefString::from(url.as_str())) - }; - manager.set_cookie( - url_cef.as_ref(), - Some(&cef_cookie), - Option::<&mut cef::SetCookieCallback>::None, - ); - } - } - WebviewMessage::DeleteCookie(cookie) => { - if let Some(manager) = cef::cookie_manager_get_global_manager(None) { - // Resolve current URL for targeted deletion - let url = get_main_frame(context, window_id, webview_id) - .map(|frame| cef::CefString::from(&frame.url()).to_string()) - .unwrap_or_default(); - let url_cef = if url.is_empty() { - None - } else { - Some(cef::CefString::from(url.as_str())) - }; - let name_cef = Some(cef::CefString::from(cookie.name())); - manager.delete_cookies( - url_cef.as_ref(), - name_cef.as_ref(), - Option::<&mut cef::DeleteCookiesCallback>::None, - ); - } - } - } -} - -#[cfg(target_os = "macos")] -fn start_window_dragging(window: &cef::Window) { - use objc2::rc::Retained; - use objc2_app_kit::{NSEvent, NSEventModifierFlags, NSEventType, NSView}; - - unsafe { - let ns_view = Retained::::retain(window.window_handle() as _); - if let Some(ns_view) = ns_view - && let Some(ns_window) = ns_view.window() - { - // Get current mouse location - let mouse_location = NSEvent::mouseLocation(); - - // Try to get the current event from NSApp - let mut event = None; - if let Some(mtm) = objc2::MainThreadMarker::new() { - let ns_app = objc2_app_kit::NSApp(mtm); - event = ns_app.currentEvent(); - } - - // Create a mouse event for dragging - // If we have a current event, try to use its properties - let drag_event = if let Some(current_event) = event { - let event_modifier_flags = current_event.modifierFlags(); - let event_timestamp = current_event.timestamp(); - let event_window_number = current_event.windowNumber(); - - NSEvent::mouseEventWithType_location_modifierFlags_timestamp_windowNumber_context_eventNumber_clickCount_pressure( - NSEventType::LeftMouseDown, - mouse_location, - event_modifier_flags, - event_timestamp, - event_window_number, - None, - 0, - 1, - 1.0, - ) - } else { - NSEvent::mouseEventWithType_location_modifierFlags_timestamp_windowNumber_context_eventNumber_clickCount_pressure( - NSEventType::LeftMouseDown, - mouse_location, - NSEventModifierFlags::empty(), - 0.0, - ns_window.windowNumber(), - None, - 0, - 1, - 1.0, - ) - }; - - if let Some(event) = drag_event { - ns_window.performWindowDragWithEvent(&event); - } - } - } -} - -#[cfg(windows)] -fn start_window_dragging(window: &cef::Window) { - use windows::Win32::Foundation::*; - use windows::Win32::UI::Input::KeyboardAndMouse::*; - use windows::Win32::UI::WindowsAndMessaging::*; - - unsafe { - let hwnd = window.window_handle(); - - let mut pos = std::mem::zeroed(); - let _ = GetCursorPos(&mut pos); - - let points = POINTS { - x: pos.x as i16, - y: pos.y as i16, - }; - - let _ = ReleaseCapture(); - - let _ = PostMessageW( - Some(HWND(hwnd.0 as _)), - WM_NCLBUTTONDOWN, - WPARAM(HTCAPTION as usize), - LPARAM(&points as *const _ as isize), - ); - } -} - -#[cfg(any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" -))] -fn start_window_dragging(window: &cef::Window) { - use std::ffi::CString; - use std::os::raw::c_long; - use x11_dl::xlib; - - let Some(xlib) = xlib::Xlib::open().ok() else { - return; - }; - - unsafe { - let display = (xlib.XOpenDisplay)(std::ptr::null()); - if display.is_null() { - return; - } - - let win = window.window_handle(); - - let mut root_x: std::ffi::c_int = 0; - let mut root_y: std::ffi::c_int = 0; - let mut _win_x: std::ffi::c_int = 0; - let mut _win_y: std::ffi::c_int = 0; - let mut _mask: std::ffi::c_uint = 0; - let mut root: xlib::Window = (xlib.XDefaultRootWindow)(display); - let mut _child_return: xlib::Window = 0; - let _ = (xlib.XQueryPointer)( - display, - win, - &mut root, - &mut _child_return, - &mut root_x, - &mut root_y, - &mut _win_x, - &mut _win_y, - &mut _mask, - ); - - let net_wm_moveresize = CString::new("_NET_WM_MOVERESIZE").unwrap(); - let atom = (xlib.XInternAtom)(display, net_wm_moveresize.as_ptr(), xlib::False); - if atom == 0 { - (xlib.XCloseDisplay)(display); - return; - } - - // EWMH _NET_WM_MOVERESIZE: direction 8 = move, button 1 = left, source 1 = application - const NET_WM_MOVERESIZE_MOVE: c_long = 8; - const SOURCE_APPLICATION: c_long = 1; - - let mut data: xlib::ClientMessageData = std::mem::zeroed(); - { - let longs = >::as_mut(&mut data); - longs[0] = root_x as i64; - longs[1] = root_y as i64; - longs[2] = NET_WM_MOVERESIZE_MOVE; - longs[3] = 1; // Button 1 (left) - longs[4] = SOURCE_APPLICATION; - } - - let xclient = xlib::XClientMessageEvent { - type_: xlib::ClientMessage, - serial: 0, - send_event: xlib::True, - display, - window: win, - message_type: atom, - format: 32, - data, - }; - - let mut event: xlib::XEvent = xclient.into(); - let _ = (xlib.XSendEvent)(display, root, xlib::False, 0, &mut event); - (xlib.XFlush)(display); - (xlib.XCloseDisplay)(display); - } -} - -fn handle_window_message( - context: &Context, - window_id: WindowId, - message: WindowMessage, -) { - match message { - WindowMessage::Close => { - on_close_requested(window_id, &context.windows, &context.callback); - } - WindowMessage::Destroy => { - on_window_close(window_id, &context.windows); - } - WindowMessage::AddEventListener(event_id, handler) => { - if let Some(app_window) = context.windows.borrow().get(&window_id) { - app_window - .window_event_listeners - .lock() - .unwrap() - .insert(event_id, handler); - } - } - // Getters - WindowMessage::ScaleFactor(tx) => { - let result = context - .windows - .borrow() - .get(&window_id) - .and_then(|w| { - w.window() - .and_then(|window| window.display().map(|d| Ok(d.device_scale_factor() as f64))) - }) - .unwrap_or_else(|| Err(tauri_runtime::Error::FailedToSendMessage)); - let _ = tx.send(result); - } - WindowMessage::InnerPosition(tx) => { - let result = context - .windows - .borrow() - .get(&window_id) - .map(|w| match &w.window { - crate::AppWindowKind::Window(window) => { - let bounds = window.bounds(); - let scale = window - .display() - .map(|d| d.device_scale_factor() as f64) - .unwrap_or(1.0); - Ok(LogicalPosition::new(bounds.x, bounds.y).to_physical::(scale)) - } - crate::AppWindowKind::BrowserWindow => Err(tauri_runtime::Error::FailedToSendMessage), - }) - .unwrap_or_else(|| Err(tauri_runtime::Error::FailedToSendMessage)); - let _ = tx.send(result); - } - WindowMessage::OuterPosition(tx) => { - let result = context - .windows - .borrow() - .get(&window_id) - .map(|w| match &w.window { - crate::AppWindowKind::Window(window) => { - let bounds = window.bounds(); - let scale = window - .display() - .map(|d| d.device_scale_factor() as f64) - .unwrap_or(1.0); - Ok(LogicalPosition::new(bounds.x, bounds.y).to_physical::(scale)) - } - crate::AppWindowKind::BrowserWindow => Err(tauri_runtime::Error::FailedToSendMessage), - }) - .unwrap_or_else(|| Err(tauri_runtime::Error::FailedToSendMessage)); - let _ = tx.send(result); - } - WindowMessage::InnerSize(tx) => { - let result = context - .windows - .borrow() - .get(&window_id) - .map(|w| match &w.window { - crate::AppWindowKind::Window(window) => { - #[cfg(not(windows))] - let size = { - let scale = window - .display() - .map(|d| d.device_scale_factor() as f64) - .unwrap_or(1.0); - - let bounds = window.bounds(); - LogicalSize::new(bounds.width as u32, bounds.height as u32).to_physical::(scale) - }; - - // On Windows, window.bounds() is the outer size, not the inner size. - #[cfg(windows)] - let size = crate::utils::windows::inner_size(window.window_handle()); - - Ok(size) - } - crate::AppWindowKind::BrowserWindow => Err(tauri_runtime::Error::FailedToSendMessage), - }) - .unwrap_or_else(|| Err(tauri_runtime::Error::FailedToSendMessage)); - let _ = tx.send(result); - } - WindowMessage::OuterSize(tx) => { - let result = context - .windows - .borrow() - .get(&window_id) - .map(|w| match &w.window { - crate::AppWindowKind::Window(window) => { - let bounds = window.bounds(); - let scale = window - .display() - .map(|d| d.device_scale_factor() as f64) - .unwrap_or(1.0); - Ok( - LogicalSize::new(bounds.width as u32, bounds.height as u32).to_physical::(scale), - ) - } - crate::AppWindowKind::BrowserWindow => Err(tauri_runtime::Error::FailedToSendMessage), - }) - .unwrap_or_else(|| Err(tauri_runtime::Error::FailedToSendMessage)); - let _ = tx.send(result); - } - WindowMessage::IsFullscreen(tx) => { - let result = context - .windows - .borrow() - .get(&window_id) - .map(|w| match &w.window { - crate::AppWindowKind::Window(window) => Ok(window.is_fullscreen() == 1), - crate::AppWindowKind::BrowserWindow => Err(tauri_runtime::Error::FailedToSendMessage), - }) - .unwrap_or_else(|| Err(tauri_runtime::Error::FailedToSendMessage)); - let _ = tx.send(result); - } - WindowMessage::IsMinimized(tx) => { - let result = context - .windows - .borrow() - .get(&window_id) - .map(|w| match &w.window { - crate::AppWindowKind::Window(window) => Ok(window.is_minimized() == 1), - crate::AppWindowKind::BrowserWindow => Err(tauri_runtime::Error::FailedToSendMessage), - }) - .unwrap_or_else(|| Err(tauri_runtime::Error::FailedToSendMessage)); - let _ = tx.send(result); - } - WindowMessage::IsMaximized(tx) => { - let result = context - .windows - .borrow() - .get(&window_id) - .map(|w| match &w.window { - crate::AppWindowKind::Window(window) => Ok(window.is_maximized() == 1), - crate::AppWindowKind::BrowserWindow => Err(tauri_runtime::Error::FailedToSendMessage), - }) - .unwrap_or_else(|| Err(tauri_runtime::Error::FailedToSendMessage)); - let _ = tx.send(result); - } - WindowMessage::IsFocused(tx) => { - let result = context - .windows - .borrow() - .get(&window_id) - .map(|w| match &w.window { - crate::AppWindowKind::Window(window) => Ok(window.has_focus() == 1), - crate::AppWindowKind::BrowserWindow => Err(tauri_runtime::Error::FailedToSendMessage), - }) - .unwrap_or_else(|| Err(tauri_runtime::Error::FailedToSendMessage)); - let _ = tx.send(result); - } - WindowMessage::IsDecorated(tx) => { - let result = context - .windows - .borrow() - .get(&window_id) - .map(|w| Ok(w.attributes.borrow().decorations.unwrap_or(true))) - .unwrap_or_else(|| Err(tauri_runtime::Error::FailedToSendMessage)); - let _ = tx.send(result); - } - WindowMessage::IsResizable(tx) => { - let result = context - .windows - .borrow() - .get(&window_id) - .map(|w| Ok(w.attributes.borrow().resizable.unwrap_or(true))) - .unwrap_or_else(|| Err(tauri_runtime::Error::FailedToSendMessage)); - let _ = tx.send(result); - } - WindowMessage::IsMaximizable(tx) => { - let result = context - .windows - .borrow() - .get(&window_id) - .map(|w| Ok(w.attributes.borrow().maximizable.unwrap_or(true))) - .unwrap_or_else(|| Err(tauri_runtime::Error::FailedToSendMessage)); - let _ = tx.send(result); - } - WindowMessage::IsMinimizable(tx) => { - let result = context - .windows - .borrow() - .get(&window_id) - .map(|w| Ok(w.attributes.borrow().minimizable.unwrap_or(true))) - .unwrap_or_else(|| Err(tauri_runtime::Error::FailedToSendMessage)); - let _ = tx.send(result); - } - WindowMessage::IsClosable(tx) => { - let result = context - .windows - .borrow() - .get(&window_id) - .map(|w| Ok(w.attributes.borrow().closable.unwrap_or(true))) - .unwrap_or_else(|| Err(tauri_runtime::Error::FailedToSendMessage)); - let _ = tx.send(result); - } - WindowMessage::IsVisible(tx) => { - let result = context - .windows - .borrow() - .get(&window_id) - .map(|w| match &w.window { - crate::AppWindowKind::Window(window) => Ok(window.is_visible() == 1), - crate::AppWindowKind::BrowserWindow => Err(tauri_runtime::Error::FailedToSendMessage), - }) - .unwrap_or_else(|| Err(tauri_runtime::Error::FailedToSendMessage)); - let _ = tx.send(result); - } - WindowMessage::Title(tx) => { - let result = context - .windows - .borrow() - .get(&window_id) - .map(|w| match &w.window { - crate::AppWindowKind::Window(window) => { - let title = window.title(); - Ok(cef::CefString::from(&title).to_string()) - } - crate::AppWindowKind::BrowserWindow => Err(tauri_runtime::Error::FailedToSendMessage), - }) - .unwrap_or_else(|| Err(tauri_runtime::Error::FailedToSendMessage)); - let _ = tx.send(result); - } - WindowMessage::CurrentMonitor(tx) => { - let result = context - .windows - .borrow() - .get(&window_id) - .and_then(|w| w.window()) - .map(|window| { - let b = window.bounds(); - cef::display_get_matching_bounds(Some(&b), 1).map(|d| { - let bounds = d.bounds(); - let work = d.work_area(); - let scale = d.device_scale_factor() as f64; - let physical_size = - LogicalSize::new(bounds.width as u32, bounds.height as u32).to_physical::(scale); - let physical_position = - LogicalPosition::new(bounds.x, bounds.y).to_physical::(scale); - let work_physical_size = - LogicalSize::new(work.width as u32, work.height as u32).to_physical::(scale); - let work_physical_position = - LogicalPosition::new(work.x, work.y).to_physical::(scale); - tauri_runtime::monitor::Monitor { - name: None, - size: PhysicalSize::new(physical_size.width, physical_size.height), - position: PhysicalPosition::new(physical_position.x, physical_position.y), - work_area: PhysicalRect { - position: PhysicalPosition::new(work_physical_position.x, work_physical_position.y), - size: PhysicalSize::new(work_physical_size.width, work_physical_size.height), - }, - scale_factor: d.device_scale_factor() as f64, - } - }) - }) - .map(Ok) - .unwrap_or_else(|| Err(tauri_runtime::Error::FailedToSendMessage)); - let _ = tx.send(result); - } - WindowMessage::PrimaryMonitor(tx) => { - let result = Ok(get_primary_monitor()); - let _ = tx.send(result); - } - WindowMessage::MonitorFromPoint(tx, x, y) => { - let result = Ok(get_monitor_from_point(x, y)); - let _ = tx.send(result); - } - WindowMessage::AvailableMonitors(tx) => { - let monitors = get_available_monitors(); - let _ = tx.send(Ok(monitors)); - } - WindowMessage::Theme(tx) => { - let result = context - .windows - .borrow() - .get(&window_id) - .map(|w| Ok(native_window_theme(w).unwrap_or(tauri_utils::Theme::Light))) - .unwrap_or_else(|| Err(tauri_runtime::Error::FailedToSendMessage)); - let _ = tx.send(result); - } - WindowMessage::IsEnabled(tx) => { - let _ = tx.send(Ok(true)); - } - WindowMessage::IsAlwaysOnTop(tx) => { - let result = context - .windows - .borrow() - .get(&window_id) - .map(|w| match &w.window { - crate::AppWindowKind::Window(window) => Ok(window.is_always_on_top() == 1), - crate::AppWindowKind::BrowserWindow => Err(tauri_runtime::Error::FailedToSendMessage), - }) - .unwrap_or_else(|| Err(tauri_runtime::Error::FailedToSendMessage)); - let _ = tx.send(result); - } - WindowMessage::RawWindowHandle(tx) => { - let result = context - .windows - .borrow() - .get(&window_id) - .map(|w| match &w.window { - crate::AppWindowKind::Window(window) => { - #[cfg(target_os = "linux")] - unsafe { - let xid = window.window_handle(); - Ok(raw_window_handle::WindowHandle::borrow_raw( - raw_window_handle::RawWindowHandle::Xlib(raw_window_handle::XlibWindowHandle::new( - xid, - )), - )) - } - - #[cfg(target_os = "macos")] - unsafe { - let ns_view = window.window_handle(); - if let Some(nn) = std::ptr::NonNull::new(ns_view) { - Ok(raw_window_handle::WindowHandle::borrow_raw( - raw_window_handle::RawWindowHandle::AppKit( - raw_window_handle::AppKitWindowHandle::new(nn), - ), - )) - } else { - Err(raw_window_handle::HandleError::Unavailable) - } - } - - #[cfg(windows)] - unsafe { - let hwnd = window.window_handle().0 as isize; - if let Some(nz) = std::num::NonZeroIsize::new(hwnd) { - Ok(raw_window_handle::WindowHandle::borrow_raw( - raw_window_handle::RawWindowHandle::Win32( - raw_window_handle::Win32WindowHandle::new(nz), - ), - )) - } else { - Err(raw_window_handle::HandleError::Unavailable) - } - } - } - crate::AppWindowKind::BrowserWindow => Err(raw_window_handle::HandleError::Unavailable), - }) - .unwrap_or(Err(raw_window_handle::HandleError::Unavailable)); - let _ = tx.send(result); - } - // Setters - WindowMessage::Center => { - if let Some(app_window) = context.windows.borrow().get(&window_id) - && let Some(window) = app_window.window() - { - window.center_window(Some(&window.size())); - } - } - WindowMessage::RequestUserAttention(_attention_type) => { - // TODO: Implement user attention - } - WindowMessage::SetEnabled(_enabled) => { - // TODO: Implement enabled - } - WindowMessage::SetResizable(resizable) => { - if let Some(app_window) = context.windows.borrow().get(&window_id) { - app_window.attributes.borrow_mut().resizable = Some(resizable); - } - } - WindowMessage::SetMaximizable(maximizable) => { - if let Some(app_window) = context.windows.borrow().get(&window_id) { - app_window.attributes.borrow_mut().maximizable = Some(maximizable); - } - } - WindowMessage::SetMinimizable(minimizable) => { - if let Some(app_window) = context.windows.borrow().get(&window_id) { - app_window.attributes.borrow_mut().minimizable = Some(minimizable); - } - } - WindowMessage::SetClosable(closable) => { - if let Some(app_window) = context.windows.borrow().get(&window_id) { - app_window.attributes.borrow_mut().closable = Some(closable); - } - } - WindowMessage::SetTitle(title) => { - if let Some(app_window) = context.windows.borrow().get(&window_id) - && let Some(window) = app_window.window() - { - window.set_title(Some(&cef::CefString::from(title.as_str()))); - } - } - WindowMessage::Maximize => { - if let Some(app_window) = context.windows.borrow().get(&window_id) - && let Some(window) = app_window.window() - { - window.maximize(); - } - } - WindowMessage::Unmaximize => { - if let Some(app_window) = context.windows.borrow().get(&window_id) - && let Some(window) = app_window.window() - { - window.restore(); - } - } - WindowMessage::Minimize => { - if let Some(app_window) = context.windows.borrow().get(&window_id) - && let Some(window) = app_window.window() - { - window.minimize(); - } - } - WindowMessage::Unminimize => { - if let Some(app_window) = context.windows.borrow().get(&window_id) - && let Some(window) = app_window.window() - { - window.restore(); - } - } - WindowMessage::Show => { - if let Some(app_window) = context.windows.borrow().get(&window_id) - && let Some(window) = app_window.window() - { - window.show(); - } - } - WindowMessage::Hide => { - if let Some(app_window) = context.windows.borrow().get(&window_id) - && let Some(window) = app_window.window() - { - window.hide(); - } - } - WindowMessage::SetDecorations(decorations) => { - if let Some(app_window) = context.windows.borrow().get(&window_id) { - app_window.attributes.borrow_mut().decorations = Some(decorations); - } - } - WindowMessage::SetShadow(_shadow) => { - // TODO: Implement shadow - } - WindowMessage::SetAlwaysOnBottom(always_on_bottom) => { - if let Some(app_window) = context.windows.borrow().get(&window_id) { - app_window.attributes.borrow_mut().always_on_bottom = Some(always_on_bottom); - } - // TODO: Apply always on bottom via platform-specific CEF APIs if available - } - WindowMessage::SetAlwaysOnTop(always_on_top) => { - if let Some(app_window) = context.windows.borrow().get(&window_id) { - app_window.attributes.borrow_mut().always_on_top = Some(always_on_top); - if let Some(window) = app_window.window() { - window.set_always_on_top(if always_on_top { 1 } else { 0 }); - } - } - } - WindowMessage::SetVisibleOnAllWorkspaces(visible_on_all_workspaces) => { - if let Some(app_window) = context.windows.borrow().get(&window_id) { - app_window.attributes.borrow_mut().visible_on_all_workspaces = - Some(visible_on_all_workspaces); - } - // TODO: Apply visible on all workspaces via platform-specific CEF APIs if available - } - WindowMessage::SetContentProtected(protected) => { - if let Some(app_window) = context.windows.borrow().get(&window_id) { - app_window.attributes.borrow_mut().content_protected = Some(protected); - if let Some(window) = app_window.window() { - apply_content_protection(&window, protected); - } - } - } - #[allow(unused_mut)] - WindowMessage::SetSize(mut size) => { - if let Some(app_window) = context.windows.borrow().get(&window_id) - && let Some(window) = app_window.window() - && let Some(display) = window.display() - { - let device_scale_factor = display.device_scale_factor() as f64; - - let size = size_to_cef(size, device_scale_factor); - - // On Windows, the size set via CEF APIs is the outer size (including borders), - // so we need to adjust it to set the correct inner size. - #[cfg(windows)] - let size = crate::utils::windows::adjust_size(window.window_handle(), size); - - window.set_size(Some(&size)); - } - } - WindowMessage::SetMinSize(size) => { - if let Some(app_window) = context.windows.borrow().get(&window_id) { - app_window.attributes.borrow_mut().min_inner_size = size; - } - } - WindowMessage::SetMaxSize(size) => { - if let Some(app_window) = context.windows.borrow().get(&window_id) { - app_window.attributes.borrow_mut().max_inner_size = size; - } - } - WindowMessage::SetSizeConstraints(constraints) => { - if let Some(app_window) = context.windows.borrow().get(&window_id) { - app_window.attributes.borrow_mut().inner_size_constraints = Some(constraints); - } - } - WindowMessage::SetPosition(position) => { - if let Some(app_window) = context.windows.borrow().get(&window_id) - && let Some(window) = app_window.window() - && let Some(display) = window.display() - { - let device_scale_factor = display.device_scale_factor() as f64; - let position = position_to_cef(position, device_scale_factor); - window.set_position(Some(&position)); - } - } - WindowMessage::SetFullscreen(fullscreen) => { - if let Some(app_window) = context.windows.borrow().get(&window_id) - && let Some(window) = app_window.window() - { - window.set_fullscreen(if fullscreen { 1 } else { 0 }); - } - } - #[cfg(target_os = "macos")] - WindowMessage::SetSimpleFullscreen(_fullscreen) => { - // TODO: Implement simple fullscreen - } - WindowMessage::SetFocus => { - if let Some(app_window) = context.windows.borrow().get(&window_id) - && let Some(window) = app_window.window() - { - window.request_focus(); - } - } - WindowMessage::SetFocusable(focusable) => { - if let Some(app_window) = context.windows.borrow().get(&window_id) - && let Some(window) = app_window.window() - { - window.set_focusable(if focusable { 1 } else { 0 }); - } - } - WindowMessage::SetIcon(icon) => { - if let Some(app_window) = context.windows.borrow().get(&window_id) - && let Some(window) = app_window.window() - { - set_window_icon(&window, icon); - } - } - WindowMessage::SetSkipTaskbar(_skip) => { - // TODO: Implement skip taskbar - } - WindowMessage::SetCursorGrab(_grab) => { - // TODO: Implement cursor grab - } - WindowMessage::SetCursorVisible(_visible) => { - // TODO: Implement cursor visible - } - WindowMessage::SetCursorIcon(_icon) => { - // TODO: Implement cursor icon - } - WindowMessage::SetCursorPosition(_position) => { - // TODO: Implement cursor position - } - WindowMessage::SetIgnoreCursorEvents(_ignore) => { - // TODO: Implement ignore cursor events - } - WindowMessage::SetProgressBar(_progress_state) => { - // TODO: Implement progress bar - } - WindowMessage::SetBadgeCount(_count, _desktop_filename) => { - // TODO: Implement badge count - } - WindowMessage::SetBadgeLabel(_label) => { - // TODO: Implement badge label - } - WindowMessage::SetOverlayIcon(icon) => { - if let Some(app_window) = context.windows.borrow().get(&window_id) - && let Some(window) = app_window.window() - { - set_overlay_icon(&window, icon); - } - } - WindowMessage::SetTitleBarStyle(_style) => { - // TODO: Implement title bar style - } - WindowMessage::SetTrafficLightPosition(_position) => { - #[cfg(target_os = "macos")] - if let Some(app_window) = context.windows.borrow().get(&window_id) { - app_window.attributes.borrow_mut().traffic_light_position = Some(_position); - if let Some(window) = app_window.window() { - apply_traffic_light_position(window.window_handle(), &_position); - } - } - } - WindowMessage::SetTheme(theme) => { - if let Some(app_window) = context.windows.borrow().get(&window_id) { - { - let mut attributes = app_window.attributes.borrow_mut(); - attributes.theme = theme; - } - apply_window_theme_scheme(app_window, theme); - #[cfg(target_os = "macos")] - { - let window = app_window.window(); - apply_macos_window_theme(window.as_ref(), theme); - } - // theme changed event is sent by the on_theme_changed handler - } - } - WindowMessage::SetBackgroundColor(color) => { - if let Some(app_window) = context.windows.borrow().get(&window_id) { - app_window.attributes.borrow_mut().background_color = color; - let Some(window) = app_window.window() else { - return; - }; - let color = color - .map(color_to_cef_argb) - .unwrap_or_else(|| window.theme_color(ColorId::COLOR_PRIMARY_BACKGROUND.get_raw() as _)); - window.set_background_color(color); - } - } - WindowMessage::StartDragging => { - if let Some(app_window) = context.windows.borrow().get(&window_id) - && let Some(window) = app_window.window() - { - start_window_dragging(&window); - } - } - WindowMessage::StartResizeDragging(_direction) => { - // TODO: Implement start resize dragging - } - } -} - -pub fn handle_message(context: &Context, message: Message) { - match message { - Message::CreateWindow { - window_id, - webview_id, - pending, - after_window_creation: _todo, - } => create_window(context, window_id, webview_id, *pending), - Message::CreateWebview { - window_id, - webview_id, - pending, - } => create_webview( - WebviewKind::WindowChild, - context, - window_id, - webview_id, - *pending, - ), - Message::Window { window_id, message } => { - handle_window_message(context, window_id, message); - } - Message::Webview { - window_id, - webview_id, - message, - } => handle_webview_message(context, window_id, webview_id, message), - Message::RequestExit(code) => { - let (tx, rx) = channel(); - (context.callback.borrow())(RunEvent::ExitRequested { - code: Some(code), - tx, - }); - - let recv = rx.try_recv(); - let should_prevent = matches!(recv, Ok(ExitRequestedEventAction::Prevent)); - - if !should_prevent { - (context.callback.borrow())(RunEvent::Exit); - } - } - Message::Task(t) => t(), - Message::UserEvent(evt) => { - (context.callback.borrow())(RunEvent::UserEvent(evt)); - } - Message::Noop => {} - } -} - -wrap_task! { - pub struct SendMessageTask { - context: Context, - message: Arc>>, - } - - impl Task { - fn execute(&self) { - handle_message(&self.context, std::mem::replace(&mut self.message.borrow_mut(), Message::Noop)); - } - } -} - -fn create_browser_window( - context: &Context, - window_id: WindowId, - webview_id: u32, - label: String, - window_builder: CefWindowBuilder, - webview: PendingWebview>, -) { - let PendingWebview { - label: webview_label, - opener: _, - mut webview_attributes, - platform_specific_attributes: _, - uri_scheme_protocols, - ipc_handler: _, - navigation_handler, - new_window_handler, - document_title_changed_handler, - address_changed_handler, - url, - web_resource_request_handler: _, - mut on_page_load_handler, - download_handler, - } = webview; - - let address_changed_handler = address_changed_handler - .map(|h| Arc::new(move |url: &url::Url| h(url)) as Arc); - - let initialization_scripts = std::mem::take(&mut webview_attributes.initialization_scripts) - .into_iter() - .map(CefInitScript::new) - .collect::>(); - let initialization_scripts = Arc::new(initialization_scripts); - - let on_page_load_handler = on_page_load_handler.take().map(Arc::from); - let document_title_changed_handler = document_title_changed_handler.map(Arc::from); - let navigation_handler = navigation_handler.map(Arc::from); - let new_window_handler = new_window_handler.map(Arc::from); - - let devtools_enabled = (cfg!(debug_assertions) || cfg!(feature = "devtools")) - && webview_attributes.devtools.unwrap_or(true); - - let custom_protocol_scheme = if webview_attributes.use_https_scheme { - "https" - } else { - "http" - }; - - // Build cached domain names for custom schemes and clone protocols for storage - // before uri_scheme_protocols is moved - let scheme_keys: Vec = uri_scheme_protocols.keys().cloned().collect(); - let custom_scheme_domain_names: Vec = scheme_keys - .iter() - .map(|scheme| format!("{scheme}.localhost")) - .collect(); - - let uri_scheme_protocols: HashMap>> = - uri_scheme_protocols - .into_iter() - .map(|(k, v)| (k, Arc::new(v))) - .collect(); - - let custom_schemes = uri_scheme_protocols.keys().cloned().collect::>(); - - let mut request_context = request_context_from_webview_attributes( - context, - &webview_attributes, - &custom_schemes, - custom_protocol_scheme, - &initialization_scripts, - ); - apply_request_context_theme_scheme(request_context.as_ref(), window_builder.theme); - - let browser_settings = browser_settings_from_webview_attributes(&webview_attributes); - - // Create the AppWindow with BrowserWindow variant before creating the browser - let force_close = Arc::new(AtomicBool::new(false)); - let attributes = Arc::new(RefCell::new(window_builder)); - - let initial_url = url.clone(); - let url = CefString::from(url.as_str()); - - let mut client = BrowserClient::new( - WindowKind::Browser, - window_id, - initialization_scripts.clone(), - on_page_load_handler, - document_title_changed_handler, - navigation_handler, - address_changed_handler, - new_window_handler, - download_handler, - devtools_enabled, - custom_scheme_domain_names.clone(), - custom_protocol_scheme.to_string(), - context.clone(), - Some(initial_url), - ); - - let mut bounds = cef::Rect { - x: 0, - y: 0, - width: 800, - height: 600, - }; - let device_scale_factor = display_get_primary() - .map(|d| d.device_scale_factor() as f64) - .unwrap_or(1.); - if let Some(size) = attributes.borrow().inner_size { - let size = size_to_cef(size, device_scale_factor); - bounds.width = size.width; - bounds.height = size.height; - } - if let Some(position) = attributes.borrow().position { - let position = position_to_cef(position, device_scale_factor); - bounds.x = position.x; - bounds.y = position.y; - } - - let window_info = cef::WindowInfo { - bounds, - ..Default::default() - }; - - let Some(browser) = browser_host_create_browser_sync( - Some(&window_info), - Some(&mut client), - Some(&url), - Some(&browser_settings), - None, - request_context.as_mut(), - ) else { - eprintln!("Failed to create browser"); - return; - }; - - let devtools_protocol_handlers = Arc::new(Mutex::new(Vec::< - Arc, - >::new())); - let devtools_observer_registration = Arc::new(Mutex::new(add_dev_tools_observer( - &browser, - devtools_protocol_handlers.clone(), - ))); - - let browser = CefWebview::Browser(browser); - let browser_id_val = browser.browser_id(); - - { - let mut registry = context.scheme_handler_registry.lock().unwrap(); - for (scheme, handler) in &uri_scheme_protocols { - registry.insert( - (browser_id_val, scheme.clone()), - ( - webview_label.clone(), - handler.clone(), - initialization_scripts.clone(), - ), - ); - } - } - - context.windows.borrow_mut().insert( - window_id, - AppWindow { - label, - window: crate::AppWindowKind::BrowserWindow, - force_close: force_close.clone(), - attributes: attributes.clone(), - webviews: vec![AppWebview { - webview_id, - browser_id: Arc::new(RefCell::new(browser_id_val)), - label: webview_label, - inner: browser, - bounds: Arc::new(Mutex::new(None)), - devtools_enabled, - uri_scheme_protocols: Arc::new(uri_scheme_protocols), - initialization_scripts, - devtools_protocol_handlers, - devtools_observer_registration, - webview_attributes: Arc::new(RefCell::new(webview_attributes)), - }], - window_event_listeners: Arc::new(Mutex::new(HashMap::new())), - webview_event_listeners: Arc::new(Mutex::new(HashMap::new())), - }, - ); -} - -pub(crate) fn create_window( - context: &Context, - window_id: WindowId, - webview_id: u32, - pending: PendingWindow>, -) { - let PendingWindow { - label, - window_builder, - webview, - } = pending; - - if window_builder.browser_window { - if let Some(webview) = webview { - return create_browser_window( - context, - window_id, - webview_id, - label, - window_builder, - webview, - ); - } else { - panic!("unexpected browser_window without webview config"); - } - } - - let force_close = Arc::new(AtomicBool::new(false)); - let attributes = Arc::new(RefCell::new(window_builder)); - - let mut delegate = AppWindowDelegate::::new( - window_id, - context.callback.clone(), - force_close.clone(), - context.windows.clone(), - attributes.clone(), - RefCell::new(Default::default()), - RefCell::new(Default::default()), - RefCell::new(false), - context.clone(), - ); - - let window = window_create_top_level(Some(&mut delegate)).expect("Failed to create window"); - - context.windows.borrow_mut().insert( - window_id, - AppWindow { - label, - window: crate::AppWindowKind::Window(window), - force_close, - attributes, - webviews: Vec::new(), - window_event_listeners: Arc::new(Mutex::new(HashMap::new())), - webview_event_listeners: Arc::new(Mutex::new(HashMap::new())), - }, - ); - - if let Some(webview) = webview { - create_webview( - WebviewKind::WindowContent, - context, - window_id, - webview_id, - webview, - ); - } -} - -#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] -pub(crate) enum WebviewKind { - // webview is the entire window content - WindowContent, - // webview is a child of the window, which can contain other webviews too - WindowChild, -} - -wrap_task! { - struct WindowEventTask { - window_id: WindowId, - windows: Arc>>, - callback: RunEventCallback, - event: WindowEvent, - } - - impl Task { - fn execute(&self) { - send_window_event( - self.window_id, - &self.windows, - &self.callback, - self.event.clone(), - ); - } - } -} - -#[cfg(target_os = "macos")] -fn send_message_task(context: &Context, message: Message) { - let mut task = SendMessageTask::new(context.clone(), Arc::new(RefCell::new(message))); - cef::post_task(sys::cef_thread_id_t::TID_UI.into(), Some(&mut task)); -} - -fn send_window_event( - window_id: WindowId, - windows: &Arc>>, - callback: &RunEventCallback, - event: WindowEvent, -) { - let Ok(windows_ref) = windows.try_borrow() else { - // post task to run later - windows currently mutably borrowed - // happens usually on reparent or destroy when there's a focus change event - let mut task = - WindowEventTask::new(window_id, windows.clone(), callback.clone(), event.clone()); - - cef::post_task(sys::cef_thread_id_t::TID_UI.into(), Some(&mut task)); - return; - }; - - if let Some(w) = windows_ref.get(&window_id) { - let label = w.label.clone(); - let window_event_listeners = w.window_event_listeners.clone(); - - drop(windows_ref); - - { - let listeners = window_event_listeners.lock().unwrap(); - let handlers: Vec<_> = listeners.values().collect(); - for handler in handlers.iter() { - handler(&event); - } - } - - (callback.borrow())(RunEvent::WindowEvent { label, event }); - } -} - -fn on_close_requested( - window_id: WindowId, - windows: &Arc>>, - callback: &RunEventCallback, -) { - let (tx, rx) = channel(); - let event = WindowEvent::CloseRequested { signal_tx: tx }; - - send_window_event(window_id, windows, callback, event.clone()); - - let prevent = rx.try_recv().unwrap_or_default(); - - if !prevent { - on_window_close(window_id, windows); - } -} - -// Collects the browser hosts from the webviews. -fn collect_hosts(webviews: &[AppWebview]) -> Vec { - webviews - .iter() - .filter_map(|webview| webview.inner.browser().and_then(|b| b.host())) - .collect() -} - -/// Force-close all windows, triggering the normal CEF lifecycle: -/// force_close → can_close → close_window_browsers → on_before_close → on_window_destroyed. -pub fn close_all_windows(windows: &Arc>>) { - let window_ids: Vec<_> = windows.borrow().keys().copied().collect(); - for window_id in window_ids { - on_window_close(window_id, windows); - } -} - -/// Close all browsers for a specific window. -/// -/// Returns true if all browsers were closed. -fn close_window_browsers( - window_id: WindowId, - windows: &Arc>>, -) -> bool { - let hosts = { - let windows_ref = windows.borrow(); - let Some(app_window) = windows_ref.get(&window_id) else { - return true; - }; - collect_hosts(&app_window.webviews) - }; - - let mut all_closed = true; - for host in hosts { - host.close_dev_tools(); - if host.try_close_browser() != 1 { - all_closed = false; - } - } - - all_closed -} - -fn on_window_close(window_id: WindowId, windows: &Arc>>) { - let cef_window = { - let windows_ref = windows.borrow(); - let Some(app_window) = windows_ref.get(&window_id) else { - return; - }; - app_window.force_close.store(true, Ordering::SeqCst); - app_window.window() - }; - - if let Some(window) = cef_window { - window.close(); - } -} - -fn on_window_destroyed(window_id: WindowId, context: &Context) { - if context.windows.borrow().get(&window_id).is_none() { - return; - } - - let event = WindowEvent::Destroyed; - send_window_event(window_id, &context.windows, &context.callback, event); - - let removed_window = { - let mut guard = context.windows.borrow_mut(); - guard.remove(&window_id) - }; - - if let Some(ref app_window) = removed_window { - let mut registry = context.scheme_handler_registry.lock().unwrap(); - for webview in &app_window.webviews { - let browser_id = *webview.browser_id.borrow(); - for scheme in webview.uri_scheme_protocols.keys() { - registry.remove(&(browser_id, scheme.clone())); - } - } - } - - drop(removed_window); - - let is_empty = context.windows.borrow().is_empty(); - if is_empty { - let (tx, rx) = channel(); - (context.callback.borrow())(RunEvent::ExitRequested { code: None, tx }); - - let recv = rx.try_recv(); - let should_prevent = matches!(recv, Ok(ExitRequestedEventAction::Prevent)); - - if !should_prevent { - (context.callback.borrow())(RunEvent::Exit); - } - } -} - -pub(crate) fn create_webview( - kind: WebviewKind, - context: &Context, - window_id: WindowId, - webview_id: u32, - pending: PendingWebview>, -) { - let PendingWebview { - label, - opener: _, - mut webview_attributes, - platform_specific_attributes, - uri_scheme_protocols, - ipc_handler: _, - navigation_handler, - new_window_handler, - document_title_changed_handler, - address_changed_handler, - url, - web_resource_request_handler: _, - mut on_page_load_handler, - download_handler, - } = pending; - - let address_changed_handler = address_changed_handler - .map(|h| Arc::new(move |url: &url::Url| h(url)) as Arc); - - let window = match context - .windows - .borrow() - .get(&window_id) - .and_then(|app_window| app_window.window()) - { - Some(w) => w, - None => { - eprintln!("Window {window_id:?} not found or is a browser window when creating webview",); - return; - } - }; - - let initialization_scripts = std::mem::take(&mut webview_attributes.initialization_scripts) - .into_iter() - .map(CefInitScript::new) - .collect::>(); - let initialization_scripts = Arc::new(initialization_scripts); - - let on_page_load_handler = on_page_load_handler.take().map(Arc::from); - let document_title_changed_handler = document_title_changed_handler.map(Arc::from); - let navigation_handler = navigation_handler.map(Arc::from); - let new_window_handler = new_window_handler.map(Arc::from); - - let devtools_enabled = (cfg!(debug_assertions) || cfg!(feature = "devtools")) - && webview_attributes.devtools.unwrap_or(true); - - let custom_protocol_scheme = if webview_attributes.use_https_scheme { - "https" - } else { - "http" - }; - - let custom_schemes = uri_scheme_protocols.keys().cloned().collect::>(); - let custom_scheme_domain_names: Vec = custom_schemes - .iter() - .map(|scheme| format!("{scheme}.localhost")) - .collect(); - - let initial_url = url.clone(); - let url = CefString::from(url.as_str()); - - let mut client = BrowserClient::new( - WindowKind::Tauri, - window_id, - initialization_scripts.clone(), - on_page_load_handler, - document_title_changed_handler, - navigation_handler, - address_changed_handler, - new_window_handler, - download_handler, - devtools_enabled, - custom_scheme_domain_names.clone(), - custom_protocol_scheme.to_string(), - context.clone(), - Some(initial_url.clone()), - ); - - let uri_scheme_protocols: HashMap>> = - uri_scheme_protocols - .into_iter() - .map(|(k, v)| (k, Arc::new(v))) - .collect(); - - let mut request_context = request_context_from_webview_attributes( - context, - &webview_attributes, - &custom_schemes, - custom_protocol_scheme, - &initialization_scripts, - ); - let window_theme = context - .windows - .borrow() - .get(&window_id) - .and_then(|w| w.attributes.borrow().theme); - apply_request_context_theme_scheme(request_context.as_ref(), window_theme); - - let browser_settings = browser_settings_from_webview_attributes(&webview_attributes); - - let bounds = webview_attributes.bounds.map(|bounds| { - let device_scale_factor = window - .display() - .map(|d| d.device_scale_factor() as f64) - .unwrap_or(1.0); - - rect_to_cef(bounds, device_scale_factor) - }); - - let window_handle = window.window_handle(); - - let runtime_style = platform_specific_attributes - .iter() - .map(|attr| match attr { - WebviewAtribute::RuntimeStyle { style } => *style, - }) - .next() - .unwrap_or(if matches!(kind, WebviewKind::WindowChild) { - CefRuntimeStyle::Alloy - } else { - CefRuntimeStyle::Chrome - }); - - let cef_runtime_style: RuntimeStyle = match runtime_style { - CefRuntimeStyle::Alloy => cef_runtime_style_t::CEF_RUNTIME_STYLE_ALLOY.into(), - CefRuntimeStyle::Chrome => cef_runtime_style_t::CEF_RUNTIME_STYLE_CHROME.into(), - }; - - if kind == WebviewKind::WindowChild { - #[cfg(target_os = "macos")] - let window_handle = ensure_valid_content_view(window_handle); - - let mut window_info = cef::WindowInfo::default().set_as_child( - window_handle, - bounds.as_ref().unwrap_or(&cef::Rect::default()), - ); - window_info.runtime_style = cef_runtime_style; - - let Some(browser_host) = browser_host_create_browser_sync( - Some(&window_info), - Some(&mut client), - Some(&url), - Some(&browser_settings), - Option::<&mut DictionaryValue>::None, - request_context.as_mut(), - ) else { - eprintln!("Failed to create browser"); - return; - }; - - // On Windows, set the browser window to be topmost to esnure correct z-order - #[cfg(windows)] - set_browser_on_top(&browser_host); - - let devtools_protocol_handlers = Arc::new(Mutex::new(Vec::< - Arc, - >::new())); - let devtools_observer_registration = Arc::new(Mutex::new(None)); - - let browser = CefWebview::Browser(browser_host); - - browser.set_bounds(bounds.as_ref()); - - // On Linux, explicitly set parent after creation as set_as_child may not work correctly - #[cfg(target_os = "linux")] - { - // Try to set parent - if window handle isn't available yet, this will be a no-op - // but the browser should become visible once the handle is available - browser.set_parent(&window); - // Ensure browser is visible after setting parent - browser.set_visible(1); - // Set bounds again after reparenting to ensure correct size - browser.set_bounds(bounds.as_ref()); - } - - let initial_bounds_ratio = if webview_attributes.auto_resize { - Some(webview_bounds_ratio(&window, bounds.clone(), &browser)) - } else { - None - }; - - let browser_id_val = browser.browser_id(); - { - let mut registry = context.scheme_handler_registry.lock().unwrap(); - for (scheme, handler) in &uri_scheme_protocols { - registry.insert( - (browser_id_val, scheme.clone()), - ( - label.clone(), - handler.clone(), - initialization_scripts.clone(), - ), - ); - } - } - - context - .windows - .borrow_mut() - .get_mut(&window_id) - .unwrap() - .webviews - .push(AppWebview { - label, - webview_id, - browser_id: Arc::new(RefCell::new(browser_id_val)), - bounds: Arc::new(Mutex::new(initial_bounds_ratio)), - inner: browser, - devtools_enabled, - uri_scheme_protocols: Arc::new(uri_scheme_protocols), - initialization_scripts, - devtools_protocol_handlers, - devtools_observer_registration, - webview_attributes: Arc::new(RefCell::new(webview_attributes)), - }); - } else { - let browser_id = Arc::new(RefCell::new(0)); - let uri_scheme_protocols = Arc::new(uri_scheme_protocols); - let devtools_protocol_handlers = Arc::new(Mutex::new(Vec::< - Arc, - >::new())); - let devtools_observer_registration = Arc::new(Mutex::new(None)); - let webview_attributes = Arc::new(RefCell::new(webview_attributes)); - - #[allow(clippy::unnecessary_find_map)] - let mut browser_view_delegate = BrowserViewDelegateImpl::new( - browser_id.clone(), - runtime_style, - context.scheme_handler_registry.clone(), - label.clone(), - uri_scheme_protocols.clone(), - initialization_scripts.clone(), - devtools_protocol_handlers.clone(), - devtools_observer_registration.clone(), - webview_attributes.clone(), - ); - - let browser_view = browser_view_create( - Some(&mut client), - Some(&url), - Some(&browser_settings), - Option::<&mut DictionaryValue>::None, - request_context.as_mut(), - Some(&mut browser_view_delegate), - ) - .expect("Failed to create browser view"); - - let browser_webview = CefWebview::BrowserView(browser_view.clone()); - - window.add_child_view(Some(&mut View::from(&browser_view))); - - context - .windows - .borrow_mut() - .get_mut(&window_id) - .unwrap() - .webviews - .push(AppWebview { - inner: browser_webview, - label, - webview_id, - browser_id, - bounds: Arc::new(Mutex::new(None)), - devtools_enabled, - uri_scheme_protocols, - initialization_scripts, - devtools_protocol_handlers, - devtools_observer_registration, - webview_attributes, - }); - } -} - -#[cfg(windows)] -fn set_browser_on_top(browser: &cef::Browser) { - use windows::Win32::Foundation::HWND; - use windows::Win32::UI::WindowsAndMessaging::*; - - let Some(host) = browser.host() else { - return; - }; - - let hwnd = HWND(host.window_handle().0 as _); - - let _ = unsafe { - SetWindowPos( - hwnd, - Some(HWND_TOP), - 0, - 0, - 0, - 0, - SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE, - ) - }; -} - -// there is some race condition on CEF that causes the app loading to fail -// when there is a network service crash -// "[85296:47750637:0127/131203.017395:ERROR:content/browser/network_service_instance_impl.cc:610] Network service crashed or was terminated, restarting service." -// we check the app URL for a while until it actually loads the initial URL -fn check_and_reload_if_blank(browser: cef::Browser, initial_url: String) { - if initial_url == "about:blank" { - return; - } - - std::thread::spawn(move || { - std::thread::sleep(std::time::Duration::from_secs(1)); - - let start_time = std::time::Instant::now(); - let timeout = std::time::Duration::from_secs(5); - let check_interval = std::time::Duration::from_millis(100); - - while start_time.elapsed() < timeout { - if let Some(frame) = browser.main_frame() { - let url = frame.url(); - let current_url = cef::CefString::from(&url).to_string(); - if current_url.is_empty() || current_url == "about:blank" { - frame.load_url(Some(&cef::CefString::from(initial_url.as_str()))); - // Continue checking in case it loads about:blank again - } else { - // URL has changed to something else (not about:blank), we can stop checking - return; - } - } - std::thread::sleep(check_interval); - } - }); -} - -fn webview_bounds_ratio( - window: &cef::Window, - webview_bounds: Option, - browser: &CefWebview, -) -> crate::WebviewBounds { - #[cfg(not(windows))] - let window_size = { - let window_bounds = window.bounds(); - LogicalSize::new(window_bounds.width as u32, window_bounds.height as u32) - }; - - // On Windows, CEF's window bounds is the outer size not the inner size. - #[cfg(windows)] - let window_size = crate::utils::windows::inner_size(window.window_handle()); - - let ob = webview_bounds.unwrap_or_else(|| browser.bounds()); - - crate::WebviewBounds { - x_rate: ob.x as f32 / window_size.width as f32, - y_rate: ob.y as f32 / window_size.height as f32, - width_rate: ob.width as f32 / window_size.width as f32, - height_rate: ob.height as f32 / window_size.height as f32, - } -} - -fn browser_settings_from_webview_attributes( - webview_attributes: &WebviewAttributes, -) -> BrowserSettings { - BrowserSettings { - javascript: State::from(if webview_attributes.javascript_disabled { - sys::cef_state_t::STATE_DISABLED - } else { - sys::cef_state_t::STATE_ENABLED - }), - javascript_access_clipboard: State::from(if webview_attributes.clipboard { - sys::cef_state_t::STATE_ENABLED - } else { - sys::cef_state_t::STATE_DISABLED - }), - background_color: webview_attributes - .background_color - .map(color_to_cef_argb) - .unwrap_or(0), - ..Default::default() - } -} - -fn request_context_from_webview_attributes( - context: &Context, - webview_attributes: &WebviewAttributes, - custom_schemes: &[String], - custom_protocol_scheme: &str, - _initialization_scripts: &[CefInitScript], -) -> Option { - let global_context = - request_context_get_global_context().expect("Failed to get global request context"); - - let cache_path: CefStringUtf16 = if webview_attributes.incognito { - CefStringUtf16::from("") - } else if let Some(_data_directory) = &webview_attributes.data_directory { - // TODO: setting a custom data directory must be a child of the root data directory, but it returns None on browser_view_create - eprintln!("data directory is not yet implemented"); - (&global_context.cache_path()).into() - // CefStringUtf16::from(data_directory.to_string_lossy().as_ref()) - } else { - (&global_context.cache_path()).into() - }; - - let request_context_settings = RequestContextSettings { - cache_path, - ..Default::default() - }; - - let request_context = request_context_create_context( - Some(&request_context_settings), - Option::<&mut RequestContextHandler>::None, - ); - if let Some(request_context) = &request_context { - for custom_scheme in custom_schemes { - request_context.register_scheme_handler_factory( - Some(&custom_protocol_scheme.into()), - Some(&format!("{custom_scheme}.localhost").as_str().into()), - Some(&mut request_handler::UriSchemeHandlerFactory::new( - context.scheme_handler_registry.clone(), - custom_scheme.clone(), - )), - ); - } - } - - request_context -} - -#[cfg(target_os = "macos")] -fn apply_titlebar_style(window: &cef::Window, style: TitleBarStyle, hidden_title: bool) { - use objc2::rc::Retained; - use objc2_app_kit::NSWindowTitleVisibility; - use objc2_app_kit::{NSView, NSWindowStyleMask}; - - let content_view = unsafe { Retained::::retain(window.window_handle() as _) }; - let Some(content_view) = content_view else { - return; - }; - - let Some(ns_window) = content_view.window() else { - return; - }; - - let mut mask = ns_window.styleMask(); - - match style { - TitleBarStyle::Visible => { - mask &= !NSWindowStyleMask::FullSizeContentView; - ns_window.setTitlebarAppearsTransparent(false); - ns_window.setStyleMask(mask); - } - TitleBarStyle::Transparent => { - ns_window.setTitlebarAppearsTransparent(true); - mask &= !NSWindowStyleMask::FullSizeContentView; - ns_window.setStyleMask(mask); - } - TitleBarStyle::Overlay => { - ns_window.setTitlebarAppearsTransparent(true); - mask |= NSWindowStyleMask::FullSizeContentView; - ns_window.setStyleMask(mask); - } - unknown => { - eprintln!("unknown title bar style applied: {unknown}"); - } - } - - if hidden_title { - ns_window.setTitleVisibility(NSWindowTitleVisibility::Hidden); - } -} - -/// On macOS, if the window content view is CEF's default `BridgedContentView`, -/// and does not have the expected subviews, replace it with a generic `NSView` -/// to avoid interactivity issues. -/// -/// Returns the new content view pointer, or the original window handle if no replacement was made. -/// -/// Subsequent calls to this function are no-ops, since the content view has already -/// been replaced and is no longer a BridgedContentView. -/// -/// SAFETY: Only call this function for Windows that are intended to host multiple webviews. -#[cfg(target_os = "macos")] -pub(crate) fn ensure_valid_content_view( - window_handle: *mut std::ffi::c_void, -) -> *mut std::ffi::c_void { - use objc2::rc::Retained; - use objc2::{MainThreadMarker, MainThreadOnly}; - use objc2_app_kit::NSView; - - let nsview = unsafe { Retained::::retain(window_handle as _) }; - let nsview = nsview.expect("NSView is null"); - - let class = nsview.class().name().to_string_lossy(); - let subviews = unsafe { nsview.subviews() }; - - // Filter subviews to only those that are expected in a valid CEF content view, - // which can only happen if a WebviewKind::WindowContent webview - // has been created in it using CEF's window.add_child_view API. - fn is_cef_view(subview: &Retained) -> bool { - let class = subview.class().name().to_string_lossy(); - class == "ViewsCompositorSuperview" || class == "WebContentsViewCocoa" - } - - // If it's a BridgedContentView without the expected subviews, - // replace it with a generic NSView to avoid interactivity issues. - if class == "BridgedContentView" && subviews.iter().filter(is_cef_view).count() != 2 { - let mtm = MainThreadMarker::new().expect("Not on main thread"); - - // Create a new generic NSView - let generic_nsview = NSView::alloc(mtm); - let generic_nsview = unsafe { NSView::init(generic_nsview) }; - - // Re-add subviews to the new generic NSView (excluding CEF's views) - for subview in subviews.iter().filter(|v| !is_cef_view(v)) { - unsafe { subview.removeFromSuperview() }; - unsafe { generic_nsview.addSubview(&subview) }; - } - - // Set the new generic NSView as the content view of the window - let nswindow = nsview.window().expect("NSWindow is null"); - nswindow.setContentView(Some(&generic_nsview)); - - // Return the new content view pointer - return Retained::into_raw(generic_nsview) as *mut std::ffi::c_void; - } - - // No replacement needed; return the original handle - window_handle -} - -#[cfg(target_os = "macos")] -fn apply_traffic_light_position(window: *mut std::ffi::c_void, position: &Position) { - use objc2::msg_send; - use objc2::rc::Retained; - use objc2_app_kit::{NSView, NSWindowButton}; - - let nsview = unsafe { Retained::::retain(window as _) }; - let Some(nsview) = nsview else { - return; - }; - - let Some(nswindow) = nsview.window() else { - return; - }; - - let Some(close) = nswindow.standardWindowButton(NSWindowButton::CloseButton) else { - return; - }; - let Some(miniaturize) = nswindow.standardWindowButton(NSWindowButton::MiniaturizeButton) else { - return; - }; - let Some(zoom) = nswindow.standardWindowButton(NSWindowButton::ZoomButton) else { - return; - }; - - let pos = position.to_logical::(nswindow.backingScaleFactor()); - let (x, y) = (pos.x, pos.y); - - let title_bar_container_view = unsafe { close.superview().unwrap().superview().unwrap() }; - - let close_rect = NSView::frame(&close); - let title_bar_frame_height = close_rect.size.height + y; - let mut title_bar_rect = NSView::frame(&title_bar_container_view); - title_bar_rect.size.height = title_bar_frame_height; - title_bar_rect.origin.y = nswindow.frame().size.height - title_bar_frame_height; - let _: () = unsafe { msg_send![&title_bar_container_view, setFrame: title_bar_rect] }; - - let window_buttons = vec![close, miniaturize.clone(), zoom]; - let space_between = NSView::frame(&miniaturize).origin.x - close_rect.origin.x; - - for (i, button) in window_buttons.into_iter().enumerate() { - let mut rect = NSView::frame(&button); - rect.origin.x = x + (i as f64 * space_between); - unsafe { button.setFrameOrigin(rect.origin) }; - } -} - -#[cfg(target_os = "macos")] -pub fn set_application_visibility(visible: bool) { - use objc2::MainThreadMarker; - use objc2_app_kit::NSApp; - - let mtm = MainThreadMarker::new().expect("not on main thread"); - let app = NSApp(mtm); - - if visible { - unsafe { app.unhide(None) }; - } else { - app.hide(None); - } -} diff --git a/crates/tauri-runtime-cef/src/cef_impl/client/context_menu.rs b/crates/tauri-runtime-cef/src/cef_impl/client/context_menu.rs new file mode 100644 index 000000000000..01286967879f --- /dev/null +++ b/crates/tauri-runtime-cef/src/cef_impl/client/context_menu.rs @@ -0,0 +1,27 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use cef::*; + +wrap_context_menu_handler! { + pub struct TauriCefContextMenuHandler { + devtools_enabled: bool, + } + + impl ContextMenuHandler { + fn on_before_context_menu( + &self, + _browser: Option<&mut Browser>, + _frame: Option<&mut Frame>, + _params: Option<&mut ContextMenuParams>, + model: Option<&mut MenuModel>, + ) { + if !self.devtools_enabled + && let Some(model) = model + { + model.remove_at(model.count() - 1); + } + } + } +} diff --git a/crates/tauri-runtime-cef/src/cef_impl/client/display.rs b/crates/tauri-runtime-cef/src/cef_impl/client/display.rs new file mode 100644 index 000000000000..d80eecf44914 --- /dev/null +++ b/crates/tauri-runtime-cef/src/cef_impl/client/display.rs @@ -0,0 +1,62 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::sync::Arc; + +use cef::*; + +use crate::webview::INITIAL_LOAD_URL; + +wrap_display_handler! { + pub struct TauriCefDisplayHandler { + document_title_changed_handler: Option>, + address_changed_handler: Option>, + } + + impl DisplayHandler { + fn on_title_change( + &self, + _browser: Option<&mut Browser>, + title: Option<&CefString>, + ) { + let Some(handler) = &self.document_title_changed_handler else { + return; + }; + let Some(title) = title else { + return; + }; + + handler(title.to_string()); + } + + fn on_address_change( + &self, + _browser: Option<&mut Browser>, + frame: Option<&mut Frame>, + url: Option<&CefString>, + ) { + // Only fire for main frame URL changes (matches on_before_browse behavior). + if let Some(frame) = frame + && frame.is_main() == 0 + { + return; + } + let Some(handler) = &self.address_changed_handler else { + return; + }; + let Some(url) = url else { + return; + }; + let url = url.to_string(); + + if url == INITIAL_LOAD_URL { + return; + } + + if let Ok(url) = url::Url::parse(&url) { + handler(&url); + } + } + } +} diff --git a/crates/tauri-runtime-cef/src/cef_impl/client/download.rs b/crates/tauri-runtime-cef/src/cef_impl/client/download.rs new file mode 100644 index 000000000000..b6f948953337 --- /dev/null +++ b/crates/tauri-runtime-cef/src/cef_impl/client/download.rs @@ -0,0 +1,116 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::sync::Arc; + +use cef::*; + +wrap_download_handler! { + pub struct TauriCefDownloadHandler { + download_handler: Arc, + } + + impl DownloadHandler { + fn can_download( + &self, + _browser: Option<&mut Browser>, + _url: Option<&CefStringUtf16>, + _request_method: Option<&CefStringUtf16>, + ) -> ::std::os::raw::c_int { + // on_before_download is the one that actually validates the download. + // so we return 1 to allow the download here + 1 + } + + fn on_before_download( + &self, + _browser: Option<&mut Browser>, + download_item: Option<&mut DownloadItem>, + suggested_name: Option<&CefStringUtf16>, + callback: Option<&mut BeforeDownloadCallback>, + ) -> ::std::os::raw::c_int { + let Some(download_item) = download_item else { + return 0; + }; + let Some(callback) = callback else { + return 0; + }; + + let url_str = CefString::from(&download_item.url()).to_string(); + let Ok(url) = url::Url::parse(&url_str) else { + return 0; + }; + + let suggested_path = suggested_name + .map(|s| s.to_string()) + .map(std::path::PathBuf::from) + .unwrap_or_default(); + + let mut destination = suggested_path.clone(); + + // Call handler with Requested event. + let should_allow = + (self.download_handler)(tauri_runtime::webview::DownloadEvent::Requested { + url: url.clone(), + destination: &mut destination, + }); + + if should_allow { + // Set the download path. + let destination_cef = CefStringUtf16::from(destination.to_string_lossy().as_ref()); + + // If the user callback did not modify the destination, show the dialog. + let show_dialog = destination == suggested_path; + callback.cont(Some(&destination_cef), show_dialog as ::std::os::raw::c_int); + } + + 1 + } + + fn on_download_updated( + &self, + _browser: Option<&mut Browser>, + download_item: Option<&mut DownloadItem>, + _callback: Option<&mut DownloadItemCallback>, + ) { + let Some(download_item) = download_item else { + return; + }; + + // Get download URL. + let url_str = CefString::from(&download_item.url()).to_string(); + let Ok(url) = url::Url::parse(&url_str) else { + return; + }; + + // Check download state - CEF returns i32 where 0 is false, non-zero is true. + let is_complete = download_item.is_complete() != 0; + let is_canceled = download_item.is_canceled() != 0; + let success = is_complete && !is_canceled; + + // Get full path if available - full_path() returns CefStringUserfreeUtf16. + let full_path = if is_complete || is_canceled { + let path_cef = download_item.full_path(); + let path_str = CefString::from(&path_cef).to_string(); + if !path_str.is_empty() { + Some(std::path::PathBuf::from(path_str)) + } else { + None + } + } else { + None + }; + + // Only call handler when download is finished (complete or canceled). + if is_complete || is_canceled { + // Call handler with Finished event. + (self.download_handler)(tauri_runtime::webview::DownloadEvent::Finished { + url, + path: full_path, + success, + }); + } + } + } +} diff --git a/crates/tauri-runtime-cef/src/cef_impl/client/drag.rs b/crates/tauri-runtime-cef/src/cef_impl/client/drag.rs new file mode 100644 index 000000000000..fd493a2e663a --- /dev/null +++ b/crates/tauri-runtime-cef/src/cef_impl/client/drag.rs @@ -0,0 +1,255 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{ + path::PathBuf, + sync::{Arc, Mutex}, +}; + +use cef::*; +use tauri_runtime::{ + UserEvent, + dpi::PhysicalPosition, + webview::InitializationScript, + window::{DragDropEvent, WindowId}, +}; +use url::Url; + +use crate::runtime::{Message, RuntimeContext}; + +const DRAG_DROP_BRIDGE_PATH: &str = "/__tauri_cef_drag_drop__"; + +const DRAG_DROP_INIT_SCRIPT: &str = r#" +(() => { + if (window.__TAURI_CEF_DRAG_DROP__) { + return; + } + + Object.defineProperty(window, "__TAURI_CEF_DRAG_DROP__", { + value: true, + configurable: false, + }); + + const PATH = "/__tauri_cef_drag_drop__"; + let entered = false; + + const position = (event) => ({ + x: event.clientX * window.devicePixelRatio, + y: event.clientY * window.devicePixelRatio, + }); + + const send = (type, event) => { + const pos = position(event); + const url = new URL(PATH, window.location.href); + url.searchParams.set("payload", JSON.stringify({ type, x: pos.x, y: pos.y })); + fetch(url.href, { + method: "GET", + cache: "no-store", + credentials: "omit", + }).catch(() => {}); + }; + + const listen = (eventName, handler) => { + window.addEventListener(eventName, handler, { capture: true }); + }; + + listen("dragenter", (event) => { + if (!entered) { + entered = true; + send("enter", event); + } + }); + + listen("dragover", (event) => { + if (!entered) { + entered = true; + send("enter", event); + } + send("over", event); + }); + + listen("drop", (event) => { + if (!entered) { + send("enter", event); + } + entered = false; + send("drop", event); + }); + + listen("dragleave", (event) => { + const x = event.clientX; + const y = event.clientY; + if (entered && (x <= 0 || y <= 0 || x >= window.innerWidth || y >= window.innerHeight)) { + entered = false; + send("leave", event); + } + }); +})(); +"#; + +pub(crate) fn drag_drop_initialization_script() -> InitializationScript { + InitializationScript { + script: DRAG_DROP_INIT_SCRIPT.to_string(), + for_main_frame_only: false, + } +} + +#[derive(Default)] +pub(crate) struct DragDropState { + pub(crate) paths: Option>, + pub(crate) native_entered: bool, + pub(crate) entered: bool, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +pub(crate) enum DragDropEventTarget { + Window, + Webview, +} + +#[derive(Clone, serde::Deserialize)] +pub(crate) struct DragDropScriptEvent { + #[serde(rename = "type")] + pub(crate) kind: String, + pub(crate) x: f64, + pub(crate) y: f64, +} + +fn collect_drag_data_paths(drag_data: &mut DragData) -> Vec { + let mut paths = CefStringList::new(); + if drag_data.file_paths(Some(&mut paths)) != 0 { + let paths = paths + .into_iter() + .filter(|path| !path.is_empty()) + .map(PathBuf::from) + .collect::>(); + + if !paths.is_empty() { + return paths; + } + } + + let file_name = CefStringUtf16::from(&drag_data.file_name()).to_string(); + if file_name.is_empty() { + Vec::new() + } else { + vec![PathBuf::from(file_name)] + } +} + +wrap_drag_handler! { + pub struct TauriCefDragHandler { + drag_drop_state: Arc>, + } + + impl DragHandler { + fn on_drag_enter( + &self, + _browser: Option<&mut Browser>, + drag_data: Option<&mut DragData>, + _mask: DragOperationsMask, + ) -> ::std::os::raw::c_int { + let mut state = self.drag_drop_state.lock().unwrap(); + state.entered = false; + state.paths = drag_data + .map(collect_drag_data_paths) + .filter(|paths| !paths.is_empty()); + state.native_entered = state.paths.is_some(); + + // Let Chromium continue with the drag operation so the injected script can + // report over/drop/leave with accurate viewport positions. + 0 + } + } +} + +pub(crate) fn event_from_script_event( + drag_drop_state: &Arc>, + script_event: DragDropScriptEvent, +) -> Option { + let position = PhysicalPosition::new(script_event.x, script_event.y); + let mut state = drag_drop_state.lock().unwrap(); + if !state.native_entered { + return None; + } + + match script_event.kind.as_str() { + "enter" => { + if state.entered { + return None; + } + + let paths = state.paths.clone()?; + state.entered = true; + Some(DragDropEvent::Enter { paths, position }) + } + "over" => state.entered.then_some(DragDropEvent::Over { position }), + "drop" => { + let paths = state.entered.then(|| state.paths.take()).flatten(); + state.entered = false; + state.native_entered = false; + paths.map(|paths| DragDropEvent::Drop { paths, position }) + } + "leave" => { + state.native_entered = false; + state.paths = None; + + if state.entered { + state.entered = false; + Some(DragDropEvent::Leave) + } else { + None + } + } + _ => None, + } +} + +wrap_resource_request_handler! { + pub(crate) struct WebDragDropResourceRequestHandler { + context: RuntimeContext, + window_id: WindowId, + webview_id: u32, + drag_drop_event_target: DragDropEventTarget, + drag_drop_handler_enabled: bool, + drag_drop_state: Arc>, + } + + impl ResourceRequestHandler { + fn on_before_resource_load( + &self, + _browser: Option<&mut Browser>, + _frame: Option<&mut Frame>, + request: Option<&mut Request>, + _callback: Option<&mut Callback>, + ) -> ReturnValue { + if self.drag_drop_handler_enabled + && let Some(request) = request + { + let url = CefString::from(&request.url()).to_string(); + if let Ok(url) = Url::parse(&url) + && url.path() == DRAG_DROP_BRIDGE_PATH + { + if let Some(payload) = url + .query_pairs() + .find_map(|(key, value)| (key == "payload").then(|| value.into_owned())) + && let Ok(event) = serde_json::from_str::(&payload) + { + let _ = self.context.send_message(Message::DragDropScriptEvent { + window_id: self.window_id, + webview_id: self.webview_id, + target: self.drag_drop_event_target, + drag_drop_state: self.drag_drop_state.clone(), + event, + }); + } + + return sys::cef_return_value_t::RV_CANCEL.into(); + } + } + + sys::cef_return_value_t::RV_CONTINUE.into() + } + } +} diff --git a/crates/tauri-runtime-cef/src/cef_impl/client/keyboard.rs b/crates/tauri-runtime-cef/src/cef_impl/client/keyboard.rs new file mode 100644 index 000000000000..8f4a3d141830 --- /dev/null +++ b/crates/tauri-runtime-cef/src/cef_impl/client/keyboard.rs @@ -0,0 +1,88 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use cef::*; + +#[cfg(target_os = "linux")] +type CefOsEvent<'a> = Option<&'a mut cef::sys::XEvent>; +#[cfg(target_os = "macos")] +type CefOsEvent<'a> = *mut u8; +#[cfg(windows)] +type CefOsEvent<'a> = Option<&'a mut cef::sys::MSG>; + +wrap_keyboard_handler! { + pub struct TauriCefKeyboardHandler { + devtools_enabled: bool, + } + + impl KeyboardHandler { + fn on_pre_key_event( + &self, + _browser: Option<&mut Browser>, + event: Option<&KeyEvent>, + _os_event: CefOsEvent<'_>, + _is_keyboard_shortcut: Option<&mut ::std::os::raw::c_int>, + ) -> ::std::os::raw::c_int { + // If devtools is disabled, block devtools keyboard shortcuts. + if !self.devtools_enabled { + let Some(event) = event else { + return 0; + }; + + // Check if this is a keydown event. + use cef::sys::cef_key_event_type_t; + let keydown_type: cef::KeyEventType = cef_key_event_type_t::KEYEVENT_RAWKEYDOWN.into(); + if event.type_ != keydown_type { + return 0; + } + + // Get modifier keys. + use cef::sys::cef_event_flags_t; + #[cfg(windows)] + let modifiers = event.modifiers as i32; + #[cfg(not(windows))] + let modifiers = event.modifiers; + + #[cfg(not(target_os = "macos"))] + let ctrl = (modifiers & (cef_event_flags_t::EVENTFLAG_CONTROL_DOWN.0)) != 0; + #[cfg(not(target_os = "macos"))] + let shift = (modifiers & (cef_event_flags_t::EVENTFLAG_SHIFT_DOWN.0)) != 0; + + let key_code = event.windows_key_code; + + // Block F12 (key code 123). + if key_code == 123 { + if let Some(is_keyboard_shortcut) = _is_keyboard_shortcut { + *is_keyboard_shortcut = 1; + } + return 1; + } + + // Block Ctrl+Shift+I (key code 73 = 'I') on Linux/Windows. + #[cfg(not(target_os = "macos"))] + if key_code == 73 && ctrl && shift { + if let Some(is_keyboard_shortcut) = _is_keyboard_shortcut { + *is_keyboard_shortcut = 1; + } + return 1; + } + + // Block Cmd+Opt+I on macOS. + #[cfg(target_os = "macos")] + { + let meta = (modifiers & cef_event_flags_t::EVENTFLAG_COMMAND_DOWN.0) != 0; + let alt = (modifiers & cef_event_flags_t::EVENTFLAG_ALT_DOWN.0) != 0; + if key_code == 73 && meta && alt { + if let Some(is_keyboard_shortcut) = _is_keyboard_shortcut { + *is_keyboard_shortcut = 1; + } + return 1; + } + } + } + + 0 + } + } +} diff --git a/crates/tauri-runtime-cef/src/cef_impl/client/life_span.rs b/crates/tauri-runtime-cef/src/cef_impl/client/life_span.rs new file mode 100644 index 000000000000..dfae5fa463ed --- /dev/null +++ b/crates/tauri-runtime-cef/src/cef_impl/client/life_span.rs @@ -0,0 +1,138 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::sync::{Arc, mpsc::Sender}; + +use cef::*; +use tauri_runtime::{ + UserEvent, + dpi::{LogicalPosition, LogicalSize}, + window::WindowId, +}; +use winit::event_loop::EventLoopProxy as WinitEventLoopProxy; + +use crate::runtime::{CefRuntime, Message, NewWindowOpener, RuntimeContext}; + +// There is some race condition on CEF that causes the app loading to fail +// when there is a network service crash: +// "[85296:47750637:0127/131203.017395:ERROR:content/browser/network_service_instance_impl.cc:610] Network service crashed or was terminated, restarting service." +// We check the app URL for a while until it actually loads the initial URL. +fn check_and_reload_if_blank(browser: cef::Browser, initial_url: String) { + if initial_url == "about:blank" { + return; + } + + std::thread::spawn(move || { + std::thread::sleep(std::time::Duration::from_secs(1)); + + let start_time = std::time::Instant::now(); + let timeout = std::time::Duration::from_secs(5); + let check_interval = std::time::Duration::from_millis(100); + + while start_time.elapsed() < timeout { + if let Some(frame) = browser.main_frame() { + let url = frame.url(); + let current_url = cef::CefString::from(&url).to_string(); + if current_url.is_empty() || current_url == "about:blank" { + frame.load_url(Some(&cef::CefString::from(initial_url.as_str()))); + // Continue checking in case it loads about:blank again. + } else { + return; + } + } + std::thread::sleep(check_interval); + } + }); +} + +wrap_life_span_handler! { + pub struct TauriCefChildLifeSpanHandler { + sender: Sender>, + proxy: WinitEventLoopProxy, + window_id: WindowId, + webview_id: u32, + context: RuntimeContext, + new_window_handler: Option>>>, + initial_url: Option, + } + + impl LifeSpanHandler { + fn on_after_created(&self, browser: Option<&mut Browser>) { + if let Some(browser) = browser + && let Some(initial_url) = &self.initial_url + { + check_and_reload_if_blank(browser.clone(), initial_url.clone()); + } + } + + fn on_before_popup( + &self, + _browser: Option<&mut Browser>, + _frame: Option<&mut Frame>, + _popup_id: std::os::raw::c_int, + target_url: Option<&CefString>, + _target_frame_name: Option<&CefString>, + _target_disposition: WindowOpenDisposition, + _user_gesture: std::os::raw::c_int, + popup_features: Option<&PopupFeatures>, + _window_info: Option<&mut WindowInfo>, + _client: Option<&mut Option>, + _settings: Option<&mut BrowserSettings>, + _extra_info: Option<&mut Option>, + _no_javascript_access: Option<&mut i32>, + ) -> std::os::raw::c_int { + let Some(handler) = &self.new_window_handler else { + return 0; + }; + + let Some(target_url) = target_url else { + return 1; + }; + + let url_str = target_url.to_string(); + let Ok(url) = url::Url::parse(&url_str) else { + return 1; + }; + + // window.open() features are CSS pixels, which map to Tauri's logical units. + let size = popup_features.and_then(|features| { + (features.width_set != 0 && features.height_set != 0) + .then(|| LogicalSize::new(features.width as f64, features.height as f64)) + }); + let position = popup_features.and_then(|features| { + (features.x_set != 0 && features.y_set != 0) + .then(|| LogicalPosition::new(features.x as f64, features.y as f64)) + }); + let features = + tauri_runtime::webview::NewWindowFeatures::new(size, position, NewWindowOpener {}); + + match handler(url, features) { + tauri_runtime::webview::NewWindowResponse::Allow => 0, + tauri_runtime::webview::NewWindowResponse::Create { window_id } => { + // CEF cannot transplant a popup's contents into an existing + // browser, so cancel the popup and navigate the designated + // window's first webview to the URL instead — the closest + // equivalent of wry hosting the popup in that window's webview. + // Note `window.opener` is not linked to the new document. + let _ = self.context.send_message(Message::NavigateFirstWebview { + window_id, + url: url_str, + }); + 1 + } + tauri_runtime::webview::NewWindowResponse::Deny => 1, + } + } + + fn on_before_close(&self, browser: Option<&mut Browser>) { + if browser.is_none() { + return; + } + let _ = self + .sender + .send(Message::BrowserClosed(self.window_id, self.webview_id)); + self.proxy.wake_up(); + } + } +} diff --git a/crates/tauri-runtime-cef/src/cef_impl/client/load.rs b/crates/tauri-runtime-cef/src/cef_impl/client/load.rs new file mode 100644 index 000000000000..33e53c270c1e --- /dev/null +++ b/crates/tauri-runtime-cef/src/cef_impl/client/load.rs @@ -0,0 +1,61 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::sync::Arc; + +use cef::*; + +wrap_load_handler! { + pub struct TauriCefLoadHandler { + on_page_load_handler: Option>, + } + + impl LoadHandler { + fn on_load_start( + &self, + _browser: Option<&mut Browser>, + frame: Option<&mut Frame>, + _transition_type: TransitionType, + ) { + let Some(handler) = &self.on_page_load_handler else { + return; + }; + let Some(frame) = frame else { + return; + }; + + if frame.is_main() == 0 { + return; + } + + let url = cef::CefString::from(&frame.url()).to_string(); + if let Ok(url) = url::Url::parse(&url) { + handler(url, tauri_runtime::webview::PageLoadEvent::Started); + } + } + + fn on_load_end( + &self, + _browser: Option<&mut Browser>, + frame: Option<&mut Frame>, + _http_status_code: ::std::os::raw::c_int, + ) { + let Some(handler) = &self.on_page_load_handler else { + return; + }; + let Some(frame) = frame else { + return; + }; + + if frame.is_main() == 0 { + return; + } + + let url = cef::CefString::from(&frame.url()).to_string(); + if let Ok(url) = url::Url::parse(&url) { + handler(url, tauri_runtime::webview::PageLoadEvent::Finished); + } + } + } +} diff --git a/crates/tauri-runtime-cef/src/cef_impl/client/mod.rs b/crates/tauri-runtime-cef/src/cef_impl/client/mod.rs new file mode 100644 index 000000000000..6ddfeb915024 --- /dev/null +++ b/crates/tauri-runtime-cef/src/cef_impl/client/mod.rs @@ -0,0 +1,159 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::sync::{Arc, Mutex, mpsc::Sender}; + +use cef::*; +use tauri_runtime::{UserEvent, window::WindowId}; +use winit::event_loop::EventLoopProxy as WinitEventLoopProxy; + +use crate::{ + cef_impl::{ipc, request_handler}, + runtime::{CefRuntime, Message, RuntimeContext}, +}; + +mod context_menu; +mod display; +mod download; +mod drag; +mod keyboard; +mod life_span; +mod load; +mod permission; +mod process; + +use context_menu::TauriCefContextMenuHandler; +use display::TauriCefDisplayHandler; +use download::TauriCefDownloadHandler; +use drag::TauriCefDragHandler; +pub(crate) use drag::{ + DragDropEventTarget, DragDropScriptEvent, DragDropState, WebDragDropResourceRequestHandler, + drag_drop_initialization_script, event_from_script_event, +}; +use keyboard::TauriCefKeyboardHandler; +use life_span::TauriCefChildLifeSpanHandler; +use load::TauriCefLoadHandler; +use permission::TauriCefPermissionHandler; +pub(crate) use process::TauriCefBrowserProcessHandler; + +pub(crate) struct TauriCefBrowserClientHandlers { + pub(crate) ipc_handler: Option>>, + pub(crate) on_page_load_handler: Option>, + pub(crate) document_title_changed_handler: + Option>, + pub(crate) navigation_handler: Option>, + pub(crate) address_changed_handler: Option>, + pub(crate) new_window_handler: + Option>>>, + pub(crate) download_handler: Option>, + pub(crate) web_content_process_terminate_handler: Option>, +} + +impl Clone for TauriCefBrowserClientHandlers { + fn clone(&self) -> Self { + Self { + ipc_handler: self.ipc_handler.clone(), + on_page_load_handler: self.on_page_load_handler.clone(), + document_title_changed_handler: self.document_title_changed_handler.clone(), + navigation_handler: self.navigation_handler.clone(), + address_changed_handler: self.address_changed_handler.clone(), + new_window_handler: self.new_window_handler.clone(), + download_handler: self.download_handler.clone(), + web_content_process_terminate_handler: self.web_content_process_terminate_handler.clone(), + } + } +} + +wrap_client! { + pub(crate) struct TauriCefBrowserClient { + pub(crate) context: RuntimeContext, + pub(crate) window_id: WindowId, + pub(crate) webview_id: u32, + pub(crate) label: String, + initial_url: Option, + devtools_enabled: bool, + drag_drop_event_target: DragDropEventTarget, + drag_drop_handler_enabled: bool, + drag_drop_state: Arc>, + pub(crate) handlers: TauriCefBrowserClientHandlers, + proxy: WinitEventLoopProxy, + sender: Sender>, + } + + impl Client { + fn drag_handler(&self) -> Option { + self + .drag_drop_handler_enabled + .then(|| TauriCefDragHandler::new(self.drag_drop_state.clone())) + } + + fn request_handler(&self) -> Option { + Some(request_handler::WebRequestHandler::new( + self.handlers.navigation_handler.clone(), + self.context.clone(), + self.window_id, + self.webview_id, + self.drag_drop_event_target, + self.drag_drop_handler_enabled, + self.drag_drop_state.clone(), + self.handlers.web_content_process_terminate_handler.clone(), + )) + } + + fn life_span_handler(&self) -> Option { + Some(TauriCefChildLifeSpanHandler::new( + self.sender.clone(), + self.proxy.clone(), + self.window_id, + self.webview_id, + self.context.clone(), + self.handlers.new_window_handler.clone(), + self.initial_url.clone(), + )) + } + + fn load_handler(&self) -> Option { + Some(TauriCefLoadHandler::new( + self.handlers.on_page_load_handler.clone(), + )) + } + + fn display_handler(&self) -> Option { + Some(TauriCefDisplayHandler::new( + self.handlers.document_title_changed_handler.clone(), + self.handlers.address_changed_handler.clone(), + )) + } + + fn download_handler(&self) -> Option { + self + .handlers + .download_handler + .clone() + .map(TauriCefDownloadHandler::new) + } + + fn context_menu_handler(&self) -> Option { + Some(TauriCefContextMenuHandler::new(self.devtools_enabled)) + } + + fn keyboard_handler(&self) -> Option { + Some(TauriCefKeyboardHandler::new(self.devtools_enabled)) + } + + fn permission_handler(&self) -> Option { + Some(TauriCefPermissionHandler::new()) + } + + fn on_process_message_received( + &self, + _browser: Option<&mut Browser>, + frame: Option<&mut Frame>, + source_process: ProcessId, + message: Option<&mut ProcessMessage>, + ) -> std::os::raw::c_int { + ipc::on_process_message_received(self, frame, source_process, message) + } + } +} diff --git a/crates/tauri-runtime-cef/src/cef_impl/client/permission.rs b/crates/tauri-runtime-cef/src/cef_impl/client/permission.rs new file mode 100644 index 000000000000..74b436d0af32 --- /dev/null +++ b/crates/tauri-runtime-cef/src/cef_impl/client/permission.rs @@ -0,0 +1,53 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use cef::*; + +wrap_permission_handler! { + pub struct TauriCefPermissionHandler {} + + impl PermissionHandler { + fn on_request_media_access_permission( + &self, + _browser: Option<&mut Browser>, + _frame: Option<&mut Frame>, + _requesting_origin: Option<&CefString>, + requested_permissions: u32, + callback: Option<&mut MediaAccessCallback>, + ) -> ::std::os::raw::c_int { + let Some(callback) = callback else { + return 0; + }; + // Allow microphone and camera when requested. + let allowed = requested_permissions + & (cef::sys::cef_media_access_permission_types_t::CEF_MEDIA_PERMISSION_DEVICE_AUDIO_CAPTURE + as u32 + | cef::sys::cef_media_access_permission_types_t::CEF_MEDIA_PERMISSION_DEVICE_VIDEO_CAPTURE + as u32); + if allowed != 0 { + callback.cont(requested_permissions); + return 1; + } + 0 + } + + fn on_show_permission_prompt( + &self, + _browser: Option<&mut Browser>, + _prompt_id: u64, + _requesting_origin: Option<&CefString>, + _requested_permissions: u32, + callback: Option<&mut PermissionPromptCallback>, + ) -> ::std::os::raw::c_int { + let Some(callback) = callback else { + return 0; + }; + // Allow permission prompt (e.g. microphone/camera). + callback.cont(PermissionRequestResult::from( + cef::sys::cef_permission_request_result_t::CEF_PERMISSION_RESULT_ACCEPT, + )); + 1 + } + } +} diff --git a/crates/tauri-runtime-cef/src/cef_impl/client/process.rs b/crates/tauri-runtime-cef/src/cef_impl/client/process.rs new file mode 100644 index 000000000000..23a6a7b58357 --- /dev/null +++ b/crates/tauri-runtime-cef/src/cef_impl/client/process.rs @@ -0,0 +1,57 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::sync::{ + Arc, + atomic::{AtomicBool, Ordering}, +}; + +use cef::*; +use tauri_runtime::UserEvent; + +use crate::runtime::{Message, RuntimeContext}; + +wrap_browser_process_handler! { + pub(crate) struct TauriCefBrowserProcessHandler { + context: RuntimeContext, + context_initialized: Arc, + deep_link_schemes: Vec, + } + + impl BrowserProcessHandler { + fn on_context_initialized(&self) { + self.context_initialized.store(true, Ordering::SeqCst); + self.context.proxy.wake_up(); + } + + fn on_schedule_message_pump_work(&self, delay_ms: i64) { + self.context.cef_pump.schedule_message_pump_work(delay_ms); + } + + fn on_already_running_app_relaunch( + &self, + command_line: Option<&mut CommandLine>, + _current_directory: Option<&CefString>, + ) -> std::os::raw::c_int { + let Some(command_line) = command_line else { + return 0; + }; + let mut list = CefStringList::new(); + command_line.arguments(Some(&mut list)); + let args: Vec = list.into_iter().collect(); + if let Some(first_arg) = args.first() + && let Ok(url) = url::Url::parse(first_arg) + { + let scheme = url.scheme().to_string(); + if self.deep_link_schemes.iter().any(|s| s == &scheme) { + let _ = self.context.sender.send(Message::Opened(vec![url])); + self.context.proxy.wake_up(); + return 1; + } + } + // TODO: add event + 1 + } + } +} diff --git a/crates/tauri-runtime-cef/src/cef_impl/cookie.rs b/crates/tauri-runtime-cef/src/cef_impl/cookie.rs index cf7ba74deeb3..79bfcdee311d 100644 --- a/crates/tauri-runtime-cef/src/cef_impl/cookie.rs +++ b/crates/tauri-runtime-cef/src/cef_impl/cookie.rs @@ -2,41 +2,79 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use cef::{rc::*, *}; use std::sync::{Arc, Mutex, mpsc::Sender}; +use cef::{rc::*, *}; +use tauri_runtime::{Cookie, Result}; +use url::Url; + +use crate::AppWebview; + +type CookieResultSender = Sender>>>; +type CollectedCookies = Arc>>>; + +fn cookie_from_cef(cookie: &cef::Cookie) -> Cookie<'static> { + let name = cookie.name.to_string(); + let value = cookie.value.to_string(); + let domain = cookie.domain.to_string(); + let path = cookie.path.to_string(); + + let mut builder = Cookie::build((name, value)); + if !domain.is_empty() { + builder = builder.domain(domain); + } + if !path.is_empty() { + builder = builder.path(path); + } + if cookie.secure == 1 { + builder = builder.secure(true); + } + if cookie.httponly == 1 { + builder = builder.http_only(true); + } + + builder.build().into_owned() +} + +fn cef_cookie_from_cookie(cookie: &Cookie<'_>) -> cef::Cookie { + let mut cef_cookie = cef::Cookie { + name: cef::CefString::from(cookie.name()), + value: cef::CefString::from(cookie.value()), + ..Default::default() + }; + + if let Some(domain) = cookie.domain() { + cef_cookie.domain = cef::CefString::from(domain); + } + if let Some(path) = cookie.path() { + cef_cookie.path = cef::CefString::from(path); + } + if cookie.secure().unwrap_or(false) { + cef_cookie.secure = 1; + } + if cookie.http_only().unwrap_or(false) { + cef_cookie.httponly = 1; + } + + cef_cookie +} + cef::wrap_cookie_visitor! { - pub struct CollectUrlCookiesVisitor { - pub tx: Sender>>>, - pub collected: Arc>>>, + struct CollectUrlCookiesVisitor { + tx: CookieResultSender, + collected: CollectedCookies, } impl CookieVisitor { fn visit( &self, cookie: Option<&cef::Cookie>, - count: ::std::os::raw::c_int, - total: ::std::os::raw::c_int, + _count: ::std::os::raw::c_int, + _total: ::std::os::raw::c_int, _delete_cookie: Option<&mut ::std::os::raw::c_int>, ) -> ::std::os::raw::c_int { - if let Some(c) = cookie { - let name = c.name.to_string(); - let value = c.value.to_string(); - let domain = c.domain.to_string(); - let path = c.path.to_string(); - - let mut builder = tauri_runtime::Cookie::build((name, value)); - if !domain.is_empty() { builder = builder.domain(domain); } - if !path.is_empty() { builder = builder.path(path); } - if c.secure == 1 { builder = builder.secure(true); } - if c.httponly == 1 { builder = builder.http_only(true); } - let ck = builder.build(); - - self.collected.lock().unwrap().push(ck.into_owned()); - } - - if (count + 1) >= total { - let _ = self.tx.send(Ok(self.collected.lock().unwrap().clone())); + if let Some(cookie) = cookie { + self.collected.lock().unwrap().push(cookie_from_cef(cookie)); } 1 } @@ -44,39 +82,88 @@ cef::wrap_cookie_visitor! { } cef::wrap_cookie_visitor! { - pub struct CollectAllCookiesVisitor { - pub tx: Sender>>>, - pub collected: Arc>>>, + struct CollectAllCookiesVisitor { + tx: CookieResultSender, + collected: CollectedCookies, } impl CookieVisitor { fn visit( &self, cookie: Option<&cef::Cookie>, - count: ::std::os::raw::c_int, - total: ::std::os::raw::c_int, + _count: ::std::os::raw::c_int, + _total: ::std::os::raw::c_int, _delete_cookie: Option<&mut ::std::os::raw::c_int>, ) -> ::std::os::raw::c_int { - if let Some(c) = cookie { - let name = c.name.to_string(); - let value = c.value.to_string(); - let domain = c.domain.to_string(); - let path = c.path.to_string(); - - let mut builder = tauri_runtime::Cookie::build((name, value)); - if !domain.is_empty() { builder = builder.domain(domain); } - if !path.is_empty() { builder = builder.path(path); } - if c.secure == 1 { builder = builder.secure(true); } - if c.httponly == 1 { builder = builder.http_only(true); } - let ck = builder.build(); - - self.collected.lock().unwrap().push(ck.into_owned()); - } - - if (count + 1) >= total { - let _ = self.tx.send(Ok(self.collected.lock().unwrap().clone())); + if let Some(cookie) = cookie { + self.collected.lock().unwrap().push(cookie_from_cef(cookie)); } 1 } } } + +// CEF never invokes `visit` when the cookie store is empty (and the visitor is +// also released without a final callback once visiting completes), so the +// result must be delivered when the visitor is dropped. The visitor's inner +// state is dropped exactly once, after CEF releases its last reference, which +// covers the empty, non-empty, and "visit failed to start" cases uniformly — +// otherwise a query against a URL/store with no matching cookies would never +// send and the dispatcher's blocking `recv` would hang forever. +impl Drop for CollectUrlCookiesVisitor { + fn drop(&mut self) { + let _ = self.tx.send(Ok(self.collected.lock().unwrap().clone())); + } +} + +impl Drop for CollectAllCookiesVisitor { + fn drop(&mut self) { + let _ = self.tx.send(Ok(self.collected.lock().unwrap().clone())); + } +} + +pub(crate) fn visit_url_cookies(manager: CookieManager, url: Url, tx: CookieResultSender) { + let collected = Arc::new(Mutex::new(Vec::new())); + let mut visitor = CollectUrlCookiesVisitor::new(tx, collected); + let url = cef::CefString::from(url.as_str()); + + // The result is sent from the visitor's `Drop`, including when this call fails + // to start the visit (the visitor is dropped here and sends the empty result). + manager.visit_url_cookies(Some(&url), 1, Some(&mut visitor)); +} + +pub(crate) fn visit_all_cookies(manager: CookieManager, tx: CookieResultSender) { + let collected = Arc::new(Mutex::new(Vec::new())); + let mut visitor = CollectAllCookiesVisitor::new(tx, collected); + + manager.visit_all_cookies(Some(&mut visitor)); +} + +pub(crate) fn set_cookie(manager: CookieManager, url: Option, cookie: Cookie<'static>) { + let cef_cookie = cef_cookie_from_cookie(&cookie); + let url = url.as_ref().map(|u| cef::CefString::from(u.as_str())); + manager.set_cookie( + url.as_ref(), + Some(&cef_cookie), + Option::<&mut cef::SetCookieCallback>::None, + ); +} + +pub(crate) fn delete_cookie(manager: CookieManager, url: Option, cookie: Cookie<'static>) { + let url = url.as_ref().map(|u| cef::CefString::from(u.as_str())); + let name = cef::CefString::from(cookie.name()); + manager.delete_cookies( + url.as_ref(), + Some(&name), + Option::<&mut cef::DeleteCookiesCallback>::None, + ); +} + +impl AppWebview { + pub fn cookie_manager(&self) -> Option { + self + .host + .request_context() + .and_then(|rc| rc.cookie_manager(None)) + } +} diff --git a/crates/tauri-runtime-cef/src/cef_impl/drag_window.rs b/crates/tauri-runtime-cef/src/cef_impl/drag_window.rs deleted file mode 100644 index a3945444c956..000000000000 --- a/crates/tauri-runtime-cef/src/cef_impl/drag_window.rs +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright 2019-2024 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -#[cfg(windows)] -pub mod windows { - use cef::*; - use windows::Win32::Foundation::*; - use windows::Win32::UI::WindowsAndMessaging::*; - use windows::core::{PCWSTR, w}; - - /// Same as [WNDPROC] but without the Option wrapper. - type WindowProc = unsafe extern "system" fn(HWND, u32, WPARAM, LPARAM) -> LRESULT; - - const ORIGINAL_WND_PROP: PCWSTR = w!("TAURI_CEF_ORIGINAL_WND_PROC"); - - /// Subclasses the given window to handle draggable regions - /// by replacing its window procedure with `root_window_proc` - /// and storing the original procedure as a property to be called later. - pub fn subclass_window_for_dragging(window: &mut cef::Window) { - let hwnd = window.window_handle(); - let hwnd = HWND(hwnd.0 as _); - subclass_window(hwnd, root_window_proc); - } - - /// Subclasses a window by replacing its window procedure with the given `proc` - /// and storing the original procedure as a property for later use. - fn subclass_window(hwnd: HWND, proc: WindowProc) { - // If already subclassed, return early - let orginial_wnd_proc = unsafe { GetPropW(hwnd, ORIGINAL_WND_PROP) }; - if !orginial_wnd_proc.is_invalid() { - return; - } - - // Reset last error - unsafe { SetLastError(ERROR_SUCCESS) }; - - // Set the new window procedure and get the orginal one - let original_wnd_proc = unsafe { SetWindowLongPtrW(hwnd, GWLP_WNDPROC, proc as isize) }; - if original_wnd_proc == 0 && unsafe { GetLastError() } != ERROR_SUCCESS { - return; - } - - unsafe { - // Store the original window proc as a property for later use - let _ = SetPropW( - hwnd, - ORIGINAL_WND_PROP, - Some(HANDLE(original_wnd_proc as _)), - ); - } - } - - /// The root window procedure to handle WM_NCLBUTTONDOWN - /// by calling DefWindowProcW directly to allow dragging - /// and forwarding other messages to the original CEF window procedure. - unsafe extern "system" fn root_window_proc( - hwnd: HWND, - msg: u32, - wparam: WPARAM, - lparam: LPARAM, - ) -> LRESULT { - if msg == WM_NCLBUTTONDOWN { - return unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }; - } - - // For other messages, call the original CEF window procedure - let original_wnd_proc = GetPropW(hwnd, ORIGINAL_WND_PROP); - let original_wnd_proc = std::mem::transmute::<_, WindowProc>(original_wnd_proc.0); - CallWindowProcW(Some(original_wnd_proc), hwnd, msg, wparam, lparam) - } -} diff --git a/crates/tauri-runtime-cef/src/cef_impl/ipc.rs b/crates/tauri-runtime-cef/src/cef_impl/ipc.rs new file mode 100644 index 000000000000..3a477a60d4be --- /dev/null +++ b/crates/tauri-runtime-cef/src/cef_impl/ipc.rs @@ -0,0 +1,168 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::sync::{Arc, Mutex}; + +use cef::*; +use tauri_runtime::{UserEvent, webview::DetachedWebview}; + +use crate::{ + cef_impl::client::TauriCefBrowserClient, runtime::CefRuntime, webview::CefWebviewDispatcher, +}; + +const IPC_MESSAGE_NAME: &str = "tauri:ipc"; +const IPC_POST_MESSAGE_FUNCTION: &str = "postMessage"; + +pub(crate) type IpcHandler = + dyn Fn(DetachedWebview>, http::Request) + Send; + +wrap_v8_handler! { + struct IpcPostMessageV8Handler; + + impl V8Handler { + fn execute( + &self, + name: Option<&CefString>, + _object: Option<&mut V8Value>, + arguments: Option<&[Option]>, + retval: Option<&mut Option>, + exception: Option<&mut CefString>, + ) -> std::os::raw::c_int { + let Some(name) = name else { + return 0; + }; + if name.to_string() != IPC_POST_MESSAGE_FUNCTION { + return 0; + } + + let Some(message) = arguments + .filter(|arguments| arguments.len() == 1) + .and_then(|arguments| arguments[0].as_ref()) + .filter(|argument| argument.is_string() != 0) + else { + if let Some(exception) = exception { + *exception = CefString::from("window.ipc.postMessage expects a string argument"); + } + return 1; + }; + + let Some(context) = v8_context_get_current_context() else { + return 1; + }; + let Some(frame) = context.frame() else { + return 1; + }; + + let body = CefString::from(&message.string_value()).to_string(); + let url = CefString::from(&frame.url()).to_string(); + let mut process_message = process_message_create(Some(&CefString::from(IPC_MESSAGE_NAME))); + if let Some(args) = process_message + .as_ref() + .and_then(ProcessMessage::argument_list) + { + args.set_string(0, Some(&CefString::from(url.as_str()))); + args.set_string(1, Some(&CefString::from(body.as_str()))); + frame.send_process_message(ProcessId::BROWSER, process_message.as_mut()); + } + + if let Some(retval) = retval { + *retval = v8_value_create_undefined(); + } + 1 + } + } +} + +fn install_ipc_post_message(context: Option<&mut V8Context>) { + let Some(window) = context.and_then(|context| context.global()) else { + return; + }; + let attributes = sys::cef_v8_propertyattribute_t( + [ + sys::cef_v8_propertyattribute_t::V8_PROPERTY_ATTRIBUTE_READONLY, + sys::cef_v8_propertyattribute_t::V8_PROPERTY_ATTRIBUTE_DONTENUM, + sys::cef_v8_propertyattribute_t::V8_PROPERTY_ATTRIBUTE_DONTDELETE, + ] + .into_iter() + .fold(0, |acc, attr| acc | attr.0), + ) + .into(); + let Some(mut ipc) = v8_value_create_object(None, None) else { + return; + }; + let mut handler = IpcPostMessageV8Handler::new(); + let post_message_name = CefString::from(IPC_POST_MESSAGE_FUNCTION); + let Some(mut post_message) = + v8_value_create_function(Some(&post_message_name), Some(&mut handler)) + else { + return; + }; + ipc.set_value_bykey( + Some(&post_message_name), + Some(&mut post_message), + attributes, + ); + window.set_value_bykey(Some(&CefString::from("ipc")), Some(&mut ipc), attributes); +} + +wrap_render_process_handler! { + pub struct TauriRenderProcessHandler; + + impl RenderProcessHandler { + fn on_context_created( + &self, + _browser: Option<&mut Browser>, + _frame: Option<&mut Frame>, + context: Option<&mut V8Context>, + ) { + install_ipc_post_message(context); + } + } +} + +pub(crate) fn on_process_message_received( + client: &TauriCefBrowserClient, + frame: Option<&mut Frame>, + source_process: ProcessId, + message: Option<&mut ProcessMessage>, +) -> std::os::raw::c_int { + if source_process != ProcessId::RENDERER { + return 0; + } + let Some(message) = message else { + return 0; + }; + if CefString::from(&message.name()).to_string() != IPC_MESSAGE_NAME { + return 0; + } + let Some(handler) = client.handlers.ipc_handler.as_ref() else { + return 1; + }; + let Some(args) = message.argument_list() else { + return 1; + }; + + let mut url = CefString::from(&args.string(0)).to_string(); + if url.is_empty() + && let Some(frame) = frame + { + url = CefString::from(&frame.url()).to_string(); + } + let body = CefString::from(&args.string(1)).to_string(); + + if let Ok(request) = http::Request::builder().uri(url).body(body) { + handler( + DetachedWebview { + label: client.label.clone(), + dispatcher: CefWebviewDispatcher { + window_id: Arc::new(Mutex::new(client.window_id)), + webview_id: client.webview_id, + context: client.context.clone(), + }, + }, + request, + ); + } + 1 +} diff --git a/crates/tauri-runtime-cef/src/cef_impl/mod.rs b/crates/tauri-runtime-cef/src/cef_impl/mod.rs new file mode 100644 index 000000000000..9c9e8612263f --- /dev/null +++ b/crates/tauri-runtime-cef/src/cef_impl/mod.rs @@ -0,0 +1,9 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +pub(crate) mod client; +pub(crate) mod cookie; +pub(crate) mod ipc; +pub(crate) mod request_context; +pub(crate) mod request_handler; diff --git a/crates/tauri-runtime-cef/src/cef_impl/request_context.rs b/crates/tauri-runtime-cef/src/cef_impl/request_context.rs new file mode 100644 index 000000000000..3a15c386c349 --- /dev/null +++ b/crates/tauri-runtime-cef/src/cef_impl/request_context.rs @@ -0,0 +1,373 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{ + fs::create_dir_all, + path::{Component, Path, PathBuf}, + sync::{ + Arc, Mutex, + atomic::{AtomicBool, Ordering}, + }, + time::Duration, +}; + +use base64::Engine; +use cef::*; +use sha2::{Digest, Sha256}; +use tauri_runtime::webview::WebviewAttributes; +use tauri_utils::Theme; + +use crate::cef_impl::request_handler; + +#[inline] +fn theme_to_color_variant(theme: Option) -> ColorVariant { + match theme { + Some(Theme::Dark) => ColorVariant::DARK, + Some(Theme::Light) => ColorVariant::LIGHT, + _ => ColorVariant::SYSTEM, + } +} + +pub(crate) fn apply_theme_scheme(request_context: Option<&RequestContext>, theme: Option) { + if let Some(request_context) = request_context { + request_context.set_chrome_color_scheme(theme_to_color_variant(theme), 0); + } +} + +/// Resolves a CEF-compatible cache path for a per-webview request context. +/// +/// CEF requires `RequestContextSettings.cache_path` to be either empty (which +/// puts the context in incognito mode) or an absolute path that is equal to, +/// or a child directory of, `Settings.root_cache_path` (which defaults to +/// `Settings.cache_path` when not set explicitly). Any value outside of that +/// root makes `request_context_create_context` (and downstream browser +/// creation) fail. +/// +/// To support an arbitrary [`WebviewAttributes::data_directory`] while +/// honoring this constraint we: +/// +/// * use the requested path directly when it already lives under the global +/// cache root, so callers that opt in to a path under the app cache get the +/// exact location they asked for; +/// * join relative paths without parent (`..`) components onto the root cache +/// path (typical short labels); and +/// * otherwise derive a stable direct child folder under `/` from +/// the requested path, preserving isolation between webviews. Distinct +/// `data_directory` values produce distinct profiles, and the same value +/// maps to the same on-disk profile across runs. +fn resolve_request_context_cache_path(global_cache_path: &Path, data_directory: &Path) -> PathBuf { + if data_directory.is_absolute() { + if data_directory.starts_with(global_cache_path) { + return data_directory.to_path_buf(); + } else { + log::warn!( + "data directory is not a child of the global cache path, we will derive a profile hash from it" + ); + } + } else if !data_directory + .components() + .any(|component| matches!(component, Component::ParentDir)) + { + return global_cache_path.join(data_directory); + } else { + log::warn!( + "data directory is a relative path with parent components, we will derive a profile hash from it" + ); + } + + let mut hasher = Sha256::new(); + hasher.update(data_directory.as_os_str().as_encoded_bytes()); + let hash = hasher.finalize(); + let suffix = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&hash[..16]); + let path = global_cache_path.join(format!("Profile-{suffix}")); + log::info!( + "derived profile hash from data directory: {suffix}, cache path: {}", + path.display() + ); + path +} + +/// Continuation invoked on the CEF UI thread once the request context's +/// underlying browser context has finished asynchronous initialization. +/// +/// Receives a fresh handle to the same [`RequestContext`] that was created in +/// [`request_context_from_webview_attributes`], so the continuation can pass +/// it to `browser_view_create` / `browser_host_create_browser_sync` knowing +/// that `VerifyBrowserContext()` will succeed. +pub(crate) type RequestContextInitContinuation = Box) + 'static>; + +/// Wraps a deferred-init continuation so that it always flips a shared +/// completion flag when it exits, regardless of how it exits (normal return, +/// early `return` on browser-create failure, or panic). +/// +/// Returns the completion flag plus the wrapped continuation. +pub(crate) fn deferred_init_continuation( + work: F, +) -> (Arc, RequestContextInitContinuation) +where + F: FnOnce(Option) + 'static, +{ + struct Guard(Arc); + impl Drop for Guard { + fn drop(&mut self) { + self.0.store(true, Ordering::SeqCst); + } + } + + let flag = Arc::new(AtomicBool::new(false)); + let guard = Guard(flag.clone()); + let wrapped: RequestContextInitContinuation = Box::new(move |request_context| { + let _guard = guard; + work(request_context); + }); + (flag, wrapped) +} + +/// Block the calling thread until `flag` is `true`. +/// +/// Browser creation goes through `RequestContextHandler::on_request_context_initialized`, +/// which CEF always dispatches via `CEF_POST_TASK(CEF_UIT, ...)`. Tauri runs +/// CEF with an external message pump (see `cef::do_message_loop_work` in the +/// runtime's main loop), so the only way for that posted task to actually +/// execute is for someone on the CEF UI thread to keep pumping the loop. +/// +/// Two cases: +/// +/// 1. We're on the CEF UI thread (typical: app setup, dispatched messages, or +/// inside a CEF callback like `LifeSpanHandler::on_after_created` / +/// `RequestHandler::on_open_url_from_tab`). Pump the message loop ourselves +/// so the `OnRequestContextInitialized` task can run. +/// +/// We must enable nestable tasks for the duration of the pump because we +/// may already be running inside another CEF task; without +/// `CefSetNestableTasksAllowed(true)` Chromium's `RunLoop::RunUntilIdle` +/// refuses to dispatch any task to the UI thread, the deferred init never +/// fires, and we'd spin here forever. +/// +/// 2. We're on some other thread (e.g. a tokio IPC handler that called the +/// Tauri API directly). The CEF UI thread is running its own pump and will +/// pick up our queued init task on its own; we just block here on a sleep +/// loop until the flag flips. We can't call `do_message_loop_work` from +/// this thread - it asserts on the init thread. +/// +/// Spinning here keeps `create_webview` synchronous from the caller's +/// perspective: the function does not return until the browser exists in +/// `state.windows`, so any subsequent dispatcher call (e.g. +/// `webview.open_devtools()`, `webview.on_dev_tools_protocol(...)`) can find +/// the webview. +pub(crate) fn wait_for_deferred_init(flag: &Arc) { + let on_ui_thread = cef::currently_on(cef::sys::cef_thread_id_t::TID_UI.into()) != 0; + + if on_ui_thread { + let _allow = AllowNestableTasks::enter(); + while !flag.load(Ordering::SeqCst) { + cef::do_message_loop_work(); + } + } else { + while !flag.load(Ordering::SeqCst) { + std::thread::sleep(Duration::from_millis(1)); + } + } +} + +/// RAII guard that scopes `CefSetNestableTasksAllowed(true)` for the current +/// CEF UI-thread call. +/// +/// CEF requires balanced enable/disable calls and explicitly forbids +/// reentrancy at the C++ level (`CHECK(allowed != has_value())`). The guard +/// uses a thread-local depth counter so only the outermost +/// [`wait_for_deferred_init`] on this thread toggles the flag, which makes +/// nesting (e.g. an `on_initialized` continuation that creates another +/// webview) safe. +struct AllowNestableTasks; + +impl AllowNestableTasks { + fn enter() -> Self { + NESTABLE_TASKS_DEPTH.with(|depth| { + let current = depth.get(); + if current == 0 { + cef::set_nestable_tasks_allowed(1); + } + depth.set(current + 1); + }); + Self + } +} + +impl Drop for AllowNestableTasks { + fn drop(&mut self) { + NESTABLE_TASKS_DEPTH.with(|depth| { + let current = depth.get(); + depth.set(current - 1); + if current == 1 { + cef::set_nestable_tasks_allowed(0); + } + }); + } +} + +thread_local! { + static NESTABLE_TASKS_DEPTH: std::cell::Cell = const { std::cell::Cell::new(0) }; +} + +wrap_request_context_handler! { + struct WebviewRequestContextHandler { + on_initialized: Arc>>, + } + + impl RequestContextHandler { + fn on_request_context_initialized(&self, request_context: Option<&mut RequestContext>) { + let Some(callback) = self.on_initialized.lock().unwrap().take() else { + return; + }; + let request_context = request_context.map(|rc| rc.clone()); + callback(request_context); + } + } +} + +/// Creates a per-webview [`RequestContext`], registers Tauri's custom URI +/// scheme handler factories on it, and arranges for `on_initialized` to fire +/// once the underlying Chromium `Profile` is fully created. +/// +/// CEF only synchronously initializes the request context when its `cache_path` +/// equals `Settings.root_cache_path` (it then reuses the global "Default" +/// profile via `GetPrimaryUserProfile()`) or when the cache_path is empty +/// (off-the-record profile). Any other path (notably the per-`data_directory` +/// case used by Tauri) takes `ChromeBrowserContext::InitializeAsync`'s +/// `CreateProfileAsync` branch which finishes asynchronously. Calling +/// `browser_host_create_browser_sync` synchronously after +/// `request_context_create_context` would then fail +/// `CefRequestContextImpl::VerifyBrowserContext()` and return a null browser. +/// +/// Routing browser creation through `on_initialized` keeps a single code path +/// for every cache_path layout: CEF always dispatches the callback through +/// `CEF_POST_TASK(CEF_UIT, ...)`, so even the synchronous-init cases are +/// handled by the same continuation. +/// +/// Scheme handler factories are registered here, synchronously after +/// `request_context_create_context` returns, and *before* the +/// `OnRequestContextInitialized` task that drives browser creation is +/// dispatched. `RegisterSchemeHandlerFactory` internally queues its work +/// behind the request context's initialization (`StoreOrTriggerInitCallback` +/// when the browser context is not yet initialized, or an immediate UI -> IO +/// hop otherwise), so by the time the browser finally issues its first +/// navigation against any of these schemes the factories have been wired up +/// on the IO thread. +/// Applies a fixed-server proxy to a request context via the Chromium `proxy` +/// preference. Must be called after the request context has initialized. +fn apply_proxy(request_context: &RequestContext, proxy_url: &url::Url) { + use cef::{ImplDictionaryValue, ImplValue}; + + let scheme = match proxy_url.scheme() { + "socks5" | "socks5h" => "socks5", + "socks4" | "socks4a" => "socks4", + "https" => "https", + _ => "http", + }; + let Some(host) = proxy_url.host_str() else { + log::warn!("ignoring proxy URL without a host: {proxy_url}"); + return; + }; + let server = match proxy_url.port_or_known_default() { + Some(port) => format!("{scheme}://{host}:{port}"), + None => format!("{scheme}://{host}"), + }; + + let pref_name = "proxy"; + if request_context.can_set_preference(Some(&pref_name.into())) != 1 { + log::warn!("the CEF request context does not allow setting the proxy preference"); + return; + } + + // Build `{ "mode": "fixed_servers", "server": "://:" }`. + let Some(dict) = cef::dictionary_value_create() else { + return; + }; + dict.set_string(Some(&"mode".into()), Some(&"fixed_servers".into())); + dict.set_string(Some(&"server".into()), Some(&server.as_str().into())); + + let Some(value) = cef::value_create() else { + return; + }; + let mut dict = dict; + value.set_dictionary(Some(&mut dict)); + + let mut value = value; + if request_context.set_preference(Some(&pref_name.into()), Some(&mut value), None) != 1 { + log::error!("failed to apply the proxy preference to the CEF request context"); + } +} + +pub(crate) fn request_context_from_webview_attributes<'a>( + global_cache_path: &Path, + webview_attributes: &WebviewAttributes, + custom_schemes: impl IntoIterator, + custom_protocol_scheme: &str, + scheme_registry: request_handler::SchemeRegistry, + on_initialized: RequestContextInitContinuation, +) -> Option { + let cache_path = if webview_attributes.incognito { + CefStringUtf16::from("") + } else if let Some(data_directory) = &webview_attributes.data_directory { + let cache_path = resolve_request_context_cache_path(global_cache_path, data_directory); + if let Err(error) = create_dir_all(&cache_path) { + log::error!( + "failed to create request context cache directory {}: {error}", + cache_path.display() + ); + } + CefStringUtf16::from(cache_path.to_string_lossy().as_ref()) + } else { + let global_context = + request_context_get_global_context().expect("Failed to get global request context"); + // global_cache_path does not work here - global_context.cache_path() returns the proper profile path. + (&global_context.cache_path()).into() + }; + + let settings = RequestContextSettings { + cache_path, + ..Default::default() + }; + + // Holds a strong reference to the `RequestContext` until the + // `on_request_context_initialized` callback fires. CEF keeps the underlying + // C++ `CefRequestContextImpl` alive during async profile creation through + // its own bound callbacks, but holding an explicit reference here guarantees + // we don't race with reference-count releases on shutdown paths. + let rc_holder: Arc>> = Arc::new(Mutex::new(None)); + let proxy_url = webview_attributes.proxy_url.clone(); + let wrapped_callback: RequestContextInitContinuation = Box::new({ + let rc_holder = rc_holder.clone(); + move |rc| { + // The proxy preference can only be set once the request context's + // underlying profile has finished initializing, which is exactly what + // this continuation signals. + if let (Some(rc), Some(proxy_url)) = (rc.as_ref(), proxy_url.as_ref()) { + apply_proxy(rc, proxy_url); + } + on_initialized(rc); + let _released = rc_holder.lock().unwrap().take(); + } + }); + + let mut handler = WebviewRequestContextHandler::new(Arc::new(Mutex::new(Some(wrapped_callback)))); + let request_context = request_context_create_context(Some(&settings), Some(&mut handler)); + *rc_holder.lock().unwrap() = request_context.clone(); + + if let Some(request_context) = request_context.as_ref() { + for scheme in custom_schemes { + request_context.register_scheme_handler_factory( + Some(&custom_protocol_scheme.into()), + Some(&format!("{scheme}.localhost").as_str().into()), + Some(&mut request_handler::UriSchemeHandlerFactory::new( + scheme_registry.clone(), + scheme.clone(), + )), + ); + } + } + + request_context +} diff --git a/crates/tauri-runtime-cef/src/cef_impl/request_handler.rs b/crates/tauri-runtime-cef/src/cef_impl/request_handler.rs index 107264473b61..f667f82031fe 100644 --- a/crates/tauri-runtime-cef/src/cef_impl/request_handler.rs +++ b/crates/tauri-runtime-cef/src/cef_impl/request_handler.rs @@ -5,7 +5,7 @@ use std::{ borrow::Cow, io::{Cursor, Read}, - sync::Arc, + sync::{Arc, Mutex}, }; use cef::{rc::*, *}; @@ -13,19 +13,39 @@ use dioxus_debug_cell::RefCell; use html5ever::{LocalName, interface::QualName, namespace_url, ns}; use http::{ HeaderMap, HeaderName, HeaderValue, - header::{CONTENT_SECURITY_POLICY, CONTENT_TYPE}, + header::{CONTENT_SECURITY_POLICY, CONTENT_TYPE, ORIGIN}, }; use kuchiki::NodeRef; -use tauri_runtime::webview::UriSchemeProtocolHandler; +use tauri_runtime::{ + UserEvent, + webview::{NavigationHandler, UriSchemeProtocolHandler}, + window::WindowId, +}; use tauri_utils::{ config::{Csp, CspDirectiveSources}, html::{parse as parse_html, serialize_node}, }; use url::Url; -use super::CefInitScript; +use crate::{ + cef_impl::client::{DragDropEventTarget, DragDropState, WebDragDropResourceRequestHandler}, + runtime::RuntimeContext, + webview::{CefInitScript, INITIAL_LOAD_URL}, +}; type HttpResponse = Arc>>>>>; +pub(crate) type SchemeRegistry = Arc< + Mutex< + std::collections::HashMap< + (i32, String), + ( + String, + Arc>, + Arc>, + ), + >, + >, +>; fn csp_inject_initialization_scripts_hashes( existing_csp: String, @@ -35,8 +55,6 @@ fn csp_inject_initialization_scripts_hashes( return existing_csp; } - // For custom schemes, include ALL script hashes (we inject all scripts into HTML) - // This matches the HTML injection behavior in inject_scripts_into_html_body let script_hashes: Vec = initialization_scripts .iter() .map(|s| s.hash.clone()) @@ -46,33 +64,26 @@ fn csp_inject_initialization_scripts_hashes( return existing_csp; } - // Parse CSP using tauri-utils let mut csp_map: std::collections::HashMap = Csp::Policy(existing_csp.to_string()).into(); - // Update or create script-src directive with script hashes let script_src = csp_map .entry("script-src".to_string()) .or_insert_with(|| CspDirectiveSources::List(vec!["'self'".to_string()])); - // Extend with script hashes script_src.extend(script_hashes); - // Convert back to CSP string Csp::DirectiveMap(csp_map).to_string() } -/// Helper function to inject initialization scripts into HTML body fn inject_scripts_into_html_body( body: &[u8], initialization_scripts: &[CefInitScript], ) -> Option> { - // Check if body is valid UTF-8 HTML let Ok(body_str) = std::str::from_utf8(body) else { return None; }; - // Parse HTML and inject scripts let document = parse_html(body_str.to_string()); let head = if let Ok(ref head_node) = document.select_first("head") { @@ -86,44 +97,40 @@ fn inject_scripts_into_html_body( head_node }; - // Inject initialization scripts (for custom schemes, inject all scripts) for init_script in initialization_scripts.iter().rev() { let script_el = NodeRef::new_element(QualName::new(None, ns!(html), "script".into()), None); - script_el.append(NodeRef::new_text(init_script.script.script.as_str())); + script_el.append(NodeRef::new_text(init_script.script.as_str())); head.prepend(script_el); } - // Serialize the modified HTML Some(serialize_node(&document)) } -wrap_resource_request_handler! { - pub struct WebResourceRequestHandler { - initialization_scripts: Arc>, +wrap_request_handler! { + pub struct WebRequestHandler { + navigation_handler: Option>, + context: RuntimeContext, + window_id: WindowId, + webview_id: u32, + drag_drop_event_target: DragDropEventTarget, + drag_drop_handler_enabled: bool, + drag_drop_state: Arc>, + web_content_process_terminate_handler: Option>, } - impl ResourceRequestHandler { - - - fn on_before_resource_load( + impl RequestHandler { + fn on_render_process_terminated( &self, _browser: Option<&mut Browser>, - _frame: Option<&mut Frame>, - _request: Option<&mut Request>, - _callback: Option<&mut Callback>, - ) -> ReturnValue { - sys::cef_return_value_t::RV_CONTINUE.into() + _status: TerminationStatus, + _error_code: ::std::os::raw::c_int, + _error_string: Option<&CefString>, + ) { + if let Some(handler) = &self.web_content_process_terminate_handler { + handler(); + } } - } -} -wrap_request_handler! { - pub struct WebRequestHandler { - initialization_scripts: Arc>, - navigation_handler: Option>, - } - - impl RequestHandler { fn on_before_browse( &self, _browser: Option<&mut Browser>, @@ -132,6 +139,8 @@ wrap_request_handler! { _user_gesture: ::std::os::raw::c_int, _is_redirect: ::std::os::raw::c_int, ) -> ::std::os::raw::c_int { + let _ = (&self.context, self.window_id, self.webview_id); + let Some(frame) = frame else { return 0; }; @@ -139,23 +148,26 @@ wrap_request_handler! { if frame.is_main() == 0 { return 0; } - let Some(handler) = &self.navigation_handler else { - return 0; - }; let Some(request) = request else { return 0; }; let url_str = CefString::from(&request.url()).to_string(); + + if url_str == INITIAL_LOAD_URL { + return 0; + } + let Ok(url) = url::Url::parse(&url_str) else { return 0; }; + + let Some(handler) = &self.navigation_handler else { + return 0; + }; + let should_navigate = handler(&url); - if should_navigate { - 0 - } else { - 1 - } + if should_navigate { 0 } else { 1 } } fn resource_request_handler( @@ -168,8 +180,21 @@ wrap_request_handler! { _request_initiator: Option<&CefString>, _disable_default_handling: Option<&mut ::std::os::raw::c_int>, ) -> Option { - Some(WebResourceRequestHandler::new( - self.initialization_scripts.clone(), + // The handler only intercepts the drag-drop bridge requests; when the + // bridge is disabled it would pass every request straight through, so skip + // building (and cloning the context + state into) a handler that CEF calls + // for every subresource/fetch/XHR the page makes. + if !self.drag_drop_handler_enabled { + return None; + } + + Some(WebDragDropResourceRequestHandler::new( + self.context.clone(), + self.window_id, + self.webview_id, + self.drag_drop_event_target, + self.drag_drop_handler_enabled, + self.drag_drop_state.clone(), )) } } @@ -180,6 +205,14 @@ wrap_resource_handler! { webview_label: String, handler: Arc>, initialization_scripts: Arc>, + // Serialized origin of the main frame that initiated this request, captured + // browser-side in the scheme handler factory. The renderer can issue an IPC + // request before its execution context is fully wired to the loader; in + // that window Chromium tags the request with `Origin: null` even though the + // document already has a proper origin. We use this to repair the `Origin` + // header in that case. `None` when the initiator is not the (non-opaque) + // main frame, so sandboxed/subframe `Origin: null` requests are left as-is. + initiator_origin: Option, // we clone response to send it to the handler thread response: HttpResponse, } @@ -201,40 +234,33 @@ wrap_resource_handler! { let response_store = ThreadSafe(self.response.clone()); let initialization_scripts = self.initialization_scripts.clone(); let responder = Box::new(move |response: http::Response>| { - // Check if this is an HTML response that needs script injection - let content_type = response.headers().get(CONTENT_TYPE); - let is_html = content_type + let is_html = response + .headers() + .get(CONTENT_TYPE) .and_then(|ct| ct.to_str().ok()) .map(|ct| ct.to_lowercase().starts_with("text/html")) .unwrap_or(false); let (parts, body) = response.into_parts(); let body_bytes = body.into_owned(); - - let modified_body = if is_html { + let body_bytes = if is_html { inject_scripts_into_html_body(&body_bytes, &initialization_scripts) .unwrap_or(body_bytes) } else { body_bytes }; - let mut response = http::Response::from_parts(parts, Cursor::new(modified_body)); - + let mut response = http::Response::from_parts(parts, Cursor::new(body_bytes)); - let csp = response - .headers_mut() - .get_mut(CONTENT_SECURITY_POLICY); - - if let Some(csp) = csp { - let csp_string = csp.to_str().unwrap().to_string(); - let new_csp = csp_inject_initialization_scripts_hashes( - csp_string, - &initialization_scripts, - ); - *csp = HeaderValue::from_str(&new_csp).unwrap(); + if let Some(csp) = response.headers_mut().get_mut(CONTENT_SECURITY_POLICY) { + let csp_string = csp.to_str().unwrap_or_default().to_string(); + let new_csp = + csp_inject_initialization_scripts_hashes(csp_string, &initialization_scripts); + if let Ok(new_csp) = HeaderValue::from_str(&new_csp) { + *csp = new_csp; + } } - response_store.into_owned().borrow_mut().replace(response); let callback = callback.into_owned(); @@ -245,13 +271,34 @@ wrap_resource_handler! { let handler = self.handler.clone(); let data = read_request_body(request); - let headers = get_request_headers(request); + let mut headers = get_request_headers(request); + + // The renderer can issue an IPC request before its execution context is + // fully wired to the loader; in that window Chromium sends the request + // with `Origin: null` even though the document already has a real + // origin. Repair it from the initiating main frame's URL, which the + // browser process tracks reliably. Only done when the renderer sent no + // origin or a literal `null`, so a correct renderer-sent origin always + // wins. + if let Some(initiator_origin) = &self.initiator_origin { + let origin_missing_or_null = headers + .get(ORIGIN) + .map(|value| value.as_bytes() == b"null") + .unwrap_or(true); + if origin_missing_or_null && let Ok(value) = HeaderValue::from_str(initiator_origin) { + headers.insert(ORIGIN, value); + } + } + let method_str = CefString::from(&request.method()).to_string(); - let method = http::Method::from_bytes(method_str.as_bytes()) - .unwrap_or(http::Method::GET); + let method = http::Method::from_bytes(method_str.as_bytes()).unwrap_or(http::Method::GET); std::thread::spawn(move || { - let mut http_request = http::Request::builder().method(method).uri(url.as_str()).body(data).unwrap(); + let mut http_request = http::Request::builder() + .method(method) + .uri(url.as_str()) + .body(data) + .unwrap(); *http_request.headers_mut() = headers; // handler is Arc>, so we need to dereference to call it (**handler)(&label, http_request, responder); @@ -273,7 +320,12 @@ wrap_resource_handler! { return 0; }; let data_out = unsafe { std::slice::from_raw_parts_mut(data_out, bytes_to_read) }; - let count = self.response.borrow_mut().as_mut().and_then(|response| response.body_mut().read(data_out).ok()).unwrap_or(0); + let count = self + .response + .borrow_mut() + .as_mut() + .and_then(|response| response.body_mut().read(data_out).ok()) + .unwrap_or(0); if let Some(bytes_read) = bytes_read { let Ok(count) = count.try_into() else { return 0; @@ -292,14 +344,18 @@ wrap_resource_handler! { response_length: Option<&mut i64>, redirect_url: Option<&mut CefString>, ) { - let (Some(response), Some(response_data)) = (response, &*self.response.borrow()) else { return }; + let (Some(response), Some(response_data)) = (response, &*self.response.borrow()) else { + return; + }; response.set_status(response_data.status().as_u16() as i32); let mut content_type = None; - // First pass: collect CSP header and set other headers + // Set response headers and remember the MIME type for CEF. for (name, value) in response_data.headers() { - let Ok(value) = value.to_str() else { continue; }; + let Ok(value) = value.to_str() else { + continue; + }; response.set_header_by_name(Some(&name.as_str().into()), Some(&value.into()), 0); @@ -308,11 +364,7 @@ wrap_resource_handler! { } } - response.set_header_by_name( - Some(&"Cache-Control".into()), - Some(&"no-store".into()), - 1, - ); + response.set_header_by_name(Some(&"Cache-Control".into()), Some(&"no-store".into()), 1); let mime_type = content_type .as_ref() @@ -321,7 +373,9 @@ wrap_resource_handler! { .unwrap_or("text/plain"); response.set_mime_type(Some(&mime_type.into())); - if let Some(length) = response_length { *length = -1; } + if let Some(length) = response_length { + *length = -1; + } if let Some(redirect_url) = redirect_url { let _ = std::mem::take(redirect_url); @@ -332,7 +386,7 @@ wrap_resource_handler! { wrap_scheme_handler_factory! { pub struct UriSchemeHandlerFactory { - registry: super::SchemeHandlerRegistry, + registry: SchemeRegistry, scheme: String, } @@ -340,7 +394,7 @@ wrap_scheme_handler_factory! { fn create( &self, browser: Option<&mut Browser>, - _frame: Option<&mut Frame>, + frame: Option<&mut Frame>, _scheme_name: Option<&CefString>, _request: Option<&mut Request>, ) -> Option { @@ -355,7 +409,24 @@ wrap_scheme_handler_factory! { .get(&(id, self.scheme.clone())) .cloned()?; - Some(WebResourceHandler::new(webview_label, handler, initialization_scripts, Arc::new(RefCell::new(None)))) + // Capture the initiating main frame's origin so `process_request` can + // repair a racy `Origin: null` header. Restricted to the main frame: it + // is never an opaque-origin (sandboxed) document in a Tauri webview, so + // upgrading its origin is safe; subframes are intentionally left alone. + let initiator_origin = frame + .filter(|frame| frame.is_main() == 1) + .map(|frame| CefString::from(&frame.url()).to_string()) + .and_then(|url| Url::parse(&url).ok()) + .map(|url| url.origin().ascii_serialization()) + .filter(|origin| origin != "null"); + + Some(WebResourceHandler::new( + webview_label, + handler, + initialization_scripts, + initiator_origin, + Arc::new(RefCell::new(None)), + )) } } } diff --git a/crates/tauri-runtime-cef/src/cef_webview.rs b/crates/tauri-runtime-cef/src/cef_webview.rs deleted file mode 100644 index f82d922220ee..000000000000 --- a/crates/tauri-runtime-cef/src/cef_webview.rs +++ /dev/null @@ -1,110 +0,0 @@ -use cef::*; - -#[cfg(target_os = "macos")] -mod macos; - -#[cfg(windows)] -mod windows; - -#[cfg(target_os = "linux")] -mod linux; - -#[derive(Clone)] -pub enum CefWebview { - BrowserView(cef::BrowserView), - Browser(cef::Browser), -} - -impl CefWebview { - pub fn is_browser(&self) -> bool { - matches!(self, CefWebview::Browser(_)) - } - - pub fn browser(&self) -> Option { - match self { - CefWebview::BrowserView(view) => view.browser(), - CefWebview::Browser(browser) => Some(browser.clone()), - } - } - - pub fn browser_id(&self) -> i32 { - match self { - CefWebview::BrowserView(view) => view.browser().map_or(-1, |b| b.identifier()), - CefWebview::Browser(browser) => browser.identifier(), - } - } - - pub fn set_background_color(&self, color: Option) { - if let CefWebview::BrowserView(view) = self { - let window = view.window(); - let color = color.or_else(|| { - window.map(|w| w.theme_color(ColorId::COLOR_PRIMARY_BACKGROUND.get_raw() as _)) - }); - - if let Some(color) = color { - view.set_background_color(color); - } - } - } - - pub fn bounds(&self) -> cef::Rect { - match self { - CefWebview::BrowserView(view) => view.bounds(), - CefWebview::Browser(browser) => browser.bounds(), - } - } - - pub fn set_bounds(&self, rect: Option<&cef::Rect>) { - match self { - CefWebview::BrowserView(view) => view.set_bounds(rect), - CefWebview::Browser(browser) => browser.set_bounds(rect), - } - } - - pub fn scale_factor(&self) -> f64 { - match self { - CefWebview::BrowserView(view) => view - .window() - .and_then(|w| w.display()) - .map_or(1.0, |d| d.device_scale_factor() as f64), - CefWebview::Browser(browser) => browser.scale_factor(), - } - } - - pub fn set_visible(&self, visible: i32) { - match self { - CefWebview::BrowserView(view) => view.set_visible(visible), - CefWebview::Browser(browser) => browser.set_visible(visible), - } - } - - pub fn close(&self) { - match self { - CefWebview::BrowserView(_) => {} - CefWebview::Browser(browser) => browser.close(), - } - } - - pub fn set_parent(&self, parent: &cef::Window) { - match self { - CefWebview::BrowserView(_) => {} - CefWebview::Browser(browser) => browser.set_parent(parent), - } - } -} - -trait CefBrowserExt { - fn bounds(&self) -> cef::Rect; - fn set_bounds(&self, rect: Option<&cef::Rect>); - fn scale_factor(&self) -> f64; - fn set_visible(&self, visible: i32); - fn close(&self); - fn set_parent(&self, parent: &cef::Window); - - #[cfg(target_os = "macos")] - fn nsview(&self) -> Option>; - #[cfg(windows)] - fn hwnd(&self) -> Option<::windows::Win32::Foundation::HWND>; - #[cfg(target_os = "linux")] - fn xid(&self) -> Option; -} diff --git a/crates/tauri-runtime-cef/src/cef_webview/linux.rs b/crates/tauri-runtime-cef/src/cef_webview/linux.rs deleted file mode 100644 index 90542e597694..000000000000 --- a/crates/tauri-runtime-cef/src/cef_webview/linux.rs +++ /dev/null @@ -1,208 +0,0 @@ -use cef::*; -use std::sync::LazyLock; -use x11_dl::xlib; - -use crate::cef_webview::CefBrowserExt; - -static X11: LazyLock> = LazyLock::new(|| xlib::Xlib::open().ok()); - -impl CefBrowserExt for cef::Browser { - fn xid(&self) -> Option { - let host = self.host()?; - let xid = host.window_handle(); - Some(xid) - } - - fn bounds(&self) -> cef::Rect { - let Some(xid) = self.xid() else { - return cef::Rect::default(); - }; - - let Some(xlib) = X11.as_ref() else { - return cef::Rect::default(); - }; - - unsafe { - let display = (xlib.XOpenDisplay)(std::ptr::null()); - if display.is_null() { - return cef::Rect::default(); - } - - let mut root: xlib::Window = 0; - let mut x: i32 = 0; - let mut y: i32 = 0; - let mut width: u32 = 0; - let mut height: u32 = 0; - let mut border_width: u32 = 0; - let mut depth: u32 = 0; - - let status = (xlib.XGetGeometry)( - display, - xid as xlib::Window, - &mut root, - &mut x, - &mut y, - &mut width, - &mut height, - &mut border_width, - &mut depth, - ); - - (xlib.XCloseDisplay)(display); - - if status == 0 { - return cef::Rect::default(); - } - - // XGetGeometry returns position relative to parent, which is what we need - cef::Rect { - x, - y, - width: width as i32, - height: height as i32, - } - } - } - - fn set_bounds(&self, rect: Option<&cef::Rect>) { - let Some(rect) = rect else { - return; - }; - - let Some(xid) = self.xid() else { - return; - }; - - let Some(xlib) = X11.as_ref() else { - return; - }; - - unsafe { - let display = (xlib.XOpenDisplay)(std::ptr::null()); - if display.is_null() { - return; - } - - (xlib.XMoveResizeWindow)( - display, - xid as xlib::Window, - rect.x, - rect.y, - rect.width as u32, - rect.height as u32, - ); - // Ensure window is mapped and raised after setting bounds - (xlib.XMapRaised)(display, xid as xlib::Window); - (xlib.XFlush)(display); - (xlib.XCloseDisplay)(display); - } - } - - fn scale_factor(&self) -> f64 { - // Get scale factor from primary display - // CEF on Linux doesn't provide direct access to the window's display, - // so we use the primary display as a reasonable default - cef::display_get_primary() - .map(|d| d.device_scale_factor() as f64) - .unwrap_or(1.0) - } - - fn set_visible(&self, visible: i32) { - let Some(xid) = self.xid() else { - return; - }; - - let Some(xlib) = X11.as_ref() else { - return; - }; - - unsafe { - let display = (xlib.XOpenDisplay)(std::ptr::null()); - if display.is_null() { - return; - } - - if visible != 0 { - (xlib.XMapWindow)(display, xid as xlib::Window); - } else { - (xlib.XUnmapWindow)(display, xid as xlib::Window); - } - (xlib.XFlush)(display); - (xlib.XCloseDisplay)(display); - } - } - - fn close(&self) { - let Some(xid) = self.xid() else { - return; - }; - - let Some(xlib) = X11.as_ref() else { - return; - }; - - unsafe { - let display = (xlib.XOpenDisplay)(std::ptr::null()); - if display.is_null() { - return; - } - - (xlib.XDestroyWindow)(display, xid as xlib::Window); - (xlib.XFlush)(display); - (xlib.XCloseDisplay)(display); - } - } - - fn set_parent(&self, parent: &cef::Window) { - let Some(xid) = self.xid() else { - return; - }; - - let parent_xid = parent.window_handle(); - if parent_xid == 0 { - return; - } - - let Some(xlib) = X11.as_ref() else { - return; - }; - - unsafe { - let display = (xlib.XOpenDisplay)(std::ptr::null()); - if display.is_null() { - return; - } - - // Check if window exists before reparenting - let mut root: xlib::Window = 0; - let mut parent_window: xlib::Window = 0; - let mut children: *mut xlib::Window = std::ptr::null_mut(); - let mut nchildren: u32 = 0; - let status = (xlib.XQueryTree)( - display, - xid as xlib::Window, - &mut root, - &mut parent_window, - &mut children, - &mut nchildren, - ); - - if status != 0 && !children.is_null() { - (xlib.XFree)(children as *mut std::ffi::c_void); - } - - (xlib.XReparentWindow)( - display, - xid as xlib::Window, - parent_xid as xlib::Window, - 0, - 0, - ); - - // Ensure window is mapped and raised after reparenting - (xlib.XMapRaised)(display, xid as xlib::Window); - (xlib.XFlush)(display); - (xlib.XCloseDisplay)(display); - } - } -} diff --git a/crates/tauri-runtime-cef/src/cef_webview/macos.rs b/crates/tauri-runtime-cef/src/cef_webview/macos.rs deleted file mode 100644 index 5323f52d26e1..000000000000 --- a/crates/tauri-runtime-cef/src/cef_webview/macos.rs +++ /dev/null @@ -1,99 +0,0 @@ -use crate::cef_webview::CefBrowserExt; -use cef::*; -use objc2::rc::Retained; -use objc2_app_kit::NSView; -use objc2_foundation::{NSPoint, NSRect, NSSize}; - -impl CefBrowserExt for cef::Browser { - fn nsview(&self) -> Option> { - let host = self.host()?; - let nsview = host.window_handle() as *mut NSView; - unsafe { Retained::::retain(nsview) } - } - - fn bounds(&self) -> cef::Rect { - let Some(nsview) = self.nsview() else { - return cef::Rect::default(); - }; - - let parent = unsafe { nsview.superview().unwrap() }; - let parent_frame = parent.frame(); - let webview_frame = nsview.frame(); - - cef::Rect { - x: webview_frame.origin.x as i32, - y: (parent_frame.size.height - webview_frame.origin.y - webview_frame.size.height) as i32, - width: webview_frame.size.width as i32, - height: webview_frame.size.height as i32, - } - } - - fn set_bounds(&self, rect: Option<&cef::Rect>) { - let Some(rect) = rect else { - return; - }; - - let Some(nsview) = self.nsview() else { - return; - }; - - let parent = unsafe { nsview.superview().unwrap() }; - let parent_frame = parent.frame(); - - let origin = NSPoint { - x: rect.x as f64, - y: (parent_frame.size.height - (rect.y as f64 + rect.height as f64)), - }; - - let size = NSSize { - width: rect.width as f64, - height: rect.height as f64, - }; - - unsafe { nsview.setFrame(NSRect { origin, size }) }; - } - - fn scale_factor(&self) -> f64 { - let Some(nsview) = self.nsview() else { - return 1.0; - }; - - let screen = nsview.window().and_then(|w| w.screen()); - screen.map(|s| s.backingScaleFactor()).unwrap_or(1.0) - } - - fn set_visible(&self, visible: i32) { - let Some(nsview) = self.nsview() else { - return; - }; - - if visible != 0 { - nsview.setHidden(false); - } else { - nsview.setHidden(true); - } - } - - fn close(&self) { - let Some(nsview) = self.nsview() else { - return; - }; - - unsafe { nsview.removeFromSuperview() }; - } - - fn set_parent(&self, parent: &cef::Window) { - crate::cef_impl::ensure_valid_content_view(parent.window_handle()); - - let Some(nsview) = self.nsview() else { - return; - }; - - let parent_nsview = parent.window_handle(); - let Some(parent_nsview) = (unsafe { Retained::::retain(parent_nsview as _) }) else { - return; - }; - - unsafe { parent_nsview.addSubview(&nsview) }; - } -} diff --git a/crates/tauri-runtime-cef/src/cef_webview/windows.rs b/crates/tauri-runtime-cef/src/cef_webview/windows.rs deleted file mode 100644 index c4af7caf857b..000000000000 --- a/crates/tauri-runtime-cef/src/cef_webview/windows.rs +++ /dev/null @@ -1,205 +0,0 @@ -use cef::*; -use std::sync::LazyLock; - -use crate::cef_webview::CefBrowserExt; -use windows::{ - Win32::{ - Foundation::*, - Graphics::Gdi::*, - System::LibraryLoader::*, - UI::{HiDpi::*, WindowsAndMessaging::*}, - }, - core::{HRESULT, HSTRING, PCSTR}, -}; - -impl CefBrowserExt for cef::Browser { - fn hwnd(&self) -> Option { - let host = self.host()?; - let hwnd = host.window_handle(); - Some(HWND(hwnd.0 as _)) - } - - fn bounds(&self) -> cef::Rect { - let Some(hwnd) = self.hwnd() else { - return cef::Rect::default(); - }; - - let mut rect = RECT::default(); - let _ = unsafe { GetClientRect(hwnd, &mut rect) }; - - let position_point = &mut [POINT { - x: rect.left, - y: rect.top, - }]; - unsafe { MapWindowPoints(Some(hwnd), GetParent(hwnd).ok(), position_point) }; - - cef::Rect { - x: position_point[0].x, - y: position_point[0].y, - width: (rect.right - rect.left) as i32, - height: (rect.bottom - rect.top) as i32, - } - } - - fn set_bounds(&self, rect: Option<&cef::Rect>) { - let Some(rect) = rect else { - return; - }; - - let Some(hwnd) = self.hwnd() else { - return; - }; - - let _ = unsafe { - SetWindowPos( - hwnd, - None, - rect.x, - rect.y, - rect.width, - rect.height, - SWP_ASYNCWINDOWPOS | SWP_NOACTIVATE | SWP_NOZORDER, - ) - }; - } - - fn scale_factor(&self) -> f64 { - let Some(hwnd) = self.hwnd() else { - return 1.0; - }; - - let dpi = unsafe { hwnd_dpi(hwnd) }; - dpi_to_scale_factor(dpi) - } - - fn set_visible(&self, visible: i32) { - let Some(hwnd) = self.hwnd() else { - return; - }; - - if visible != 0 { - let _ = unsafe { ShowWindow(hwnd, SW_SHOW) }; - unsafe { ensure_render_target(hwnd) }; - } else { - let _ = unsafe { ShowWindow(hwnd, SW_HIDE) }; - } - } - - fn close(&self) { - let Some(hwnd) = self.hwnd() else { - return; - }; - - let _ = unsafe { DestroyWindow(hwnd) }; - } - - fn set_parent(&self, parent: &cef::Window) { - let Some(hwnd) = self.hwnd() else { - return; - }; - - let parent_hwnd = HWND(parent.window_handle().0 as _); - let _ = unsafe { SetParent(hwnd, Some(parent_hwnd)) }; - } -} - -/// Toggle visibility on Chrome_WidgetWin_1 children that may have lost their -/// Chrome_RenderWidgetHostHWND render target (probably destroyed by CDP freeze). -unsafe fn ensure_render_target(hwnd: HWND) { - use windows::core::PCWSTR; - - const CHROME_WIDGET: PCWSTR = windows::core::w!("Chrome_WidgetWin_1"); - const RENDER_TARGET: PCWSTR = windows::core::w!("Chrome_RenderWidgetHostHWND"); - - let mut child = unsafe { FindWindowExW(Some(hwnd), None, CHROME_WIDGET, PCWSTR::null()) }; - while let Ok(child_hwnd) = child { - if unsafe { FindWindowExW(Some(child_hwnd), None, RENDER_TARGET, PCWSTR::null()).is_err() } { - // Hide and show the child to force CEF to recreate the render target - let _ = unsafe { ShowWindow(child_hwnd, SW_HIDE) }; - let _ = unsafe { ShowWindow(child_hwnd, SW_SHOW) }; - } - child = unsafe { FindWindowExW(Some(hwnd), Some(child_hwnd), CHROME_WIDGET, PCWSTR::null()) }; - } -} - -fn get_function_impl(library: &str, function: &str) -> FARPROC { - let library = HSTRING::from(library); - assert_eq!(function.chars().last(), Some('\0')); - - // Library names we will use are ASCII so we can use the A version to avoid string conversion. - let module = unsafe { LoadLibraryW(&library) }.unwrap_or_default(); - if module.is_invalid() { - return None; - } - - unsafe { GetProcAddress(module, PCSTR::from_raw(function.as_ptr())) } -} - -macro_rules! get_function { - ($lib:expr, $func:ident) => { - get_function_impl($lib, concat!(stringify!($func), '\0')) - .map(|f| unsafe { std::mem::transmute::<_, $func>(f) }) - }; -} - -pub type GetDpiForWindow = unsafe extern "system" fn(hwnd: HWND) -> u32; -pub type GetDpiForMonitor = unsafe extern "system" fn( - hmonitor: HMONITOR, - dpi_type: MONITOR_DPI_TYPE, - dpi_x: *mut u32, - dpi_y: *mut u32, -) -> HRESULT; - -static GET_DPI_FOR_WINDOW: LazyLock> = - LazyLock::new(|| get_function!("user32.dll", GetDpiForWindow)); -static GET_DPI_FOR_MONITOR: LazyLock> = - LazyLock::new(|| get_function!("shcore.dll", GetDpiForMonitor)); - -pub const BASE_DPI: u32 = 96; -pub fn dpi_to_scale_factor(dpi: u32) -> f64 { - dpi as f64 / BASE_DPI as f64 -} - -#[allow(non_snake_case)] -pub unsafe fn hwnd_dpi(hwnd: HWND) -> u32 { - if let Some(GetDpiForWindow) = *GET_DPI_FOR_WINDOW { - // We are on Windows 10 Anniversary Update (1607) or later. - match GetDpiForWindow(hwnd) { - 0 => BASE_DPI, // 0 is returned if hwnd is invalid - #[allow(clippy::unnecessary_cast)] - dpi => dpi as u32, - } - } else if let Some(GetDpiForMonitor) = *GET_DPI_FOR_MONITOR { - // We are on Windows 8.1 or later. - let monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); - if monitor.is_invalid() { - return BASE_DPI; - } - - let mut dpi_x = 0; - let mut dpi_y = 0; - #[allow(clippy::unnecessary_cast)] - if GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &mut dpi_x, &mut dpi_y) == S_OK { - dpi_x as u32 - } else { - BASE_DPI - } - } else { - let hdc = GetDC(Some(hwnd)); - if hdc.is_invalid() { - return BASE_DPI; - } - - // We are on Vista or later. - if IsProcessDPIAware().as_bool() { - // If the process is DPI aware, then scaling must be handled by the application using - // this DPI value. - GetDeviceCaps(Some(hdc), LOGPIXELSX) as u32 - } else { - // If the process is DPI unaware, then scaling is performed by the OS; we thus return - // 96 (scale factor 1.0) to prevent the window from being re-scaled by both the - // application and the WM. - BASE_DPI - } - } -} diff --git a/crates/tauri-runtime-cef/src/external_message_pump/linux.rs b/crates/tauri-runtime-cef/src/external_message_pump/linux.rs new file mode 100644 index 000000000000..dd4e2df75f70 --- /dev/null +++ b/crates/tauri-runtime-cef/src/external_message_pump/linux.rs @@ -0,0 +1,96 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Linux/BSD backend for the CEF external message pump. +//! +//! cefclient owns the GLib loop and drives a GLib timeout from it +//! (`main_message_loop_external_pump_linux.cc`). Here winit owns an X11 loop +//! instead, so the timeout is attached to the default GLib main context and the +//! winit loop services that context every iteration (see the runtime's +//! `service_glib`), waking at [`PlatformPump::deadline`] via +//! `ControlFlow::WaitUntil`. The timeout still fires inside nested GLib loops +//! (e.g. GTK menus/dialogs) that winit cannot observe, keeping CEF pumping +//! there — the same property the Windows/macOS backends get from `WM_TIMER` / +//! `NSTimer`. +//! +//! Reference: +//! + +use std::sync::Weak; +use std::time::{Duration, Instant}; + +use gtk::glib; +use winit::event_loop::EventLoopProxy; + +use super::PumpState; + +pub(super) struct PlatformPump { + state: Weak, + proxy: EventLoopProxy, + timer: Option, + deadline: Option, +} + +impl PlatformPump { + pub(super) fn new(state: Weak, proxy: EventLoopProxy) -> Self { + Self { + state, + proxy, + timer: None, + deadline: None, + } + } + + pub(super) fn post_schedule_work(&mut self, delay_ms: i64) { + // May be called on any thread. Marshal the request onto the default GLib + // main context, which the winit loop services on the main thread; wake winit + // so it does so promptly. + let state = self.state.clone(); + glib::idle_add_once(move || { + if let Some(state) = state.upgrade() { + state.on_schedule_work(delay_ms); + } + }); + self.proxy.wake_up(); + } + + pub(super) fn set_timer(&mut self, delay_ms: i64) { + debug_assert!(self.timer.is_none()); + debug_assert!(delay_ms > 0); + + let delay = Duration::from_millis(delay_ms as u64); + let state = self.state.clone(); + let source = glib::timeout_add_once(delay, move || { + let Some(state) = state.upgrade() else { + return; + }; + // This one-shot source removes itself after firing, so forget it before + // `on_timer_timeout` runs `kill_timer` — `SourceId::remove` would panic on + // an already-removed source. + if let Ok(mut platform) = state.platform.lock() { + platform.timer = None; + platform.deadline = None; + } + state.on_timer_timeout(); + }); + + self.timer = Some(source); + self.deadline = Some(Instant::now() + delay); + } + + pub(super) fn kill_timer(&mut self) { + if let Some(source) = self.timer.take() { + source.remove(); + } + self.deadline = None; + } + + pub(super) fn is_timer_pending(&self) -> bool { + self.timer.is_some() + } + + pub(super) fn deadline(&self) -> Option { + self.deadline + } +} diff --git a/crates/tauri-runtime-cef/src/external_message_pump/macos.rs b/crates/tauri-runtime-cef/src/external_message_pump/macos.rs new file mode 100644 index 000000000000..fdc7376841d2 --- /dev/null +++ b/crates/tauri-runtime-cef/src/external_message_pump/macos.rs @@ -0,0 +1,127 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! macOS backend for the CEF external message pump. +//! +//! Mirrors cefclient's `main_message_loop_external_pump_mac.mm`: scheduling +//! requests are posted back onto the owning AppKit thread with +//! `performSelector:onThread:`, and delayed work is driven by an `NSTimer` +//! installed in the common and event-tracking run-loop modes so it keeps firing +//! while AppKit spins a nested menu/tracking loop (e.g. a webview context menu) +//! that winit's callbacks never observe. +//! +//! Reference: +//! + +use std::sync::Weak; + +use objc2::{AnyThread, DefinedClass, define_class, msg_send, rc::Retained, sel}; +use objc2_app_kit::NSEventTrackingRunLoopMode; +use objc2_foundation::{ + NSNumber, NSObject, NSObjectNSThreadPerformAdditions, NSObjectProtocol, NSRunLoop, + NSRunLoopCommonModes, NSThread, NSTimer, +}; + +use super::PumpState; + +define_class! { + #[unsafe(super(NSObject))] + #[ivars = Weak] + struct EventHandler; + + impl EventHandler { + #[unsafe(method(scheduleWork:))] + fn schedule_work(&self, delay_ms: &NSNumber) { + let Some(state) = self.ivars().upgrade() else { + return; + }; + state.on_schedule_work(delay_ms.as_i64()); + } + + #[unsafe(method(timerTimeout:))] + fn timer_timeout(&self, _: &NSTimer) { + let Some(state) = self.ivars().upgrade() else { + return; + }; + state.on_timer_timeout(); + } + } + + unsafe impl NSObjectProtocol for EventHandler {} +} + +impl EventHandler { + fn new(state: Weak) -> Retained { + let this = Self::alloc().set_ivars(state); + unsafe { msg_send![super(this), init] } + } +} + +pub(super) struct PlatformPump { + owner_thread: Retained, + event_handler: Retained, + timer: Option>, +} + +// SAFETY: the owner thread and timer are only touched on the AppKit thread that +// constructed the pump; `post_schedule_work` marshals back to it before use. +unsafe impl Send for PlatformPump {} + +impl PlatformPump { + pub(super) fn new(state: Weak) -> Self { + Self { + owner_thread: NSThread::currentThread(), + event_handler: EventHandler::new(state), + timer: None, + } + } + + pub(super) fn post_schedule_work(&mut self, delay_ms: i64) { + let delay_ms = isize::try_from(delay_ms).unwrap_or(isize::MAX); + let delay_ms = NSNumber::new_isize(delay_ms); + unsafe { + self + .event_handler + .performSelector_onThread_withObject_waitUntilDone( + sel!(scheduleWork:), + &self.owner_thread, + Some(&delay_ms), + false, + ); + } + } + + pub(super) fn set_timer(&mut self, delay_ms: i64) { + debug_assert!(delay_ms > 0); + debug_assert!(self.timer.is_none()); + + let timer = unsafe { + NSTimer::timerWithTimeInterval_target_selector_userInfo_repeats( + delay_ms as f64 / 1000.0, + &self.event_handler, + sel!(timerTimeout:), + None, + false, + ) + }; + + let run_loop = NSRunLoop::currentRunLoop(); + unsafe { + run_loop.addTimer_forMode(&timer, NSRunLoopCommonModes); + run_loop.addTimer_forMode(&timer, NSEventTrackingRunLoopMode); + } + + self.timer = Some(timer); + } + + pub(super) fn kill_timer(&mut self) { + if let Some(timer) = self.timer.take() { + timer.invalidate(); + } + } + + pub(super) fn is_timer_pending(&self) -> bool { + self.timer.is_some() + } +} diff --git a/crates/tauri-runtime-cef/src/external_message_pump/mod.rs b/crates/tauri-runtime-cef/src/external_message_pump/mod.rs new file mode 100644 index 000000000000..5140d2e25f6f --- /dev/null +++ b/crates/tauri-runtime-cef/src/external_message_pump/mod.rs @@ -0,0 +1,231 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Self-contained CEF external message pump. +//! +//! With [`cef::Settings::external_message_pump`] enabled, CEF does not run its +//! own message loop. Instead it asks the host to call +//! [`cef::do_message_loop_work`] by invoking `OnScheduleMessagePumpWork(delay)` +//! whenever it has work pending. +//! +//! This is a port of upstream cefclient's external pump — same semantics and +//! logic, adapted to Rust. The platform-independent scheduling/reentrancy logic +//! lives here; each platform supplies a [`PlatformPump`] backend that drives a +//! timer: +//! +//! - Windows: a `WM_TIMER` on a message-only window. +//! - macOS: an `NSTimer` in the common and event-tracking run-loop modes. +//! - Linux/BSD: a GLib timeout serviced by the winit loop. +//! +//! On Windows and macOS the timer lives on the same native loop winit already +//! runs, so CEF keeps painting and processing IPC even while the OS spins a +//! nested modal loop winit cannot observe (window move/resize on Windows, menu +//! and event tracking on macOS). On Linux/BSD the GLib timeout still fires +//! inside nested GLib loops (e.g. GTK menus/dialogs) for the same reason. +//! +//! Reference implementation (cefclient base class): +//! - +//! - + +use std::sync::{ + Arc, Mutex, + atomic::{AtomicBool, Ordering}, +}; + +#[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] +mod linux; +#[cfg(target_os = "macos")] +mod macos; +#[cfg(windows)] +mod windows; + +#[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] +use linux::PlatformPump; +#[cfg(target_os = "macos")] +use macos::PlatformPump; +#[cfg(windows)] +use windows::PlatformPump; + +#[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] +use winit::event_loop::EventLoopProxy; + +/// Sentinel delay used to (re)arm the fallback "max delay" tick. Kept within 32 +/// bits for Win32/AppKit timer API compatibility, matching cefclient's +/// `kTimerDelayPlaceholder`. +const TIMER_DELAY_PLACEHOLDER: i64 = i32::MAX as i64; + +/// Upper bound on how long we wait between [`cef::do_message_loop_work`] calls +/// (~30fps), matching cefclient's `kMaxTimerDelay`. +const MAX_TIMER_DELAY_MS: i64 = 1000 / 30; + +/// Handle to the external message pump. Cloning shares the same underlying +/// state; the backing platform resources are released when the last clone drops. +#[derive(Clone)] +pub(crate) struct CefExternalPump { + state: Arc, +} + +impl CefExternalPump { + pub(crate) fn new( + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + proxy: EventLoopProxy, + ) -> Self { + let state = Arc::new_cyclic(|weak| PumpState { + is_active: AtomicBool::new(false), + reentrancy_detected: AtomicBool::new(false), + platform: Mutex::new(PlatformPump::new( + weak.clone(), + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + proxy, + )), + }); + + Self { state } + } + + /// Called from CEF's `OnScheduleMessagePumpWork`. May run on any thread. + pub(crate) fn schedule_message_pump_work(&self, delay_ms: i64) { + self.state.schedule_message_pump_work(delay_ms); + } + + /// Explicit tick, used to drive CEF before winit's loop is running (startup) + /// and after winit processes a batch of events. Must run on the owner thread. + pub(crate) fn do_message_loop_work(&self) { + self.state.do_work(); + } + + /// When the GLib timer is next due, so the winit loop can wake to service + /// GLib (see [`linux`]). + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + pub(crate) fn next_deadline(&self) -> Option { + self.state.platform.lock().ok().and_then(|p| p.deadline()) + } +} + +/// Platform-independent pump state, shared with the [`PlatformPump`] backend. +struct PumpState { + is_active: AtomicBool, + reentrancy_detected: AtomicBool, + platform: Mutex, +} + +impl PumpState { + /// Post a scheduling request onto the owner thread. The platform backend is + /// responsible for delivering it there, where it lands back in + /// [`Self::on_schedule_work`]. Mirrors the platform `OnScheduleMessagePumpWork`. + fn schedule_message_pump_work(&self, delay_ms: i64) { + if let Ok(mut platform) = self.platform.lock() { + platform.post_schedule_work(delay_ms); + } + } + + /// Runs on the owner thread once a scheduling request is delivered. Mirrors + /// cefclient's `OnScheduleWork`. + fn on_schedule_work(&self, delay_ms: i64) { + { + let Ok(mut platform) = self.platform.lock() else { + return; + }; + + // An already-pending timer covers the fallback tick; don't reset it. + if delay_ms == TIMER_DELAY_PLACEHOLDER && platform.is_timer_pending() { + return; + } + + platform.kill_timer(); + } + + if delay_ms <= 0 { + self.do_work(); + return; + } + + if let Ok(mut platform) = self.platform.lock() { + platform.set_timer(delay_ms.min(MAX_TIMER_DELAY_MS)); + } + } + + /// Runs on the owner thread when the platform timer fires. Mirrors cefclient's + /// `OnTimerTimeout`. + fn on_timer_timeout(&self) { + if let Ok(mut platform) = self.platform.lock() { + platform.kill_timer(); + } + self.do_work(); + } + + /// Mirrors cefclient's `DoWork`. + fn do_work(&self) { + let was_reentrant = self.perform_message_loop_work(); + if was_reentrant { + // The work was discarded because we were already inside + // do_message_loop_work; repost so it runs on the next clean turn. + self.schedule_message_pump_work(0); + return; + } + + // Arm the fallback tick so CEF work it didn't explicitly announce (e.g. via + // its own internal timers) still runs within the max delay. + let timer_pending = self + .platform + .lock() + .map(|platform| platform.is_timer_pending()) + .unwrap_or(true); + if !timer_pending { + self.schedule_message_pump_work(TIMER_DELAY_PLACEHOLDER); + } + } + + /// Mirrors cefclient's `PerformMessageLoopWork`. + fn perform_message_loop_work(&self) -> bool { + if self.is_active.swap(true, Ordering::SeqCst) { + // do_message_loop_work can trigger CEF callbacks (paint, IPC) that + // re-enter this method. Record it so the caller reschedules the work. + self.reentrancy_detected.store(true, Ordering::SeqCst); + return false; + } + + self.reentrancy_detected.store(false, Ordering::SeqCst); + cef::do_message_loop_work(); + self.is_active.store(false, Ordering::SeqCst); + + self.reentrancy_detected.load(Ordering::SeqCst) + } +} diff --git a/crates/tauri-runtime-cef/src/external_message_pump/windows.rs b/crates/tauri-runtime-cef/src/external_message_pump/windows.rs new file mode 100644 index 000000000000..69bf45175258 --- /dev/null +++ b/crates/tauri-runtime-cef/src/external_message_pump/windows.rs @@ -0,0 +1,163 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Windows backend for the CEF external message pump. +//! +//! Mirrors cefclient's `main_message_loop_external_pump_win.cc`: a message-only +//! window owns a `WM_TIMER`, and `OnScheduleMessagePumpWork` posts a private +//! `WM_HAVE_WORK` message to it. winit runs the thread's own +//! `GetMessage`/`DispatchMessage` loop, which delivers both messages to our +//! window procedure — so CEF is pumped from the same loop, including while +//! Windows runs a modal move/resize loop that winit's `ApplicationHandler` +//! callbacks never observe. +//! +//! Reference: +//! + +use std::sync::Weak; + +use windows::{ + Win32::{ + Foundation::{HINSTANCE, HWND, LPARAM, LRESULT, WPARAM}, + System::LibraryLoader::GetModuleHandleW, + UI::WindowsAndMessaging::{ + CreateWindowExW, DefWindowProcW, DestroyWindow, GWLP_USERDATA, GetWindowLongPtrW, + HWND_MESSAGE, KillTimer, PostMessageW, RegisterClassExW, SetTimer, SetWindowLongPtrW, + WINDOW_EX_STYLE, WM_TIMER, WM_USER, WNDCLASSEXW, WS_OVERLAPPEDWINDOW, + }, + }, + core::{PCWSTR, w}, +}; + +use super::PumpState; + +/// Window class for the pump's message-only window. +const WINDOW_CLASS: PCWSTR = w!("TauriCefExternalMessagePump"); +/// Private message posted by `OnScheduleMessagePumpWork`; matches cefclient's +/// `kMsgHaveWork` (`WM_USER + 1`). The `LPARAM` carries the delay in ms. +const WM_HAVE_WORK: u32 = WM_USER + 1; +/// Timer id used with `SetTimer`/`KillTimer` on the pump window. +const TIMER_ID: usize = 1; + +pub(super) struct PlatformPump { + hwnd: HWND, + timer_pending: bool, +} + +// SAFETY: `hwnd` is created on, and its timer is only armed/disarmed from, the +// main (winit) thread. The sole cross-thread use is `PostMessageW` in +// `post_schedule_work`, which Win32 explicitly permits from any thread. +unsafe impl Send for PlatformPump {} + +impl PlatformPump { + pub(super) fn new(state: Weak) -> Self { + let hinstance: HINSTANCE = unsafe { GetModuleHandleW(None) } + .map(|module| HINSTANCE(module.0)) + .unwrap_or_default(); + + let class = WNDCLASSEXW { + cbSize: std::mem::size_of::() as u32, + lpfnWndProc: Some(wndproc), + hInstance: hinstance, + lpszClassName: WINDOW_CLASS, + ..Default::default() + }; + // Registering an already-registered class fails harmlessly; this also lets a + // second runtime in the same process reuse the class. + unsafe { RegisterClassExW(&class) }; + + let hwnd = unsafe { + CreateWindowExW( + WINDOW_EX_STYLE::default(), + WINDOW_CLASS, + PCWSTR::null(), + WS_OVERLAPPEDWINDOW, + 0, + 0, + 0, + 0, + Some(HWND_MESSAGE), + None, + Some(hinstance), + None, + ) + } + .expect("failed to create CEF external message pump window"); + + // Store a Weak back-reference for the window procedure; freed in `Drop`. No + // timer is armed and no message is posted yet, so the window cannot be + // dispatched anything that reads this slot before it is set. + let state = Box::into_raw(Box::new(state)); + unsafe { SetWindowLongPtrW(hwnd, GWLP_USERDATA, state as isize) }; + + Self { + hwnd, + timer_pending: false, + } + } + + pub(super) fn post_schedule_work(&mut self, delay_ms: i64) { + // Thread-safe; lands in `wndproc` (WM_HAVE_WORK) on the owner thread. + let _ = unsafe { + PostMessageW( + Some(self.hwnd), + WM_HAVE_WORK, + WPARAM(0), + LPARAM(delay_ms as isize), + ) + }; + } + + pub(super) fn set_timer(&mut self, delay_ms: i64) { + debug_assert!(!self.timer_pending); + debug_assert!(delay_ms > 0); + self.timer_pending = true; + unsafe { SetTimer(Some(self.hwnd), TIMER_ID, delay_ms as u32, None) }; + } + + pub(super) fn kill_timer(&mut self) { + if self.timer_pending { + let _ = unsafe { KillTimer(Some(self.hwnd), TIMER_ID) }; + self.timer_pending = false; + } + } + + pub(super) fn is_timer_pending(&self) -> bool { + self.timer_pending + } +} + +impl Drop for PlatformPump { + fn drop(&mut self) { + unsafe { + if self.timer_pending { + let _ = KillTimer(Some(self.hwnd), TIMER_ID); + } + + let state = SetWindowLongPtrW(self.hwnd, GWLP_USERDATA, 0) as *mut Weak; + if !state.is_null() { + drop(Box::from_raw(state)); + } + + let _ = DestroyWindow(self.hwnd); + } + } +} + +unsafe extern "system" fn wndproc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT { + if msg == WM_TIMER || msg == WM_HAVE_WORK { + let state = unsafe { GetWindowLongPtrW(hwnd, GWLP_USERDATA) } as *const Weak; + if !state.is_null() + && let Some(state) = unsafe { &*state }.upgrade() + { + if msg == WM_HAVE_WORK { + state.on_schedule_work(lparam.0 as i64); + } else { + state.on_timer_timeout(); + } + } + } + + unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) } +} diff --git a/crates/tauri-runtime-cef/src/lib.rs b/crates/tauri-runtime-cef/src/lib.rs index 64ff59058b12..cb9b74c322db 100644 --- a/crates/tauri-runtime-cef/src/lib.rs +++ b/crates/tauri-runtime-cef/src/lib.rs @@ -5,2525 +5,15 @@ #![allow(clippy::arc_with_non_send_sync)] #![allow(clippy::too_many_arguments)] -use cef::{CefString, ImplCommandLine, ImplTaskRunner}; -use tauri_runtime::{ - Cookie, DeviceEventFilter, EventLoopProxy, Icon, InitAttribute, ProgressBarState, Result, - RunEvent, Runtime, RuntimeHandle, RuntimeInitArgs, UserAttentionType, UserEvent, WebviewDispatch, - WebviewEventId, WindowDispatch, WindowEventId, - dpi::{PhysicalPosition, PhysicalSize, Position, Rect, Size}, - monitor::Monitor, - webview::{DetachedWebview, PendingWebview, WebviewAttributes}, - window::{ - CursorIcon, DetachedWindow, DetachedWindowWebview, PendingWindow, RawWindow, WebviewEvent, - WindowBuilder, WindowBuilderBase, WindowEvent, WindowId, - }, -}; - -#[cfg(target_os = "macos")] -use tauri_utils::TitleBarStyle; -use tauri_utils::{ - Theme, - config::{Color, WindowConfig}, -}; -use url::Url; - -#[cfg(windows)] -use windows::Win32::Foundation::HWND; - -use dioxus_debug_cell::RefCell; -use std::{ - collections::HashMap, - fmt, - fs::create_dir_all, - sync::{ - Arc, Mutex, - atomic::AtomicBool, - mpsc::{Sender, channel}, - }, - thread::{self, ThreadId}, -}; - -#[cfg(target_os = "macos")] -use crate::application::AppDelegateEvent; -use crate::cef_webview::CefWebview; - mod cef_impl; -mod cef_webview; -mod utils; - -type DevToolsProtocolHandler = dyn Fn(DevToolsProtocol) + Send + Sync; - -pub fn webview_version() -> Result { - Ok(format!( - "{}.{}.{}.{}", - cef_dll_sys::CHROME_VERSION_MAJOR, - cef_dll_sys::CHROME_VERSION_MINOR, - cef_dll_sys::CHROME_VERSION_PATCH, - cef_dll_sys::CHROME_VERSION_BUILD - )) -} - -#[macro_export] -macro_rules! getter { - ($self: ident, $rx: expr, $message: expr) => {{ - $crate::send_user_message(&$self.context, $message)?; - $rx - .recv() - .map_err(|_| tauri_runtime::Error::FailedToReceiveMessage) - }}; -} - -macro_rules! webview_getter { - ($self: ident, $message_variant: path) => {{ - let (tx, rx) = channel(); - getter!( - $self, - rx, - Message::Webview { - window_id: *$self.window_id.lock().unwrap(), - webview_id: $self.webview_id, - message: $message_variant(tx) - } - ) - }}; -} - -macro_rules! window_getter { - ($self: ident, $message_variant: path) => {{ - let (tx, rx) = channel(); - getter!( - $self, - rx, - Message::Window { - window_id: $self.window_id, - message: $message_variant(tx) - } - ) - }}; -} - -type AfterWindowCreation = Box; - -enum Message { - Task(Box), - CreateWindow { - window_id: WindowId, - webview_id: u32, - pending: Box>>, - after_window_creation: Option, - }, - CreateWebview { - window_id: WindowId, - webview_id: u32, - pending: Box>>, - }, - Window { - window_id: WindowId, - message: WindowMessage, - }, - Webview { - window_id: WindowId, - webview_id: u32, - message: WebviewMessage, - }, - RequestExit(i32), - UserEvent(T), - Noop, -} - -enum WindowMessage { - Close, - Destroy, - AddEventListener(WindowEventId, Box), - // Getters - ScaleFactor(Sender>), - InnerPosition(Sender>>), - OuterPosition(Sender>>), - InnerSize(Sender>>), - OuterSize(Sender>>), - IsFullscreen(Sender>), - IsMinimized(Sender>), - IsMaximized(Sender>), - IsFocused(Sender>), - IsDecorated(Sender>), - IsResizable(Sender>), - IsMaximizable(Sender>), - IsMinimizable(Sender>), - IsClosable(Sender>), - IsVisible(Sender>), - Title(Sender>), - CurrentMonitor(Sender>>), - PrimaryMonitor(Sender>>), - MonitorFromPoint(Sender>>, f64, f64), - AvailableMonitors(Sender>>), - Theme(Sender>), - IsEnabled(Sender>), - IsAlwaysOnTop(Sender>), - RawWindowHandle( - Sender< - std::result::Result, raw_window_handle::HandleError>, - >, - ), - // Setters - Center, - RequestUserAttention(Option), - SetEnabled(bool), - SetResizable(bool), - SetMaximizable(bool), - SetMinimizable(bool), - SetClosable(bool), - SetTitle(String), - Maximize, - Unmaximize, - Minimize, - Unminimize, - Show, - Hide, - SetDecorations(bool), - SetShadow(bool), - SetAlwaysOnBottom(bool), - SetAlwaysOnTop(bool), - SetVisibleOnAllWorkspaces(bool), - SetContentProtected(bool), - SetSize(Size), - SetMinSize(Option), - SetMaxSize(Option), - SetSizeConstraints(tauri_runtime::window::WindowSizeConstraints), - SetPosition(Position), - SetFullscreen(bool), - #[cfg(target_os = "macos")] - SetSimpleFullscreen(bool), - SetFocus, - SetFocusable(bool), - SetIcon(Icon<'static>), - SetSkipTaskbar(bool), - SetCursorGrab(bool), - SetCursorVisible(bool), - SetCursorIcon(CursorIcon), - SetCursorPosition(Position), - SetIgnoreCursorEvents(bool), - SetProgressBar(ProgressBarState), - SetBadgeCount(Option, Option), - SetBadgeLabel(Option), - SetOverlayIcon(Option>), - SetTitleBarStyle(tauri_utils::TitleBarStyle), - SetTrafficLightPosition(Position), - SetTheme(Option), - SetBackgroundColor(Option), - StartDragging, - StartResizeDragging(tauri_runtime::ResizeDirection), -} - -pub enum WebviewMessage { - AddEventListener(WebviewEventId, Box), - EvaluateScript(String), - CookiesForUrl(Url, Sender>>>), - Cookies(Sender>>>), - SetCookie(Cookie<'static>), - DeleteCookie(Cookie<'static>), - Navigate(Url), - Reload, - GoBack, - CanGoBack(Sender>), - GoForward, - CanGoForward(Sender>), - Print, - Close, - Show, - Hide, - SetPosition(Position), - SetSize(Size), - SetBounds(Rect), - SetFocus, - Reparent(WindowId, Sender>), - SetAutoResize(bool), - SetZoom(f64), - SetBackgroundColor(Option), - ClearAllBrowsingData, - // Getters - Url(Sender>), - Bounds(Sender>), - Position(Sender>>), - Size(Sender>>), - WithWebview(Box) + Send>), - // Devtools - #[cfg(any(debug_assertions, feature = "devtools"))] - OpenDevTools, - #[cfg(any(debug_assertions, feature = "devtools"))] - CloseDevTools, - #[cfg(any(debug_assertions, feature = "devtools"))] - IsDevToolsOpen(Sender), - SendDevToolsMessage(Vec, Sender>), - OnDevToolsProtocol(Arc, Sender>), -} - -/// A DevTools protocol message delivered to [`on_dev_tools_protocol`](CefWebviewDispatcher::on_dev_tools_protocol) callbacks. -#[derive(Debug, Clone)] -pub enum DevToolsProtocol { - /// Raw UTF-8 encoded JSON message (method result or event). - Message(Vec), - /// DevTools protocol event with method name and params. - Event { method: String, params: Vec }, - /// Result of a DevTools method call. - MethodResult { - message_id: i32, - success: bool, - result: Vec, - }, -} - -impl Clone for Message { - fn clone(&self) -> Self { - match self { - Self::UserEvent(t) => Self::UserEvent(t.clone()), - _ => unimplemented!(), - } - } -} - -#[derive(Clone)] -pub(crate) struct AppWebview { - pub webview_id: u32, - #[allow(dead_code)] - pub label: String, - pub inner: CefWebview, - // browser_view.browser is null on the scheme handler factory, - // so we need to use the browser_id to identify the browser - pub browser_id: Arc>, - pub bounds: Arc>>, - #[allow(unused)] - pub devtools_enabled: bool, - pub uri_scheme_protocols: - Arc>>>, - #[allow(dead_code)] - pub initialization_scripts: Arc>, - pub devtools_protocol_handlers: Arc>>>, - /// Keeps the DevTools message observer registered. Dropping this unregisters the observer. - #[allow(dead_code)] - pub devtools_observer_registration: Arc>>, - pub webview_attributes: Arc>, -} - -#[derive(Debug, Clone)] -pub struct WebviewBounds { - pub x_rate: f32, - pub y_rate: f32, - pub width_rate: f32, - pub height_rate: f32, -} - -pub type WindowEventHandler = Box; -pub type WindowEventListeners = Arc>>; -pub type WebviewEventHandler = Box; -pub type WebviewEventListeners = - Arc>>>>>; - -pub(crate) enum AppWindowKind { - Window(cef::Window), - BrowserWindow, -} - -pub(crate) struct AppWindow { - pub label: String, - pub window: AppWindowKind, - pub force_close: Arc, - pub attributes: Arc>, - pub webviews: Vec, - pub window_event_listeners: WindowEventListeners, - pub webview_event_listeners: WebviewEventListeners, -} - -impl AppWindow { - fn window(&self) -> Option { - match &self.window { - AppWindowKind::Window(window) => Some(window.clone()), - AppWindowKind::BrowserWindow => None, - } - } -} - -#[derive(Clone)] -pub struct RuntimeContext { - main_thread_task_runner: cef::TaskRunner, - main_thread_id: ThreadId, - cef_context: cef_impl::Context, -} - -// SAFETY: we ensure this type is only used on the main thread. -#[allow(clippy::non_send_fields_in_send_ty)] -unsafe impl Send for RuntimeContext {} - -// SAFETY: we ensure this type is only used on the main thread. -#[allow(clippy::non_send_fields_in_send_ty)] -unsafe impl Sync for RuntimeContext {} - -impl RuntimeContext { - fn post_message(&self, message: Message) -> Result<()> { - if thread::current().id() == self.main_thread_id { - // Already on main thread, execute directly - cef_impl::handle_message(&self.cef_context, message); - Ok(()) - } else { - // Post to main thread via TaskRunner - self - .main_thread_task_runner - .post_task(Some(&mut cef_impl::SendMessageTask::new( - self.cef_context.clone(), - Arc::new(RefCell::new(message)), - ))); - Ok(()) - } - } - - fn create_window( - &self, - pending: PendingWindow>, - after_window_creation: Option, - ) -> Result>> { - let label = pending.label.clone(); - let context = self.clone(); - let window_id = self.cef_context.next_window_id(); - let (webview_id, use_https_scheme, devtools) = pending - .webview - .as_ref() - .map(|w| { - ( - Some(context.cef_context.next_webview_id()), - w.webview_attributes.use_https_scheme, - w.webview_attributes.devtools, - ) - }) - .unwrap_or((None, false, None)); - - self.post_message(Message::CreateWindow { - window_id, - webview_id: webview_id.unwrap_or_default(), - pending: Box::new(pending), - after_window_creation: after_window_creation.map(|f| Box::new(f) as AfterWindowCreation), - })?; - - let dispatcher = CefWindowDispatcher { - window_id, - context: self.clone(), - }; - - let detached_webview = webview_id.map(|id| { - let webview = DetachedWebview { - label: label.clone(), - dispatcher: CefWebviewDispatcher { - window_id: Arc::new(Mutex::new(window_id)), - webview_id: id, - context: self.clone(), - }, - }; - DetachedWindowWebview { - webview, - use_https_scheme, - devtools, - } - }); - - Ok(DetachedWindow { - id: window_id, - label, - dispatcher, - webview: detached_webview, - }) - } - - fn create_webview( - &self, - window_id: WindowId, - pending: PendingWebview>, - ) -> Result>> { - let label = pending.label.clone(); - let webview_id = self.cef_context.next_webview_id(); - - self.post_message(Message::CreateWebview { - window_id, - webview_id, - pending: Box::new(pending), - })?; - - let dispatcher = CefWebviewDispatcher { - window_id: Arc::new(Mutex::new(window_id)), - webview_id, - context: self.clone(), - }; - - Ok(DetachedWebview { label, dispatcher }) - } -} - -// Mirrors tauri-runtime-wry's send_user_message behavior: if we're already on the main -// thread, handle the message immediately; otherwise, post it to the main thread. -pub(crate) fn send_user_message( - context: &RuntimeContext, - message: Message, -) -> Result<()> { - if thread::current().id() == context.main_thread_id { - cef_impl::handle_message(&context.cef_context, message); - } else { - context - .main_thread_task_runner - .post_task(Some(&mut cef_impl::SendMessageTask::new( - context.cef_context.clone(), - Arc::new(RefCell::new(message)), - ))); - } - Ok(()) -} - -impl fmt::Debug for RuntimeContext { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("RuntimeContext").finish() - } -} - -#[derive(Debug, Clone)] -pub struct CefRuntimeHandle { - context: RuntimeContext, -} - -impl RuntimeHandle for CefRuntimeHandle { - type Runtime = CefRuntime; - - fn create_proxy(&self) -> >::EventLoopProxy { - EventProxy { - context: self.context.clone(), - } - } - - #[cfg(target_os = "macos")] - fn set_activation_policy( - &self, - _activation_policy: tauri_runtime::ActivationPolicy, - ) -> Result<()> { - Ok(()) - } - - #[cfg(target_os = "macos")] - fn set_dock_visibility(&self, _visible: bool) -> Result<()> { - Ok(()) - } - - fn request_exit(&self, code: i32) -> Result<()> { - // Request exit by posting a task to quit the message loop - self.context.post_message(Message::RequestExit(code)) - } - - /// Create a new webview window. - fn create_window) + Send + 'static>( - &self, - pending: PendingWindow, - after_window_creation: Option, - ) -> Result> { - self.context.create_window(pending, after_window_creation) - } - - fn create_webview( - &self, - window_id: WindowId, - pending: PendingWebview, - ) -> Result> { - self.context.create_webview(window_id, pending) - } - - /// Run a task on the main thread. - fn run_on_main_thread(&self, f: F) -> Result<()> { - self.context.post_message(Message::Task(Box::new(f))) - } - - fn display_handle( - &self, - ) -> std::result::Result, raw_window_handle::HandleError> { - #[cfg(target_os = "linux")] - return Ok(unsafe { - raw_window_handle::DisplayHandle::borrow_raw(raw_window_handle::RawDisplayHandle::Xlib( - raw_window_handle::XlibDisplayHandle::new(None, 0), - )) - }); - #[cfg(target_os = "macos")] - return Ok(unsafe { - raw_window_handle::DisplayHandle::borrow_raw(raw_window_handle::RawDisplayHandle::AppKit( - raw_window_handle::AppKitDisplayHandle::new(), - )) - }); - #[cfg(windows)] - return Ok(unsafe { - raw_window_handle::DisplayHandle::borrow_raw(raw_window_handle::RawDisplayHandle::Windows( - raw_window_handle::WindowsDisplayHandle::new(), - )) - }); - #[cfg(not(any(target_os = "linux", target_os = "macos", windows)))] - unimplemented!(); - } - - fn primary_monitor(&self) -> Option { - crate::cef_impl::get_primary_monitor() - } - - fn monitor_from_point(&self, x: f64, y: f64) -> Option { - crate::cef_impl::get_monitor_from_point(x, y) - } - - fn available_monitors(&self) -> Vec { - crate::cef_impl::get_available_monitors() - } - - fn set_theme(&self, _theme: Option) {} - - /// Shows the application, but does not automatically focus it. - #[cfg(target_os = "macos")] - fn show(&self) -> Result<()> { - self.context.post_message(Message::Task(Box::new(|| { - cef_impl::set_application_visibility(true); - }))) - } - - /// Hides the application. - #[cfg(target_os = "macos")] - fn hide(&self) -> Result<()> { - self.context.post_message(Message::Task(Box::new(|| { - cef_impl::set_application_visibility(false); - }))) - } - - fn set_device_event_filter(&self, _filter: DeviceEventFilter) {} - - #[cfg(target_os = "android")] - fn find_class<'a>( - &self, - env: &mut jni::JNIEnv<'a>, - activity: &jni::objects::JObject<'_>, - name: impl Into, - ) -> std::result::Result, jni::errors::Error> { - todo!() - } - - #[cfg(target_os = "android")] - fn run_on_android_context(&self, f: F) - where - F: FnOnce(&mut jni::JNIEnv, &jni::objects::JObject, &jni::objects::JObject) + Send + 'static, - { - todo!() - } - - #[cfg(any(target_os = "macos", target_os = "ios"))] - fn fetch_data_store_identifiers) + Send + 'static>( - &self, - _cb: F, - ) -> Result<()> { - todo!() - } - - #[cfg(any(target_os = "macos", target_os = "ios"))] - fn remove_data_store) + Send + 'static>( - &self, - _uuid: [u8; 16], - _cb: F, - ) -> Result<()> { - todo!() - } - - fn cursor_position(&self) -> Result> { - Ok(PhysicalPosition::new(0.0, 0.0)) - } -} - -#[derive(Debug, Clone)] -pub struct CefWebviewDispatcher { - window_id: Arc>, - webview_id: u32, - context: RuntimeContext, -} - -#[derive(Debug, Clone)] -pub struct CefWindowDispatcher { - window_id: WindowId, - context: RuntimeContext, -} - -#[derive(Debug, Clone)] -pub struct CefWindowBuilder { - title: Option, - position: Option, - inner_size: Option, - min_inner_size: Option, - max_inner_size: Option, - inner_size_constraints: Option, - center: bool, - prevent_overflow: Option, - resizable: Option, - maximizable: Option, - minimizable: Option, - closable: Option, - fullscreen: Option, - focused: Option, - focusable: Option, - maximized: Option, - visible: Option, - #[cfg(any(not(target_os = "macos"), feature = "macos-private-api"))] - transparent: Option, - decorations: Option, - always_on_bottom: Option, - always_on_top: Option, - visible_on_all_workspaces: Option, - content_protected: Option, - skip_taskbar: Option, - shadow: Option, - theme: Option, - background_color: Option, - #[cfg(target_os = "macos")] - title_bar_style: Option, - #[cfg(target_os = "macos")] - traffic_light_position: Option, - #[cfg(target_os = "macos")] - hidden_title: Option, - #[cfg(target_os = "macos")] - tabbing_identifier: Option, - #[cfg(windows)] - window_classname: Option, - #[cfg(windows)] - owner: Option, - #[cfg(windows)] - parent: Option, - #[cfg(windows)] - drag_and_drop: Option, - has_icon: bool, - icon: Option>, - browser_window: bool, -} - -impl Default for CefWindowBuilder { - fn default() -> Self { - Self { - title: None, - position: None, - inner_size: None, - min_inner_size: None, - max_inner_size: None, - inner_size_constraints: None, - center: false, - prevent_overflow: None, - resizable: None, - maximizable: None, - minimizable: None, - closable: None, - fullscreen: None, - focused: Some(true), - focusable: None, - maximized: None, - visible: Some(true), - #[cfg(any(not(target_os = "macos"), feature = "macos-private-api"))] - transparent: None, - decorations: Some(true), - always_on_bottom: None, - always_on_top: None, - visible_on_all_workspaces: None, - content_protected: None, - skip_taskbar: None, - shadow: None, - theme: None, - background_color: None, - #[cfg(target_os = "macos")] - title_bar_style: None, - #[cfg(target_os = "macos")] - traffic_light_position: None, - #[cfg(target_os = "macos")] - hidden_title: None, - #[cfg(target_os = "macos")] - tabbing_identifier: None, - #[cfg(windows)] - window_classname: None, - #[cfg(windows)] - owner: None, - #[cfg(windows)] - parent: None, - #[cfg(windows)] - drag_and_drop: None, - has_icon: false, - icon: None, - browser_window: false, - } - } -} - -impl CefWindowBuilder { - pub fn browser_window(mut self) -> Self { - self.browser_window = true; - self - } -} - -impl WindowBuilderBase for CefWindowBuilder {} - -impl WindowBuilder for CefWindowBuilder { - fn new() -> Self { - Self::default().title("Tauri App") - } - - fn with_config(config: &WindowConfig) -> Self { - let mut builder = Self::default(); - - builder = builder - .title(config.title.to_string()) - .inner_size(config.width, config.height) - .focused(config.focus) - .focusable(config.focusable) - .visible(config.visible) - .resizable(config.resizable) - .fullscreen(config.fullscreen) - .decorations(config.decorations) - .maximized(config.maximized) - .always_on_bottom(config.always_on_bottom) - .always_on_top(config.always_on_top) - .visible_on_all_workspaces(config.visible_on_all_workspaces) - .content_protected(config.content_protected) - .skip_taskbar(config.skip_taskbar) - .theme(config.theme) - .closable(config.closable) - .maximizable(config.maximizable) - .minimizable(config.minimizable) - .shadow(config.shadow); - - let mut constraints = tauri_runtime::window::WindowSizeConstraints::default(); - if let Some(min_width) = config.min_width { - constraints.min_width = Some(tauri_runtime::dpi::LogicalUnit::new(min_width).into()); - } - if let Some(min_height) = config.min_height { - constraints.min_height = Some(tauri_runtime::dpi::LogicalUnit::new(min_height).into()); - } - if let Some(max_width) = config.max_width { - constraints.max_width = Some(tauri_runtime::dpi::LogicalUnit::new(max_width).into()); - } - if let Some(max_height) = config.max_height { - constraints.max_height = Some(tauri_runtime::dpi::LogicalUnit::new(max_height).into()); - } - builder = builder.inner_size_constraints(constraints); - - if let Some(color) = config.background_color { - builder = builder.background_color(color); - } - - if let (Some(x), Some(y)) = (config.x, config.y) { - builder = builder.position(x, y); - } - - if config.center { - builder = builder.center(); - } - - #[cfg(any(not(target_os = "macos"), feature = "macos-private-api"))] - { - builder = builder.transparent(config.transparent); - } - - #[cfg(target_os = "macos")] - { - builder = builder - .hidden_title(config.hidden_title) - .title_bar_style(config.title_bar_style); - if let Some(identifier) = &config.tabbing_identifier { - builder = builder.tabbing_identifier(identifier); - } - if let Some(position) = &config.traffic_light_position { - builder = builder.traffic_light_position(tauri_runtime::dpi::LogicalPosition::new( - position.x, position.y, - )); - } - } - - #[cfg(windows)] - { - if let Some(window_classname) = &config.window_classname { - builder = builder.window_classname(window_classname); - } - } - - builder - } - - fn center(mut self) -> Self { - self.center = true; - self - } - - fn position(mut self, x: f64, y: f64) -> Self { - self.position = Some(Position::Logical(tauri_runtime::dpi::LogicalPosition::new( - x, y, - ))); - self - } - - fn inner_size(mut self, width: f64, height: f64) -> Self { - self.inner_size = Some(Size::Logical(tauri_runtime::dpi::LogicalSize::new( - width, height, - ))); - self - } - - fn min_inner_size(mut self, min_width: f64, min_height: f64) -> Self { - self.min_inner_size = Some(Size::Logical(tauri_runtime::dpi::LogicalSize::new( - min_width, min_height, - ))); - self - } - - fn max_inner_size(mut self, max_width: f64, max_height: f64) -> Self { - self.max_inner_size = Some(Size::Logical(tauri_runtime::dpi::LogicalSize::new( - max_width, max_height, - ))); - self - } - - fn inner_size_constraints( - mut self, - constraints: tauri_runtime::window::WindowSizeConstraints, - ) -> Self { - self.inner_size_constraints = Some(constraints); - self - } - - fn prevent_overflow(mut self) -> Self { - self.prevent_overflow = Some(Size::Physical(PhysicalSize::new(0, 0))); - self - } - - fn prevent_overflow_with_margin(mut self, margin: Size) -> Self { - self.prevent_overflow = Some(margin); - self - } - - fn resizable(mut self, resizable: bool) -> Self { - self.resizable = Some(resizable); - self - } - - fn maximizable(mut self, maximizable: bool) -> Self { - self.maximizable = Some(maximizable); - self - } - - fn minimizable(mut self, minimizable: bool) -> Self { - self.minimizable = Some(minimizable); - self - } - - fn closable(mut self, closable: bool) -> Self { - self.closable = Some(closable); - self - } - - fn title>(mut self, title: S) -> Self { - self.title = Some(title.into()); - self - } - - fn fullscreen(mut self, fullscreen: bool) -> Self { - self.fullscreen = Some(fullscreen); - self - } - - fn focused(mut self, focused: bool) -> Self { - self.focused = Some(focused); - self - } - - fn focusable(mut self, focusable: bool) -> Self { - self.focusable = Some(focusable); - self - } - - fn maximized(mut self, maximized: bool) -> Self { - self.maximized = Some(maximized); - self - } - - fn visible(mut self, visible: bool) -> Self { - self.visible = Some(visible); - self - } - - #[cfg(any(not(target_os = "macos"), feature = "macos-private-api"))] - fn transparent(self, transparent: bool) -> Self { - let mut s = self; - s.transparent = Some(transparent); - s - } - - fn decorations(mut self, decorations: bool) -> Self { - self.decorations = Some(decorations); - self - } - - fn always_on_bottom(mut self, always_on_bottom: bool) -> Self { - self.always_on_bottom = Some(always_on_bottom); - self - } - - fn always_on_top(mut self, always_on_top: bool) -> Self { - self.always_on_top = Some(always_on_top); - self - } - - fn visible_on_all_workspaces(mut self, visible_on_all_workspaces: bool) -> Self { - self.visible_on_all_workspaces = Some(visible_on_all_workspaces); - self - } - - fn content_protected(mut self, protected: bool) -> Self { - self.content_protected = Some(protected); - self - } - - fn icon(mut self, icon: Icon<'_>) -> Result { - self.has_icon = true; - self.icon.replace(icon.into_owned()); - Ok(self) - } - - fn skip_taskbar(mut self, skip: bool) -> Self { - self.skip_taskbar = Some(skip); - self - } - - fn window_classname>(self, classname: S) -> Self { - #[cfg(windows)] - { - let mut s = self; - s.window_classname = Some(classname.into()); - s - } - #[cfg(not(windows))] - { - let _classname = classname; - self - } - } - - fn shadow(mut self, enable: bool) -> Self { - self.shadow = Some(enable); - self - } - - #[cfg(windows)] - fn owner(mut self, owner: HWND) -> Self { - self.owner = Some(owner); - self - } - - #[cfg(windows)] - fn parent(mut self, parent: HWND) -> Self { - self.parent = Some(parent); - self - } - - #[cfg(target_os = "macos")] - fn parent(self, _parent: *mut std::ffi::c_void) -> Self { - self - } - - #[cfg(any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" - ))] - fn transient_for(self, _parent: &impl gtk::glib::IsA) -> Self { - self - } - - #[cfg(windows)] - fn drag_and_drop(mut self, enabled: bool) -> Self { - self.drag_and_drop = Some(enabled); - self - } - - #[cfg(target_os = "macos")] - fn title_bar_style(mut self, style: TitleBarStyle) -> Self { - self.title_bar_style = Some(style); - self - } - - #[cfg(target_os = "macos")] - fn traffic_light_position>(mut self, position: P) -> Self { - self.traffic_light_position = Some(position.into()); - self - } - - #[cfg(target_os = "macos")] - fn hidden_title(mut self, hidden: bool) -> Self { - self.hidden_title = Some(hidden); - self - } - - #[cfg(target_os = "macos")] - fn tabbing_identifier(mut self, identifier: &str) -> Self { - self.tabbing_identifier = Some(identifier.into()); - self - } - - fn theme(mut self, theme: Option) -> Self { - self.theme = theme; - self - } - - fn has_icon(&self) -> bool { - self.has_icon - } - - fn get_theme(&self) -> Option { - self.theme - } - - fn background_color(self, color: tauri_utils::config::Color) -> Self { - let mut s = self; - s.background_color = Some(color); - s - } -} - -/// CEF-specific webview APIs. -impl CefWebviewDispatcher { - /// Send a message to the DevTools agent. The message should be a UTF-8 encoded JSON - /// string following the Chrome DevTools Protocol format. - pub fn send_dev_tools_message(&self, message: &[u8]) -> Result<()> { - let (tx, rx) = channel(); - self.context.post_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::SendDevToolsMessage(message.to_vec(), tx), - })?; - rx.recv() - .map_err(|_| tauri_runtime::Error::FailedToReceiveMessage)? - } - - /// Register a callback to receive DevTools protocol messages. Messages include - /// both method results and events from the DevTools agent. - pub fn on_dev_tools_protocol( - &self, - f: F, - ) -> Result<()> { - let (tx, rx) = channel(); - let handler = - Arc::new(move |protocol: DevToolsProtocol| f(protocol)) as Arc; - self.context.post_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::OnDevToolsProtocol(handler, tx), - })?; - rx.recv() - .map_err(|_| tauri_runtime::Error::FailedToReceiveMessage)? - } -} - -impl WebviewDispatch for CefWebviewDispatcher { - type Runtime = CefRuntime; - - fn run_on_main_thread(&self, f: F) -> Result<()> { - self.context.post_message(Message::Task(Box::new(f))) - } - - fn on_webview_event( - &self, - f: F, - ) -> tauri_runtime::WebviewEventId { - let id = self.context.cef_context.next_webview_event_id(); - let _ = self.context.post_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::AddEventListener(id, Box::new(f)), - }); - id - } - - fn with_webview) + Send + 'static>(&self, f: F) -> Result<()> { - self.context.post_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::WithWebview(Box::new(f)), - }) - } - - #[cfg(any(debug_assertions, feature = "devtools"))] - fn open_devtools(&self) { - let _ = self.context.post_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::OpenDevTools, - }); - } - - #[cfg(any(debug_assertions, feature = "devtools"))] - fn close_devtools(&self) { - let _ = self.context.post_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::CloseDevTools, - }); - } - - #[cfg(any(debug_assertions, feature = "devtools"))] - fn is_devtools_open(&self) -> Result { - webview_getter!(self, WebviewMessage::IsDevToolsOpen) - } - - fn set_zoom(&self, scale_factor: f64) -> Result<()> { - self.context.post_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::SetZoom(scale_factor), - }) - } - - fn eval_script>(&self, script: S) -> Result<()> { - self.context.post_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::EvaluateScript(script.into()), - }) - } - - fn url(&self) -> Result { - webview_getter!(self, WebviewMessage::Url)? - } - - fn bounds(&self) -> Result { - webview_getter!(self, WebviewMessage::Bounds)? - } - - fn position(&self) -> Result> { - webview_getter!(self, WebviewMessage::Position)? - } - - fn size(&self) -> Result> { - webview_getter!(self, WebviewMessage::Size)? - } - - fn navigate(&self, url: Url) -> Result<()> { - self.context.post_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::Navigate(url), - }) - } - - fn reload(&self) -> Result<()> { - self.context.post_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::Reload, - }) - } - - fn go_back(&self) -> Result<()> { - self.context.post_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::GoBack, - }) - } - - fn can_go_back(&self) -> Result { - webview_getter!(self, WebviewMessage::CanGoBack)? - } - - fn go_forward(&self) -> Result<()> { - self.context.post_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::GoForward, - }) - } - - fn can_go_forward(&self) -> Result { - webview_getter!(self, WebviewMessage::CanGoForward)? - } - - fn print(&self) -> Result<()> { - self.context.post_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::Print, - }) - } - - fn close(&self) -> Result<()> { - self.context.post_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::Close, - }) - } - - fn set_bounds(&self, bounds: Rect) -> Result<()> { - self.context.post_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::SetBounds(bounds), - }) - } - - fn set_size(&self, size: Size) -> Result<()> { - self.context.post_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::SetSize(size), - }) - } - - fn set_position(&self, position: Position) -> Result<()> { - self.context.post_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::SetPosition(position), - }) - } - - fn set_focus(&self) -> Result<()> { - self.context.post_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::SetFocus, - }) - } - - fn reparent(&self, window_id: WindowId) -> Result<()> { - let mut current_window_id = self.window_id.lock().unwrap(); - let (tx, rx) = channel(); - self.context.post_message(Message::Webview { - window_id: *current_window_id, - webview_id: self.webview_id, - message: WebviewMessage::Reparent(window_id, tx), - })?; - - rx.recv().unwrap()?; - - *current_window_id = window_id; - Ok(()) - } - - fn cookies_for_url(&self, url: Url) -> Result>> { - let current_window_id = self.window_id.lock().unwrap(); - let (tx, rx) = channel(); - self.context.post_message(Message::Webview { - window_id: *current_window_id, - webview_id: self.webview_id, - message: WebviewMessage::CookiesForUrl(url, tx), - })?; - - rx.recv().unwrap() - } - - fn cookies(&self) -> Result>> { - webview_getter!(self, WebviewMessage::Cookies)? - } - - fn set_cookie(&self, cookie: Cookie<'_>) -> Result<()> { - self.context.post_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::SetCookie(cookie.into_owned()), - }) - } - - fn delete_cookie(&self, cookie: Cookie<'_>) -> Result<()> { - self.context.post_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::DeleteCookie(cookie.into_owned()), - }) - } - - fn set_auto_resize(&self, auto_resize: bool) -> Result<()> { - self.context.post_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::SetAutoResize(auto_resize), - }) - } - - fn clear_all_browsing_data(&self) -> Result<()> { - self.context.post_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::ClearAllBrowsingData, - }) - } - - fn hide(&self) -> Result<()> { - self.context.post_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::Hide, - }) - } - - fn show(&self) -> Result<()> { - self.context.post_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::Show, - }) - } - - fn set_background_color(&self, color: Option) -> Result<()> { - self.context.post_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::SetBackgroundColor(color), - }) - } -} - -impl WindowDispatch for CefWindowDispatcher { - type Runtime = CefRuntime; - - type WindowBuilder = CefWindowBuilder; - - fn run_on_main_thread(&self, f: F) -> Result<()> { - self.context.post_message(Message::Task(Box::new(f))) - } - - fn on_window_event(&self, f: F) -> WindowEventId { - let context = self.context.clone(); - let window_id = self.window_id; - let event_id = context.cef_context.next_window_event_id(); - let handler = Box::new(f); - - // Register the listener on the main thread - let _ = context.post_message(Message::Window { - window_id, - message: WindowMessage::AddEventListener(event_id, handler), - }); - - event_id - } - - fn scale_factor(&self) -> Result { - window_getter!(self, WindowMessage::ScaleFactor)? - } - - fn inner_position(&self) -> Result> { - window_getter!(self, WindowMessage::InnerPosition)? - } - - fn outer_position(&self) -> Result> { - window_getter!(self, WindowMessage::OuterPosition)? - } - - fn inner_size(&self) -> Result> { - window_getter!(self, WindowMessage::InnerSize)? - } - - fn outer_size(&self) -> Result> { - window_getter!(self, WindowMessage::OuterSize)? - } - - fn is_fullscreen(&self) -> Result { - window_getter!(self, WindowMessage::IsFullscreen)? - } - - fn is_minimized(&self) -> Result { - window_getter!(self, WindowMessage::IsMinimized)? - } - - fn is_maximized(&self) -> Result { - window_getter!(self, WindowMessage::IsMaximized)? - } - - fn is_focused(&self) -> Result { - window_getter!(self, WindowMessage::IsFocused)? - } - - fn is_decorated(&self) -> Result { - window_getter!(self, WindowMessage::IsDecorated)? - } - - fn is_resizable(&self) -> Result { - window_getter!(self, WindowMessage::IsResizable)? - } - - fn is_maximizable(&self) -> Result { - window_getter!(self, WindowMessage::IsMaximizable)? - } - - fn is_minimizable(&self) -> Result { - window_getter!(self, WindowMessage::IsMinimizable)? - } - - fn is_closable(&self) -> Result { - window_getter!(self, WindowMessage::IsClosable)? - } - - fn is_visible(&self) -> Result { - window_getter!(self, WindowMessage::IsVisible)? - } - - fn title(&self) -> Result { - window_getter!(self, WindowMessage::Title)? - } - - fn current_monitor(&self) -> Result> { - window_getter!(self, WindowMessage::CurrentMonitor)? - } - - fn primary_monitor(&self) -> Result> { - window_getter!(self, WindowMessage::PrimaryMonitor)? - } - - fn monitor_from_point(&self, x: f64, y: f64) -> Result> { - let (tx, rx) = channel(); - getter!( - self, - rx, - Message::Window { - window_id: self.window_id, - message: WindowMessage::MonitorFromPoint(tx, x, y) - } - )? - } - - fn available_monitors(&self) -> Result> { - window_getter!(self, WindowMessage::AvailableMonitors)? - } - - fn theme(&self) -> Result { - window_getter!(self, WindowMessage::Theme)? - } - - #[cfg(any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" - ))] - fn gtk_window(&self) -> Result { - unimplemented!() - } - - #[cfg(any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" - ))] - fn default_vbox(&self) -> Result { - unimplemented!() - } - - fn window_handle( - &self, - ) -> std::result::Result, raw_window_handle::HandleError> { - let (tx, rx) = channel(); - self - .context - .post_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::RawWindowHandle(tx), - }) - .map_err(|_| raw_window_handle::HandleError::Unavailable)?; - rx.recv() - .map_err(|_| raw_window_handle::HandleError::Unavailable)? - } - - fn center(&self) -> Result<()> { - self.context.post_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::Center, - }) - } - - fn request_user_attention(&self, request_type: Option) -> Result<()> { - self.context.post_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::RequestUserAttention(request_type), - }) - } - - fn create_window) + Send + 'static>( - &mut self, - pending: PendingWindow, - after_window_creation: Option, - ) -> Result> { - self.context.create_window(pending, after_window_creation) - } - - fn create_webview( - &mut self, - pending: PendingWebview, - ) -> Result> { - self.context.create_webview(self.window_id, pending) - } - - fn set_resizable(&self, resizable: bool) -> Result<()> { - self.context.post_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetResizable(resizable), - }) - } - - fn set_maximizable(&self, maximizable: bool) -> Result<()> { - self.context.post_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetMaximizable(maximizable), - }) - } - - fn set_minimizable(&self, minimizable: bool) -> Result<()> { - self.context.post_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetMinimizable(minimizable), - }) - } - - fn set_closable(&self, closable: bool) -> Result<()> { - self.context.post_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetClosable(closable), - }) - } - - fn set_title>(&self, title: S) -> Result<()> { - self.context.post_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetTitle(title.into()), - }) - } - - fn maximize(&self) -> Result<()> { - self.context.post_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::Maximize, - }) - } - - fn unmaximize(&self) -> Result<()> { - self.context.post_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::Unmaximize, - }) - } - - fn minimize(&self) -> Result<()> { - self.context.post_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::Minimize, - }) - } - - fn unminimize(&self) -> Result<()> { - self.context.post_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::Unminimize, - }) - } - - fn show(&self) -> Result<()> { - self.context.post_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::Show, - }) - } - - fn hide(&self) -> Result<()> { - self.context.post_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::Hide, - }) - } - - fn close(&self) -> Result<()> { - self.context.post_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::Close, - }) - } - - fn destroy(&self) -> Result<()> { - self.context.post_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::Destroy, - }) - } - - fn set_decorations(&self, decorations: bool) -> Result<()> { - self.context.post_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetDecorations(decorations), - }) - } - - fn set_shadow(&self, shadow: bool) -> Result<()> { - self.context.post_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetShadow(shadow), - }) - } - - fn set_always_on_bottom(&self, always_on_bottom: bool) -> Result<()> { - self.context.post_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetAlwaysOnBottom(always_on_bottom), - }) - } - - fn set_always_on_top(&self, always_on_top: bool) -> Result<()> { - self.context.post_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetAlwaysOnTop(always_on_top), - }) - } - - fn set_visible_on_all_workspaces(&self, visible_on_all_workspaces: bool) -> Result<()> { - self.context.post_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetVisibleOnAllWorkspaces(visible_on_all_workspaces), - }) - } - - fn set_content_protected(&self, protected: bool) -> Result<()> { - self.context.post_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetContentProtected(protected), - }) - } - - fn set_size(&self, size: Size) -> Result<()> { - self.context.post_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetSize(size), - }) - } - - fn set_min_size(&self, size: Option) -> Result<()> { - self.context.post_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetMinSize(size), - }) - } - - fn set_max_size(&self, size: Option) -> Result<()> { - self.context.post_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetMaxSize(size), - }) - } - - fn set_position(&self, position: Position) -> Result<()> { - self.context.post_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetPosition(position), - }) - } - - fn set_fullscreen(&self, fullscreen: bool) -> Result<()> { - self.context.post_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetFullscreen(fullscreen), - }) - } - - #[cfg(target_os = "macos")] - fn set_simple_fullscreen(&self, fullscreen: bool) -> Result<()> { - self.context.post_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetSimpleFullscreen(fullscreen), - }) - } - - fn set_focus(&self) -> Result<()> { - self.context.post_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetFocus, - }) - } - - fn set_focusable(&self, focusable: bool) -> Result<()> { - self.context.post_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetFocusable(focusable), - }) - } - - fn set_icon(&self, icon: Icon<'_>) -> Result<()> { - self.context.post_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetIcon(icon.into_owned()), - }) - } - - fn set_skip_taskbar(&self, skip: bool) -> Result<()> { - self.context.post_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetSkipTaskbar(skip), - }) - } - - fn set_cursor_grab(&self, grab: bool) -> Result<()> { - self.context.post_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetCursorGrab(grab), - }) - } - - fn set_cursor_visible(&self, visible: bool) -> Result<()> { - self.context.post_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetCursorVisible(visible), - }) - } - - fn set_cursor_icon(&self, icon: CursorIcon) -> Result<()> { - self.context.post_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetCursorIcon(icon), - }) - } - - fn set_cursor_position>(&self, position: Pos) -> Result<()> { - self.context.post_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetCursorPosition(position.into()), - }) - } - - fn set_ignore_cursor_events(&self, ignore: bool) -> Result<()> { - self.context.post_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetIgnoreCursorEvents(ignore), - }) - } - - fn start_dragging(&self) -> Result<()> { - self.context.post_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::StartDragging, - }) - } - - fn start_resize_dragging(&self, direction: tauri_runtime::ResizeDirection) -> Result<()> { - self.context.post_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::StartResizeDragging(direction), - }) - } - - fn set_progress_bar(&self, progress_state: ProgressBarState) -> Result<()> { - self.context.post_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetProgressBar(progress_state), - }) - } - - fn set_badge_count(&self, count: Option, desktop_filename: Option) -> Result<()> { - self.context.post_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetBadgeCount(count, desktop_filename), - }) - } - - fn set_badge_label(&self, label: Option) -> Result<()> { - self.context.post_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetBadgeLabel(label), - }) - } - - fn set_overlay_icon(&self, icon: Option>) -> Result<()> { - self.context.post_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetOverlayIcon(icon.map(|i| i.into_owned())), - }) - } - - fn set_title_bar_style(&self, style: tauri_utils::TitleBarStyle) -> Result<()> { - self.context.post_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetTitleBarStyle(style), - }) - } - - fn set_traffic_light_position(&self, position: Position) -> Result<()> { - self.context.post_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetTrafficLightPosition(position), - }) - } - - fn set_size_constraints( - &self, - constraints: tauri_runtime::window::WindowSizeConstraints, - ) -> Result<()> { - self.context.post_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetSizeConstraints(constraints), - }) - } - - fn set_theme(&self, theme: Option) -> Result<()> { - self.context.post_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetTheme(theme), - }) - } - - fn set_enabled(&self, enabled: bool) -> Result<()> { - self.context.post_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetEnabled(enabled), - }) - } - - fn is_enabled(&self) -> Result { - window_getter!(self, WindowMessage::IsEnabled)? - } - - fn is_always_on_top(&self) -> Result { - window_getter!(self, WindowMessage::IsAlwaysOnTop)? - } - - fn set_background_color(&self, color: Option) -> Result<()> { - self.context.post_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetBackgroundColor(color), - }) - } -} - -#[derive(Clone)] -pub struct EventProxy { - context: RuntimeContext, -} - -// SAFETY: we ensure the context is only used on the main thread. -#[allow(clippy::non_send_fields_in_send_ty)] -unsafe impl Send for EventProxy {} - -// SAFETY: we ensure the context is only used on the main thread. -#[allow(clippy::non_send_fields_in_send_ty)] -unsafe impl Sync for EventProxy {} - -impl fmt::Debug for EventProxy { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("EventProxy").finish() - } -} - -impl EventLoopProxy for EventProxy { - fn send_event(&self, event: T) -> Result<()> { - self.context.post_message(Message::UserEvent(event)) - } -} - -#[derive(Debug)] -pub struct CefRuntime { - pub context: RuntimeContext, - event_tx: std::sync::mpsc::Sender>, - event_rx: std::sync::mpsc::Receiver>, -} - -#[cfg(target_os = "macos")] -fn is_cef_helper_process() -> bool { - const HELPER_SUFFIXES: &[&str] = &[ - " Helper (GPU)", - " Helper (Renderer)", - " Helper (Plugin)", - " Helper (Alerts)", - " Helper", - ]; - std::env::current_exe() - .ok() - .and_then(|p| { - p.file_name() - .and_then(|s| s.to_str()) - .map(|name| HELPER_SUFFIXES.iter().any(|s| name.ends_with(s))) - }) - .unwrap_or_default() -} - -impl CefRuntime { - fn init(runtime_args: RuntimeInitArgs) -> Self { - let args = cef::args::Args::new(); - - let (event_tx, event_rx) = channel(); - let windows: Arc>> = Default::default(); - - #[cfg(target_os = "macos")] - let (_sandbox, _loader) = { - let is_helper = is_cef_helper_process(); - - #[cfg(feature = "sandbox")] - let sandbox = if is_helper { - let mut sandbox = cef::sandbox::Sandbox::new(); - sandbox.initialize(args.as_main_args()); - Some(sandbox) - } else { - None - }; - #[cfg(not(feature = "sandbox"))] - let sandbox = (); - - let loader = - cef::library_loader::LibraryLoader::new(&std::env::current_exe().unwrap(), is_helper); - assert!(loader.load()); - - if !is_helper { - let event_tx_ = event_tx.clone(); - let windows_ = windows.clone(); - init_ns_app(Box::new(move |event| match event { - AppDelegateEvent::ShouldTerminate { tx } => { - // Cancel macOS termination — we handle shutdown ourselves. - // - // Start closing all browsers (including devtools). The actual - // destruction is async and completes via the CEF message loop. - // - // Signal the main loop to exit. The post-loop safety net will - // pump the message loop until all browsers are fully destroyed - // before calling cef::shutdown(). - - tx.send(objc2_app_kit::NSApplicationTerminateReply::TerminateCancel) - .unwrap(); - event_tx_.send(RunEvent::Exit).unwrap(); - } - AppDelegateEvent::OpenURLs { urls } => { - event_tx_.send(RunEvent::Opened { urls }).unwrap(); - } - })); - } - - (sandbox, loader) - }; - - let _ = cef::api_hash(cef::sys::CEF_API_VERSION_LAST, 0); - - let cache_base = dirs::cache_dir().unwrap_or_else(std::env::temp_dir); - let cache_path = cache_base.join(&runtime_args.identifier).join("cef"); - - // Ensure the cache directory exists - let _ = create_dir_all(&cache_path); - - let event_tx_ = event_tx.clone(); - let cef_context = cef_impl::Context { - windows: windows.clone(), - callback: Arc::new(RefCell::new(Box::new(move |event| { - event_tx_.send(event).unwrap(); - }))), - next_webview_event_id: Default::default(), - next_webview_id: Default::default(), - next_window_id: Default::default(), - next_window_event_id: Default::default(), - scheme_handler_registry: Default::default(), - }; - - let mut command_line_args = Vec::new(); - let mut deep_link_schemes = Vec::new(); - for arg in runtime_args.platform_specific_attributes { - match arg { - RuntimeInitAttribute::CommandLineArgs { args } => command_line_args.extend(args), - RuntimeInitAttribute::DeepLinkSchemes { schemes } => deep_link_schemes.extend(schemes), - } - } - command_line_args.push(("--enable-media-stream".to_string(), None)); - - let mut app = cef_impl::TauriApp::new( - cef_context.clone(), - runtime_args.custom_schemes, - deep_link_schemes, - command_line_args, - ); - - let cmd = args.as_cmd_line().unwrap(); - let switch = CefString::from("type"); - let is_browser_process = cmd.has_switch(Some(&switch)) != 1; - - let ret = cef::execute_process( - Some(args.as_main_args()), - Some(&mut app), - std::ptr::null_mut(), - ); - - if is_browser_process { - assert!(ret == -1, "cannot execute browser process"); - } else { - assert!(ret >= 0, "cannot execute non-browser process"); - // non-browser process does not initialize cef - std::process::exit(0); - } - - let settings = cef::Settings { - no_sandbox: !cfg!(feature = "sandbox") as i32, - cache_path: cache_path.to_string_lossy().to_string().as_str().into(), - ..Default::default() - }; - assert_eq!( - cef::initialize( - Some(args.as_main_args()), - Some(&settings), - Some(&mut app), - std::ptr::null_mut() - ), - 1 - ); - - let main_thread_id = thread::current().id(); - let context = RuntimeContext { - main_thread_task_runner: cef::task_runner_get_for_current_thread().expect("null task runner"), - main_thread_id, - cef_context, - }; - Self { - context, - event_tx, - event_rx, - } - } -} - -/// Helper function for non-browser CEF processes (renderer, GPU, plugin, etc.). -/// This should be called from the entry point macro when the process is not the browser process. -pub fn run_cef_helper_process() { - let args = cef::args::Args::new(); - - #[cfg(all(target_os = "macos", feature = "sandbox"))] - let _sandbox = { - let mut sandbox = cef::sandbox::Sandbox::new(); - sandbox.initialize(args.as_main_args()); - sandbox - }; - - #[cfg(target_os = "macos")] - let _loader = { - let loader = cef::library_loader::LibraryLoader::new(&std::env::current_exe().unwrap(), true); - assert!(loader.load()); - loader - }; - - cef::execute_process( - Some(args.as_main_args()), - None::<&mut cef::App>, - std::ptr::null_mut(), - ); -} - -/// Platform-specific runtime init attributes. -pub enum RuntimeInitAttribute { - /// Command line arguments passed to CEF. - CommandLineArgs { args: Vec<(String, Option)> }, - /// Deep link schemes. - DeepLinkSchemes { schemes: Vec }, -} - -impl InitAttribute for RuntimeInitAttribute { - fn new(config: &tauri_utils::config::Config) -> Result> { - let mut attrs = Vec::new(); - if let Some(plugin_config) = config - .plugins - .0 - .get("deep-link") - .and_then(|c| c.get("desktop").cloned()) - { - #[derive(serde::Deserialize)] - #[serde(untagged)] - enum DesktopDeepLinks { - One(tauri_utils::config::DeepLinkProtocol), - List(Vec), - } - - let protocols: DesktopDeepLinks = - serde_json::from_value(plugin_config).map_err(tauri_runtime::Error::Json)?; - let schemes = match protocols { - DesktopDeepLinks::One(p) => p.schemes, - DesktopDeepLinks::List(p) => p.into_iter().flat_map(|p| p.schemes).collect(), - }; - - attrs.push(RuntimeInitAttribute::DeepLinkSchemes { schemes }); - } - Ok(attrs) - } -} - -/// Webview attributes. -pub enum WebviewAtribute { - /// Sets the browser runtime style. - RuntimeStyle { style: RuntimeStyle }, -} - -/// The browser runtime style. -#[derive(Clone, Copy)] -pub enum RuntimeStyle { - /// Alloy runtime. - /// - /// Used by default on multiwebview mode. - Alloy, - /// Chrome runtime. - /// - /// Used by default on webview window mode. - /// - /// Only a single browser view can use the Chrome runtime in a given window. - Chrome, -} - -#[derive(Debug)] -pub struct NewWindowOpener {} - -impl Runtime for CefRuntime { - type WindowDispatcher = CefWindowDispatcher; - type WebviewDispatcher = CefWebviewDispatcher; - type Handle = CefRuntimeHandle; - type EventLoopProxy = EventProxy; - type PlatformSpecificWebviewAttribute = WebviewAtribute; - type PlatformSpecificInitAttribute = RuntimeInitAttribute; - type WindowOpener = NewWindowOpener; - - fn new(args: RuntimeInitArgs) -> Result { - Ok(Self::init(args)) - } - - #[cfg(any(windows, target_os = "linux"))] - fn new_any_thread(args: RuntimeInitArgs) -> Result { - Ok(Self::init(args)) - } - - fn create_proxy(&self) -> Self::EventLoopProxy { - EventProxy { - context: self.context.clone(), - } - } - - fn handle(&self) -> Self::Handle { - CefRuntimeHandle { - context: self.context.clone(), - } - } - - fn create_window) + Send + 'static>( - &self, - pending: PendingWindow, - _after_window_creation: Option, - ) -> Result> { - let label = pending.label.clone(); - let window_id = self.context.cef_context.next_window_id(); - let (webview_id, use_https_scheme, devtools) = pending - .webview - .as_ref() - .map(|w| { - ( - Some(self.context.cef_context.next_webview_id()), - w.webview_attributes.use_https_scheme, - w.webview_attributes.devtools, - ) - }) - .unwrap_or((None, false, None)); - - cef_impl::create_window( - &self.context.cef_context, - window_id, - webview_id.unwrap_or_default(), - pending, - ); - - let dispatcher = CefWindowDispatcher { - window_id, - context: self.context.clone(), - }; - - let detached_webview = webview_id.map(|id| { - let webview = DetachedWebview { - label: label.clone(), - dispatcher: CefWebviewDispatcher { - window_id: Arc::new(Mutex::new(window_id)), - webview_id: id, - context: self.context.clone(), - }, - }; - DetachedWindowWebview { - webview, - use_https_scheme, - devtools, - } - }); - - Ok(DetachedWindow { - id: window_id, - label, - dispatcher, - webview: detached_webview, - }) - } - - fn create_webview( - &self, - window_id: WindowId, - pending: PendingWebview, - ) -> Result> { - let label = pending.label.clone(); - let webview_id = self.context.cef_context.next_webview_id(); - - cef_impl::create_webview( - cef_impl::WebviewKind::WindowChild, - &self.context.cef_context, - window_id, - webview_id, - pending, - ); - - let dispatcher = CefWebviewDispatcher { - window_id: Arc::new(Mutex::new(window_id)), - webview_id, - context: self.context.clone(), - }; - - Ok(DetachedWebview { label, dispatcher }) - } - - fn primary_monitor(&self) -> Option { - crate::cef_impl::get_primary_monitor() - } - - fn monitor_from_point(&self, x: f64, y: f64) -> Option { - crate::cef_impl::get_monitor_from_point(x, y) - } - - fn available_monitors(&self) -> Vec { - crate::cef_impl::get_available_monitors() - } - - fn set_theme(&self, _theme: Option) {} - - #[cfg(target_os = "macos")] - fn set_activation_policy(&mut self, _activation_policy: tauri_runtime::ActivationPolicy) {} - - #[cfg(target_os = "macos")] - fn set_dock_visibility(&mut self, _visible: bool) {} - - #[cfg(target_os = "macos")] - fn show(&self) { - cef_impl::set_application_visibility(true); - } - - #[cfg(target_os = "macos")] - fn hide(&self) { - cef_impl::set_application_visibility(false); - } - - fn set_device_event_filter(&mut self, _filter: DeviceEventFilter) {} - - fn custom_scheme_url(scheme: &str, https: bool) -> String { - // CEF always uses http/https format regardless of platform - format!( - "{}://{scheme}.localhost", - if https { "https" } else { "http" } - ) - } - - #[cfg(any( - target_os = "macos", - windows, - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" - ))] - fn run_iteration)>(&mut self, _callback: F) {} - - fn run_return) + 'static>(self, _callback: F) -> i32 { - 0 - } - - fn run) + 'static>(self, callback: F) { - let callback = Arc::new(RefCell::new(callback)); - let callback_ = callback.clone(); - let event_tx_ = self.event_tx.clone(); - let _ = std::mem::replace( - &mut *self.context.cef_context.callback.borrow_mut(), - Box::new(move |event| { - if let RunEvent::Exit = event { - // notify the event loop to exit - let _ = event_tx_.send(RunEvent::Exit); - } else { - // Try to call callback directly, if busy queue to channel - if let Ok(mut cb) = callback.try_borrow_mut() { - cb(event); - } else { - let _ = event_tx_.send(event); - } - } - }), - ); - - 'main_loop: loop { - while let Ok(event) = self.event_rx.try_recv() { - if matches!(&event, RunEvent::Exit) { - // Exit event is triggered when we break out of the loop - break 'main_loop; - } - - (self.context.cef_context.callback.borrow())(event); - } - - // Do CEF message loop work - // This processes one iteration of the message loop - cef::do_message_loop_work(); - - // Emit MainEventsCleared event - (self.context.cef_context.callback.borrow())(RunEvent::MainEventsCleared); - } - - // We need to run the message loop until all windows are closed. Otherwise, we run into use after free crashes. - cef_impl::close_all_windows(&self.context.cef_context.windows); - while !self.context.cef_context.windows.borrow().is_empty() { - cef::do_message_loop_work(); - } - - cef::shutdown(); - - // Final Exit event - // use callback_ directly because cef_context.callback posts Exit events to the event loop rx - (callback_.borrow_mut())(RunEvent::Exit); - } - - fn cursor_position(&self) -> Result> { - Ok(PhysicalPosition::new(0.0, 0.0)) - } -} - -#[cfg(target_os = "macos")] -fn init_ns_app(on_event: Box) { - use objc2::{ClassType, MainThreadMarker, msg_send, rc::Retained, runtime::NSObjectProtocol}; - use objc2_app_kit::{NSApp, NSApplication}; - - use application::{AppDelegate, SimpleApplication}; - - let mtm = MainThreadMarker::new().unwrap(); - - unsafe { - // Initialize the SimpleApplication instance. - // SAFETY: mtm ensures that here is the main thread. - - use objc2::runtime::ProtocolObject; - - let app: Retained = msg_send![SimpleApplication::class(), sharedApplication]; - let delegate = AppDelegate::new(mtm, on_event); - let proto_delegate = ProtocolObject::from_ref(&*delegate); - app.setDelegate(Some(proto_delegate)); - } - - // If there was an invocation to NSApp prior to here, - // then the NSApp will not be a SimpleApplication. - // The following assertion ensures that this doesn't happen. - assert!(NSApp(mtm).isKindOfClass(SimpleApplication::class())); -} - -#[cfg(target_os = "macos")] -mod application { - use std::{cell::Cell, sync::mpsc::channel}; - - use cef::application_mac::{CefAppProtocol, CrAppControlProtocol, CrAppProtocol}; - use objc2::{ - DefinedClass, MainThreadMarker, MainThreadOnly, define_class, msg_send, - rc::Retained, - runtime::{Bool, NSObject, NSObjectProtocol}, - }; - use objc2_app_kit::{NSApplication, NSApplicationDelegate, NSApplicationTerminateReply}; - use objc2_foundation::{NSArray, NSURL}; - - pub enum AppDelegateEvent { - ShouldTerminate { - tx: std::sync::mpsc::Sender, - }, - OpenURLs { - urls: Vec, - }, - } - - pub struct CefAppDelegateIvars { - pub on_event: Box, - } - - define_class!( - #[unsafe(super(NSObject))] - #[name = "CefAppDelegate"] - #[ivars = CefAppDelegateIvars] - #[thread_kind = MainThreadOnly] - pub struct AppDelegate; - - unsafe impl NSObjectProtocol for AppDelegate {} - - #[allow(non_snake_case)] - unsafe impl NSApplicationDelegate for AppDelegate { - #[unsafe(method(application:openURLs:))] - unsafe fn application_openURLs(&self, _application: &NSApplication, urls: &NSArray) { - let converted_urls: Vec = urls - .iter() - .filter_map(|ns_url| unsafe { - ns_url - .absoluteString() - .and_then(|url_string| url_string.to_string().parse().ok()) - }) - .collect(); - - let handler = &self.ivars().on_event; - handler(AppDelegateEvent::OpenURLs { - urls: converted_urls, - }); - } - - #[unsafe(method(applicationShouldTerminate:))] - unsafe fn applicationShouldTerminate( - &self, - _sender: &NSApplication, - ) -> NSApplicationTerminateReply { - let (tx, rx) = channel(); - let handler = &self.ivars().on_event; - handler(AppDelegateEvent::ShouldTerminate { tx }); - rx.try_recv() - .unwrap_or(NSApplicationTerminateReply::TerminateNow) - } - } - ); - - impl AppDelegate { - pub fn new(mtm: MainThreadMarker, on_event: Box) -> Retained { - let delegate = Self::alloc(mtm).set_ivars(CefAppDelegateIvars { on_event }); - let delegate: Retained = unsafe { msg_send![super(delegate), init] }; - delegate - } - } - - /// Instance variables of `SimpleApplication`. - pub struct SimpleApplicationIvars { - handling_send_event: Cell, - } - - define_class!( - /// A `NSApplication` subclass that implements the required CEF protocols. - /// - /// This class provides the necessary `CefAppProtocol` conformance to - /// ensure that events are handled correctly by the Chromium framework on macOS. - #[unsafe(super(NSApplication))] - #[ivars = SimpleApplicationIvars] - pub struct SimpleApplication; - - unsafe impl CrAppControlProtocol for SimpleApplication { - #[unsafe(method(setHandlingSendEvent:))] - unsafe fn set_handling_send_event(&self, handling_send_event: Bool) { - self.ivars().handling_send_event.set(handling_send_event); - } - } - - unsafe impl CrAppProtocol for SimpleApplication { - #[unsafe(method(isHandlingSendEvent))] - unsafe fn is_handling_send_event(&self) -> Bool { - self.ivars().handling_send_event.get() - } - } - - unsafe impl CefAppProtocol for SimpleApplication {} - ); -} +mod external_message_pump; +mod platform; +mod runtime; +mod webview; +mod window; +mod window_builder; + +pub use runtime::*; +pub use webview::*; +pub use window::CefWindowDispatcher; +pub use window_builder::WindowBuilderWrapper; diff --git a/crates/tauri-runtime-cef/src/platform/linux/event_loop.rs b/crates/tauri-runtime-cef/src/platform/linux/event_loop.rs new file mode 100644 index 000000000000..2664b0412ad8 --- /dev/null +++ b/crates/tauri-runtime-cef/src/platform/linux/event_loop.rs @@ -0,0 +1,49 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::os::raw::{c_uint, c_ulong}; +use tauri_runtime::{Error, Result, dpi::PhysicalPosition}; +use winit::event_loop::ActiveEventLoop; + +use crate::platform::EventLoopExt; + +use super::{taskbar, utils::with_x11}; + +impl EventLoopExt for dyn ActiveEventLoop + '_ { + fn set_badge_count(&self, count: Option, desktop_filename: Option) { + taskbar::set_badge_count(count, desktop_filename); + } + + fn set_badge_label(&self, _label: Option) { + // Unsupported on Linux/BSD + } + + fn cursor_position(&self) -> Result> { + with_x11(None, |xlib, display| unsafe { + let root = (xlib.XDefaultRootWindow)(display); + let mut root_return: c_ulong = 0; + let mut child_return: c_ulong = 0; + let mut root_x = 0; + let mut root_y = 0; + let mut win_x = 0; + let mut win_y = 0; + let mut mask: c_uint = 0; + + let ok = (xlib.XQueryPointer)( + display, + root, + &mut root_return, + &mut child_return, + &mut root_x, + &mut root_y, + &mut win_x, + &mut win_y, + &mut mask, + ); + + (ok != 0).then_some(PhysicalPosition::new(root_x as f64, root_y as f64)) + }) + .ok_or(Error::FailedToGetCursorPosition) + } +} diff --git a/crates/tauri-runtime-cef/src/platform/linux/mod.rs b/crates/tauri-runtime-cef/src/platform/linux/mod.rs new file mode 100644 index 000000000000..f49c04e60076 --- /dev/null +++ b/crates/tauri-runtime-cef/src/platform/linux/mod.rs @@ -0,0 +1,10 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +mod event_loop; +mod monitor; +mod taskbar; +mod utils; +mod webview; +mod window; diff --git a/crates/tauri-runtime-cef/src/platform/linux/monitor.rs b/crates/tauri-runtime-cef/src/platform/linux/monitor.rs new file mode 100644 index 000000000000..c7f1c31e6d8a --- /dev/null +++ b/crates/tauri-runtime-cef/src/platform/linux/monitor.rs @@ -0,0 +1,133 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::os::raw::{c_int, c_long, c_uchar, c_ulong}; +use tauri_runtime::dpi::{PhysicalPosition, PhysicalRect, PhysicalSize}; +use winit::monitor::MonitorHandle; +use x11_dl::xlib; + +use crate::platform::{MonitorExt, monitor_bounds}; + +use super::utils::{atom, with_x11}; + +impl MonitorExt for MonitorHandle { + fn work_area(&self) -> PhysicalRect { + let bounds = monitor_bounds(self); + x11_work_area(bounds).unwrap_or(bounds) + } +} + +fn x11_work_area(monitor_bounds: PhysicalRect) -> Option> { + with_x11(None, |xlib, display| unsafe { + let root = (xlib.XDefaultRootWindow)(display); + let workareas = get_cardinal_property(xlib, display, root, "_NET_WORKAREA")?; + + let desktop = get_cardinal_property(xlib, display, root, "_NET_CURRENT_DESKTOP") + .and_then(|desktops| desktops.first().copied()) + .and_then(|desktop| usize::try_from(desktop).ok()) + .unwrap_or(0); + + let offset = desktop.checked_mul(4)?; + let workarea = workareas + .get(offset..offset + 4) + .or_else(|| workareas.get(0..4))?; + + let workarea = PhysicalRect { + position: PhysicalPosition::new( + i32::try_from(workarea[0]).ok()?, + i32::try_from(workarea[1]).ok()?, + ), + size: PhysicalSize::new( + u32::try_from(workarea[2]).ok()?, + u32::try_from(workarea[3]).ok()?, + ), + }; + + intersect_rects(monitor_bounds, workarea) + }) +} + +fn get_cardinal_property( + xlib: &xlib::Xlib, + display: *mut xlib::Display, + window: xlib::Window, + name: &str, +) -> Option> { + let property = atom(xlib, display, name); + if property == 0 { + return None; + } + + let mut actual_type: c_ulong = 0; + let mut actual_format: c_int = 0; + let mut nitems: c_ulong = 0; + let mut bytes_after: c_ulong = 0; + let mut data: *mut c_uchar = std::ptr::null_mut(); + + let status = unsafe { + (xlib.XGetWindowProperty)( + display, + window, + property, + 0, + 256, + 0, + xlib::XA_CARDINAL, + &mut actual_type, + &mut actual_format, + &mut nitems, + &mut bytes_after, + &mut data, + ) + }; + + if status != xlib::Success as c_int || data.is_null() { + return None; + } + + let value = if actual_type == xlib::XA_CARDINAL && actual_format == 32 && bytes_after == 0 { + let values = unsafe { std::slice::from_raw_parts(data.cast::(), nitems as usize) }; + Some(values.to_vec()) + } else { + None + }; + + unsafe { + (xlib.XFree)(data.cast()); + } + value +} + +fn intersect_rects( + a: PhysicalRect, + b: PhysicalRect, +) -> Option> { + let left = a.position.x.max(b.position.x); + let top = a.position.y.max(b.position.y); + let right = rect_right(a).min(rect_right(b)); + let bottom = rect_bottom(a).min(rect_bottom(b)); + + if right <= left || bottom <= top { + return None; + } + + Some(PhysicalRect { + position: PhysicalPosition::new(left, top), + size: PhysicalSize::new((right - left) as u32, (bottom - top) as u32), + }) +} + +fn rect_right(rect: PhysicalRect) -> i32 { + rect + .position + .x + .saturating_add(i32::try_from(rect.size.width).unwrap_or(i32::MAX)) +} + +fn rect_bottom(rect: PhysicalRect) -> i32 { + rect + .position + .y + .saturating_add(i32::try_from(rect.size.height).unwrap_or(i32::MAX)) +} diff --git a/crates/tauri-runtime-cef/src/platform/linux/taskbar.rs b/crates/tauri-runtime-cef/src/platform/linux/taskbar.rs new file mode 100644 index 000000000000..f5ff83505059 --- /dev/null +++ b/crates/tauri-runtime-cef/src/platform/linux/taskbar.rs @@ -0,0 +1,178 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use dlopen2::wrapper::{Container, WrapperApi}; +use std::{cell::RefCell, ffi::CString, os::raw::c_char}; +use tauri_runtime::{ProgressBarState, ProgressBarStatus}; + +pub(super) fn set_progress_bar(state: ProgressBarState) { + TASKBAR_INDICATOR.with_borrow_mut(|taskbar| taskbar.update(state)); +} + +pub(super) fn set_badge_count(count: Option, desktop_filename: Option) { + TASKBAR_INDICATOR.with_borrow_mut(|taskbar| taskbar.update_count(count, desktop_filename)); +} + +#[derive(WrapperApi)] +struct UnityLib { + unity_launcher_entry_get_for_desktop_id: unsafe extern "C" fn(id: *const c_char) -> *const isize, + unity_inspector_get_default: unsafe extern "C" fn() -> *const isize, + unity_inspector_get_unity_running: unsafe extern "C" fn(inspector: *const isize) -> i32, + unity_launcher_entry_set_progress: unsafe extern "C" fn(entry: *const isize, value: f64) -> i32, + unity_launcher_entry_set_progress_visible: + unsafe extern "C" fn(entry: *const isize, value: i32) -> i32, + unity_launcher_entry_set_count: unsafe extern "C" fn(entry: *const isize, value: i64) -> i32, + unity_launcher_entry_set_count_visible: + unsafe extern "C" fn(entry: *const isize, value: bool) -> bool, +} + +struct TaskbarIndicator { + desktop_filename: Option, + desktop_filename_c_str: Option, + + unity_lib: Option>, + attempted_load: bool, + + unity_inspector: Option<*const isize>, + unity_entry: Option<*const isize>, +} + +thread_local! { + static TASKBAR_INDICATOR: RefCell = RefCell::new(TaskbarIndicator::new()); +} + +impl TaskbarIndicator { + fn new() -> Self { + Self { + desktop_filename: None, + desktop_filename_c_str: None, + + unity_lib: None, + attempted_load: false, + + unity_inspector: None, + unity_entry: None, + } + } + + fn ensure_lib_load(&mut self) { + if self.attempted_load { + return; + } + + self.attempted_load = true; + + self.unity_lib = unsafe { + Container::load("libunity.so.4") + .or_else(|_| Container::load("libunity.so.6")) + .or_else(|_| Container::load("libunity.so.9")) + .ok() + }; + + if let Some(unity_lib) = &self.unity_lib { + let handle = unsafe { unity_lib.unity_inspector_get_default() }; + if !handle.is_null() { + self.unity_inspector = Some(handle); + } + } + } + + fn ensure_entry_load(&mut self) { + if let Some(unity_lib) = &self.unity_lib + && let Some(id) = &self.desktop_filename_c_str + { + let handle = unsafe { unity_lib.unity_launcher_entry_get_for_desktop_id(id.as_ptr()) }; + if !handle.is_null() { + self.unity_entry = Some(handle); + } + } + } + + fn is_unity_running(&self) -> bool { + if let Some(inspector) = self.unity_inspector + && let Some(unity_lib) = &self.unity_lib + { + return unsafe { unity_lib.unity_inspector_get_unity_running(inspector) } == 1; + } + + false + } + + fn update(&mut self, progress: ProgressBarState) { + if let Some(uri) = progress.desktop_filename { + self.desktop_filename = Some(uri); + self.desktop_filename_c_str = None; + self.unity_entry = None; + } + + self.ensure_lib_load(); + + if !self.is_unity_running() { + return; + } + + if self.desktop_filename_c_str.is_none() + && let Some(uri) = &self.desktop_filename + { + self.desktop_filename_c_str = Some(CString::new(uri.as_str()).unwrap_or_default()); + } + + if self.unity_entry.is_none() { + self.ensure_entry_load(); + } + + if let Some(unity_lib) = &self.unity_lib + && let Some(unity_entry) = self.unity_entry + { + if let Some(progress) = progress.progress { + let progress = progress.min(100) as f64 / 100.0; + unsafe { unity_lib.unity_launcher_entry_set_progress(unity_entry, progress) }; + } + + if let Some(status) = progress.status { + let is_visible = !matches!(status, ProgressBarStatus::None); + unsafe { + unity_lib + .unity_launcher_entry_set_progress_visible(unity_entry, if is_visible { 1 } else { 0 }) + }; + } + } + } + + fn update_count(&mut self, count: Option, desktop_filename: Option) { + if let Some(uri) = desktop_filename { + self.desktop_filename = Some(uri); + self.desktop_filename_c_str = None; + self.unity_entry = None; + } + + self.ensure_lib_load(); + + if !self.is_unity_running() { + return; + } + + if self.desktop_filename_c_str.is_none() + && let Some(uri) = &self.desktop_filename + { + self.desktop_filename_c_str = Some(CString::new(uri.as_str()).unwrap_or_default()); + } + + if self.unity_entry.is_none() { + self.ensure_entry_load(); + } + + if let Some(unity_lib) = &self.unity_lib + && let Some(unity_entry) = self.unity_entry + { + if let Some(count) = count { + unsafe { unity_lib.unity_launcher_entry_set_count(unity_entry, count) }; + unsafe { unity_lib.unity_launcher_entry_set_count_visible(unity_entry, true) }; + } else { + unsafe { unity_lib.unity_launcher_entry_set_count(unity_entry, 0) }; + unsafe { unity_lib.unity_launcher_entry_set_count_visible(unity_entry, false) }; + } + } + } +} diff --git a/crates/tauri-runtime-cef/src/platform/linux/utils.rs b/crates/tauri-runtime-cef/src/platform/linux/utils.rs new file mode 100644 index 000000000000..fe9b4e6a253e --- /dev/null +++ b/crates/tauri-runtime-cef/src/platform/linux/utils.rs @@ -0,0 +1,108 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{ + cell::RefCell, + ffi::CString, + os::raw::{c_long, c_ulong}, + sync::LazyLock, +}; +use x11_dl::xlib; + +const NET_WM_STATE_REMOVE: c_long = 0; +const NET_WM_STATE_ADD: c_long = 1; +const CLIENT_MESSAGE: i32 = 33; +const SUBSTRUCTURE_REDIRECT_MASK: c_long = 1 << 20; +const SUBSTRUCTURE_NOTIFY_MASK: c_long = 1 << 19; + +static XLIB: LazyLock> = LazyLock::new(|| xlib::Xlib::open().ok()); + +struct Display(*mut xlib::Display); + +thread_local! { + static DISPLAY: RefCell> = const { RefCell::new(None) }; +} + +pub(super) fn with_cef_display( + default: R, + f: impl FnOnce(&xlib::Xlib, *mut xlib::Display) -> R, +) -> R { + let Some(xlib) = XLIB.as_ref() else { + return default; + }; + let display = cef::get_xdisplay() as *mut xlib::Display; + if display.is_null() { + return default; + } + + let result = f(xlib, display); + unsafe { + (xlib.XFlush)(display); + } + result +} + +pub(super) fn with_x11(default: R, f: impl FnOnce(&xlib::Xlib, *mut xlib::Display) -> R) -> R { + let Some(xlib) = XLIB.as_ref() else { + return default; + }; + + DISPLAY.with(|cell| { + let mut guard = cell.borrow_mut(); + if guard.is_none() { + let display = unsafe { (xlib.XOpenDisplay)(std::ptr::null()) }; + if display.is_null() { + return default; + } + *guard = Some(Display(display)); + } + + let display = guard.as_ref().unwrap().0; + let result = f(xlib, display); + unsafe { + (xlib.XFlush)(display); + } + result + }) +} + +pub(super) fn atom(xlib: &xlib::Xlib, display: *mut xlib::Display, name: &str) -> c_ulong { + let cname = CString::new(name).unwrap(); + unsafe { (xlib.XInternAtom)(display, cname.as_ptr(), 0) } +} + +pub(super) fn set_wm_state(xid: c_ulong, add: bool, atom1: &str, atom2: Option<&str>) { + with_x11((), |xlib, display| { + let wm_state = atom(xlib, display, "_NET_WM_STATE"); + let a1 = atom(xlib, display, atom1); + let a2 = atom2.map(|name| atom(xlib, display, name)).unwrap_or(0); + let action = if add { + NET_WM_STATE_ADD + } else { + NET_WM_STATE_REMOVE + }; + + unsafe { + let root = (xlib.XDefaultRootWindow)(display); + let mut event: xlib::XEvent = std::mem::zeroed(); + event.client_message = xlib::XClientMessageEvent { + type_: CLIENT_MESSAGE, + serial: 0, + send_event: 1, + display, + window: xid, + message_type: wm_state, + format: 32, + data: xlib::ClientMessageData::from([action, a1 as c_long, a2 as c_long, 1, 0]), + }; + (xlib.XSendEvent)( + display, + root, + 0, + SUBSTRUCTURE_REDIRECT_MASK | SUBSTRUCTURE_NOTIFY_MASK, + &mut event, + ); + } + }); +} diff --git a/crates/tauri-runtime-cef/src/platform/linux/webview.rs b/crates/tauri-runtime-cef/src/platform/linux/webview.rs new file mode 100644 index 000000000000..d443e3787c7c --- /dev/null +++ b/crates/tauri-runtime-cef/src/platform/linux/webview.rs @@ -0,0 +1,132 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use cef::ImplBrowserHost; +use std::os::raw::c_ulong; +use tauri_runtime::dpi::{PhysicalPosition, PhysicalSize, Rect}; +use tauri_utils::config::Color; +use x11_dl::xlib; + +use crate::{webview::AppWebview, window::AppWindow}; + +use super::utils::{atom, with_cef_display}; + +impl AppWebview { + fn xid(&self) -> Option { + let xid = self.host.window_handle(); + (xid != 0).then_some(xid as xlib::Window) + } + + pub(crate) fn set_background_color(&self, color: Option) { + let _ = (self, color); + // Native child-window background is not equivalent to Chromium's rendered + // background. Creation still applies BrowserSettings. + } + + pub(crate) fn bounds(&self) -> Option { + let xid = self.xid()?; + + with_cef_display(None, |xlib, display| unsafe { + let mut root: xlib::Window = 0; + let mut x: i32 = 0; + let mut y: i32 = 0; + let mut width: u32 = 0; + let mut height: u32 = 0; + let mut border_width: u32 = 0; + let mut depth: u32 = 0; + + if (xlib.XGetGeometry)( + display, + xid, + &mut root, + &mut x, + &mut y, + &mut width, + &mut height, + &mut border_width, + &mut depth, + ) == 0 + { + return None; + } + + Some(Rect { + position: PhysicalPosition::new(x, y).into(), + size: PhysicalSize::new(width, height).into(), + }) + }) + } + + pub(crate) fn reparent(&self, parent: &AppWindow) { + let Some(xid) = self.xid() else { + return; + }; + let parent_xid = parent.raw_handle_as_cef_handle(); + if parent_xid == 0 { + return; + } + + with_cef_display((), |xlib, display| unsafe { + (xlib.XReparentWindow)(display, xid, parent_xid as xlib::Window, 0, 0); + (xlib.XMapRaised)(display, xid); + }); + } + + pub(crate) fn apply_visible(&self, visible: bool) { + let Some(xid) = self.xid() else { + return; + }; + + with_cef_display((), |xlib, display| unsafe { + let net_wm_state = atom(xlib, display, "_NET_WM_STATE"); + const PROP_MODE_REPLACE: i32 = 0; + + if visible { + (xlib.XChangeProperty)( + display, + xid, + net_wm_state, + xlib::XA_ATOM, + 32, + PROP_MODE_REPLACE, + std::ptr::null(), + 0, + ); + (xlib.XMapWindow)(display, xid); + } else { + let hidden: [c_ulong; 1] = [atom(xlib, display, "_NET_WM_STATE_HIDDEN")]; + (xlib.XChangeProperty)( + display, + xid, + net_wm_state, + xlib::XA_ATOM, + 32, + PROP_MODE_REPLACE, + hidden.as_ptr() as *const u8, + 1, + ); + (xlib.XUnmapWindow)(display, xid); + } + }); + } + + pub(crate) fn apply_physical_bounds(&self, _scale: f64, x: i32, y: i32, width: i32, height: i32) { + let Some(xid) = self.xid() else { + return; + }; + + with_cef_display((), |xlib, display| unsafe { + (xlib.XMoveResizeWindow)( + display, + xid, + x, + y, + width.max(1) as u32, + height.max(1) as u32, + ); + // `with_cef_display` issues an `XFlush` once the closure returns, so a + // blocking `XSync` round-trip here just stalls every resize frame. + }); + } +} diff --git a/crates/tauri-runtime-cef/src/platform/linux/window.rs b/crates/tauri-runtime-cef/src/platform/linux/window.rs new file mode 100644 index 000000000000..d9ffc1025721 --- /dev/null +++ b/crates/tauri-runtime-cef/src/platform/linux/window.rs @@ -0,0 +1,55 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use raw_window_handle::{HasWindowHandle, RawWindowHandle}; +use std::os::raw::c_ulong; +use tauri_runtime::ProgressBarState; +use tauri_utils::config::Color; + +use crate::window::AppWindow; + +use super::{taskbar, utils::set_wm_state}; + +impl AppWindow { + fn xid(&self) -> Option { + let handle = self.window.window_handle().ok()?; + match handle.as_raw() { + RawWindowHandle::Xlib(handle) => Some(handle.window as c_ulong), + RawWindowHandle::Xcb(handle) => Some(handle.window.get() as c_ulong), + _ => None, + } + } + + pub(crate) fn set_enabled(&self, enabled: bool) { + let _ = (self, enabled); + // TODO: implement native window enabled state on Linux/BSD. + } + + pub(crate) fn is_enabled(&self) -> bool { + let _ = self; + // TODO: query native window enabled state on Linux/BSD. + true + } + + pub(crate) fn set_background_color(&self, color: Option) { + let _ = (self, color); + // TODO: implement native window background color on Linux/BSD. + } + + pub(crate) fn set_skip_taskbar(&self, skip: bool) { + if let Some(xid) = self.xid() { + set_wm_state(xid, skip, "_NET_WM_STATE_SKIP_TASKBAR", None); + } + } + + pub(crate) fn set_visible_on_all_workspaces(&self, visible: bool) { + if let Some(xid) = self.xid() { + set_wm_state(xid, visible, "_NET_WM_STATE_STICKY", None); + } + } + + pub(crate) fn set_progress_bar(&self, state: ProgressBarState) { + taskbar::set_progress_bar(state); + } +} diff --git a/crates/tauri-runtime-cef/src/platform/macos/application.rs b/crates/tauri-runtime-cef/src/platform/macos/application.rs new file mode 100644 index 000000000000..f40c09464dfc --- /dev/null +++ b/crates/tauri-runtime-cef/src/platform/macos/application.rs @@ -0,0 +1,243 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{ + cell::Cell, + time::{Duration, Instant}, +}; + +use cef::application_mac::{CefAppProtocol, CrAppControlProtocol, CrAppProtocol}; +use objc2::{ + ClassType, DefinedClass, MainThreadMarker, MainThreadOnly, define_class, extern_methods, + msg_send, + rc::Retained, + runtime::{AnyObject, Bool, ProtocolObject}, +}; +use objc2_app_kit::{ + NSApp, NSApplication, NSApplicationActivationOptions, NSApplicationDelegate, + NSApplicationTerminateReply, NSEvent, NSRunningApplication, +}; +use objc2_application_services::kProcessTransformToForegroundApplication; +use objc2_foundation::{NSArray, NSObject, NSObjectProtocol, NSString, NSURL}; + +use super::utils; + +#[derive(Default)] +pub(crate) struct CefWinitApplicationIvars { + handling_send_event: Cell, + last_dock_show_ms: Cell, + delegate: Cell<*const AppDelegate>, +} + +pub(crate) enum AppDelegateEvent { + TryTerminate, + Reopen { has_visible_windows: bool }, + AccessibilityChanged { enabled: bool }, + OpenURLs { urls: Vec }, +} + +pub(crate) struct CefAppDelegateIvars { + on_event: Box, +} + +define_class!( + #[unsafe(super(NSObject))] + #[name = "CefWinitAppDelegate"] + #[ivars = CefAppDelegateIvars] + #[thread_kind = MainThreadOnly] + pub(crate) struct AppDelegate; + + unsafe impl NSObjectProtocol for AppDelegate {} + + #[allow(non_snake_case)] + unsafe impl NSApplicationDelegate for AppDelegate { + #[unsafe(method(application:openURLs:))] + fn application_openURLs(&self, _application: &NSApplication, urls: &NSArray) { + let urls = urls + .iter() + .filter_map(|ns_url| { + ns_url + .absoluteString() + .and_then(|url_string| url_string.to_string().parse().ok()) + }) + .collect(); + + self.emit(AppDelegateEvent::OpenURLs { urls }); + } + + #[unsafe(method(applicationShouldTerminate:))] + fn applicationShouldTerminate(&self, _sender: &NSApplication) -> NSApplicationTerminateReply { + NSApplicationTerminateReply::TerminateNow + } + + #[unsafe(method(applicationShouldHandleReopen:hasVisibleWindows:))] + fn applicationShouldHandleReopen_hasVisibleWindows( + &self, + _sender: &NSApplication, + has_visible_windows: bool, + ) -> bool { + self.emit(AppDelegateEvent::Reopen { + has_visible_windows, + }); + false + } + + #[unsafe(method(applicationSupportsSecureRestorableState:))] + fn applicationSupportsSecureRestorableState(&self, _app: &NSApplication) -> bool { + true + } + } + + impl AppDelegate { + #[unsafe(method(tryToTerminateApplication:))] + fn try_to_terminate_application(&self, _app: &NSApplication) { + self.emit(AppDelegateEvent::TryTerminate); + } + + #[unsafe(method(enableAccessibility:))] + fn enable_accessibility(&self, enabled: Bool) { + self.emit(AppDelegateEvent::AccessibilityChanged { + enabled: enabled.as_bool(), + }); + } + } +); + +define_class!( + #[unsafe(super(NSApplication))] + #[ivars = CefWinitApplicationIvars] + pub(crate) struct CefWinitApplication; + + impl CefWinitApplication { + #[unsafe(method(sendEvent:))] + unsafe fn send_event(&self, event: &NSEvent) { + let was_handling = self.ivars().handling_send_event.get(); + self.ivars().handling_send_event.set(Bool::YES); + let _: () = unsafe { msg_send![super(self), sendEvent: event] }; + self.ivars().handling_send_event.set(was_handling); + } + + #[unsafe(method(tauriTransformProcessToForeground))] + fn transform_process_to_foreground(&self) { + utils::transform_process_type(kProcessTransformToForegroundApplication); + } + + #[unsafe(method(tauriActivateCurrentApplication))] + fn activate_current_application(&self) { + let app = NSRunningApplication::currentApplication(); + #[allow(deprecated)] + app.activateWithOptions(NSApplicationActivationOptions::ActivateIgnoringOtherApps); + } + + #[unsafe(method(terminate:))] + unsafe fn terminate(&self, _sender: Option<&AnyObject>) { + if let Some(delegate) = self.delegate() { + delegate.emit(AppDelegateEvent::TryTerminate); + } + } + + #[unsafe(method(accessibilitySetValue:forAttribute:))] + unsafe fn accessibility_set_value_for_attribute( + &self, + value: Option<&AnyObject>, + attribute: Option<&NSString>, + ) { + if let (Some(value), Some(attribute)) = (value, attribute) { + if attribute.to_string() == "AXEnhancedUserInterface" { + let int_value: std::ffi::c_int = unsafe { msg_send![value, intValue] }; + if let Some(delegate) = self.delegate() { + delegate.emit(AppDelegateEvent::AccessibilityChanged { + enabled: int_value == 1, + }); + } + } + } + + let _: () = unsafe { + msg_send![super(self), accessibilitySetValue: value, forAttribute: attribute] + }; + } + } + + unsafe impl CrAppControlProtocol for CefWinitApplication { + #[unsafe(method(setHandlingSendEvent:))] + unsafe fn set_handling_send_event(&self, handling_send_event: Bool) { + self.ivars().handling_send_event.set(handling_send_event); + } + } + + unsafe impl CrAppProtocol for CefWinitApplication { + #[unsafe(method(isHandlingSendEvent))] + unsafe fn is_handling_send_event(&self) -> Bool { + self.ivars().handling_send_event.get() + } + } + + unsafe impl CefAppProtocol for CefWinitApplication {} +); + +impl AppDelegate { + fn new(mtm: MainThreadMarker, on_event: Box) -> Retained { + let this = Self::alloc(mtm).set_ivars(CefAppDelegateIvars { on_event }); + unsafe { msg_send![super(this), init] } + } + + fn emit(&self, event: AppDelegateEvent) { + (self.ivars().on_event)(event); + } +} + +impl CefWinitApplication { + extern_methods! { + #[unsafe(method(sharedApplication))] + pub fn shared_application() -> Retained; + } + + pub fn last_dock_show(&self) -> Option { + match self.ivars().last_dock_show_ms.get() { + 0 => None, + // Store elapsed milliseconds + 1 so the zero-initialized ivar can mean + // "not set" for AppKit-created NSApplication instances. + milliseconds => Some(utils::instant_epoch() + Duration::from_millis(milliseconds - 1)), + } + } + + pub fn set_last_dock_show(&self, instant: Instant) { + let milliseconds = instant + .saturating_duration_since(utils::instant_epoch()) + .as_millis() + .try_into() + .unwrap_or(u64::MAX); + self + .ivars() + .last_dock_show_ms + // Offset by one so `0` remains the zero-initialized "not set" sentinel. + .set(milliseconds.saturating_add(1)); + } + + fn delegate(&self) -> Option<&AppDelegate> { + unsafe { self.ivars().delegate.get().as_ref() } + } +} + +pub fn setup_application() { + let _ = CefWinitApplication::shared_application(); + let mtm = MainThreadMarker::new().expect("macOS application must start on the main thread"); + assert!(NSApp(mtm).isKindOfClass(CefWinitApplication::class())); +} + +pub(crate) fn set_application_event_handler( + on_event: Box, +) -> Retained { + let mtm = MainThreadMarker::new().expect("macOS application must start on the main thread"); + let delegate = AppDelegate::new(mtm, on_event); + let app = CefWinitApplication::shared_application(); + + // `NSApplication.delegate` is weak. The runtime owns the retained delegate; + // the app stores only a non-owning pointer because AppKit creates the + // NSApplication singleton and its ivars must stay zero-initialized/no-drop. + app.ivars().delegate.set(&*delegate as *const AppDelegate); + app.setDelegate(Some(ProtocolObject::from_ref(&*delegate))); + delegate +} diff --git a/crates/tauri-runtime-cef/src/platform/macos/dock.rs b/crates/tauri-runtime-cef/src/platform/macos/dock.rs new file mode 100644 index 000000000000..bb0770954de2 --- /dev/null +++ b/crates/tauri-runtime-cef/src/platform/macos/dock.rs @@ -0,0 +1,93 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::time::{Duration, Instant}; + +use objc2::{msg_send, runtime::AnyObject, sel}; +use objc2_app_kit::{NSApplicationActivationOptions, NSRunningApplication}; +use objc2_application_services::{ + kProcessTransformToForegroundApplication, kProcessTransformToUIElementApplication, +}; +use objc2_foundation::NSString; + +use super::{application::CefWinitApplication, utils}; + +const DOCK_SHOW_TIMEOUT: Duration = Duration::from_secs(1); +const DOCK_BUNDLE_IDENTIFIER: &str = "com.apple.dock"; + +impl CefWinitApplication { + pub fn set_dock_visibility(&self, visible: bool) { + if visible { + self.set_dock_show(); + } else { + self.set_dock_hide(); + } + } + + fn set_dock_hide(&self) { + let now = Instant::now(); + if let Some(last_dock_show_time) = self.last_dock_show() { + // TransformProcessType from UIElement back to foreground is asynchronous + // and does not expose a completion signal. Electron found that rapid + // hide/show cycles can race the macOS Dock and leave duplicate app icons + // behind, so it ignores hide requests immediately after showing. + // https://github.com/electron/electron/blob/88cd4b418618424fbcd11917fffee489f534ad72/shell/browser/browser_mac.mm#L2376-L2408 + if now.duration_since(last_dock_show_time) < DOCK_SHOW_TIMEOUT { + return; + } + } + + self.set_windows_can_hide(false); + utils::transform_process_type(kProcessTransformToUIElementApplication); + } + + fn set_dock_show(&self) { + self.set_last_dock_show(Instant::now()); + self.set_windows_can_hide(true); + + if NSRunningApplication::currentApplication().isActive() { + // TransformProcessType is buggy when bringing an active UIElement app + // back to foreground. Electron works around it by activating Dock first, + // then delaying the foreground transform and app reactivation: + // https://github.com/electron/electron/blob/88cd4b418618424fbcd11917fffee489f534ad72/shell/browser/browser_mac.mm#L2424-L2475 + activate_dock(); + self.perform_delayed_dock_show(); + } else { + utils::transform_process_type(kProcessTransformToForegroundApplication); + } + } + + fn set_windows_can_hide(&self, can_hide: bool) { + let windows = self.windows(); + for idx in 0..windows.count() { + windows.objectAtIndex(idx).setCanHide(can_hide); + } + } + + fn perform_delayed_dock_show(&self) { + unsafe { + let _: () = msg_send![ + self, + performSelector: sel!(tauriTransformProcessToForeground), + withObject: None::<&AnyObject>, + afterDelay: 1.0f64, + ]; + let _: () = msg_send![ + self, + performSelector: sel!(tauriActivateCurrentApplication), + withObject: None::<&AnyObject>, + afterDelay: 2.0f64, + ]; + } + } +} + +fn activate_dock() { + let dock_id = NSString::from_str(DOCK_BUNDLE_IDENTIFIER); + let dock_apps = NSRunningApplication::runningApplicationsWithBundleIdentifier(&dock_id); + if dock_apps.count() > 0 { + let app = dock_apps.objectAtIndex(0); + app.activateWithOptions(NSApplicationActivationOptions::ActivateIgnoringOtherApps); + } +} diff --git a/crates/tauri-runtime-cef/src/platform/macos/event_loop.rs b/crates/tauri-runtime-cef/src/platform/macos/event_loop.rs new file mode 100644 index 000000000000..7b6c844f3340 --- /dev/null +++ b/crates/tauri-runtime-cef/src/platform/macos/event_loop.rs @@ -0,0 +1,98 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use objc2::MainThreadMarker; +use objc2_app_kit::{NSApp, NSApplication, NSApplicationActivationPolicy, NSEvent, NSScreen}; +use objc2_foundation::{NSPoint, NSString}; +use tauri_runtime::{ + Error, ProgressBarState, Result, + dpi::{LogicalPosition, PhysicalPosition}, +}; +use winit::event_loop::ActiveEventLoop; + +use crate::platform::EventLoopExt; + +use super::{application::CefWinitApplication, progress}; + +impl EventLoopExt for dyn ActiveEventLoop + '_ { + fn set_activation_policy(&self, policy: tauri_runtime::ActivationPolicy) { + let Some(mtm) = MainThreadMarker::new() else { + return; + }; + + let app = NSApplication::sharedApplication(mtm); + let policy = match policy { + tauri_runtime::ActivationPolicy::Regular => NSApplicationActivationPolicy::Regular, + tauri_runtime::ActivationPolicy::Accessory => NSApplicationActivationPolicy::Accessory, + tauri_runtime::ActivationPolicy::Prohibited => NSApplicationActivationPolicy::Prohibited, + _ => NSApplicationActivationPolicy::Regular, + }; + app.setActivationPolicy(policy); + } + + fn set_dock_visibility(&self, visible: bool) { + let Some(_mtm) = MainThreadMarker::new() else { + return; + }; + + let app = CefWinitApplication::shared_application(); + app.set_dock_visibility(visible); + } + + fn show_application(&self) { + let Some(mtm) = MainThreadMarker::new() else { + return; + }; + + NSApp(mtm).unhide(None); + } + + fn hide_application(&self) { + let Some(mtm) = MainThreadMarker::new() else { + return; + }; + + NSApp(mtm).hide(None); + } + + fn set_progress_bar(&self, state: ProgressBarState) { + progress::set_dock_progress_bar(state); + } + + fn set_badge_count(&self, count: Option, _desktop_filename: Option) { + self.set_badge_label(count.map(|count| count.to_string())); + } + + fn set_badge_label(&self, label: Option) { + let Some(mtm) = MainThreadMarker::new() else { + return; + }; + + let app = NSApplication::sharedApplication(mtm); + let dock_tile = app.dockTile(); + let ns_label = label.map(|label| NSString::from_str(&label)); + dock_tile.setBadgeLabel(ns_label.as_deref()); + } + + fn cursor_position(&self) -> Result> { + let Some(mtm) = MainThreadMarker::new() else { + return Err(Error::FailedToGetCursorPosition); + }; + + // `NSEvent::mouseLocation` is in global coordinates with a bottom-left + // origin, in logical points. The global origin is the bottom-left of the + // primary screen, so flip Y against the primary screen height, then scale to + // physical pixels to satisfy the trait contract (the Windows/Linux backends + // and wry/tao all return physical pixels — returning logical here is off by + // the scale factor on HiDPI/Retina displays). + let location: NSPoint = NSEvent::mouseLocation(); + let primary = + unsafe { NSScreen::screens(mtm).firstObject() }.ok_or(Error::FailedToGetCursorPosition)?; + let screen_height = primary.frame().size.height; + let scale = primary.backingScaleFactor(); + + let logical = LogicalPosition::new(location.x, screen_height - location.y); + Ok(logical.to_physical(scale)) + } +} diff --git a/crates/tauri-runtime-cef/src/platform/macos/mod.rs b/crates/tauri-runtime-cef/src/platform/macos/mod.rs new file mode 100644 index 000000000000..dd28522bb7cb --- /dev/null +++ b/crates/tauri-runtime-cef/src/platform/macos/mod.rs @@ -0,0 +1,15 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +mod application; +mod dock; +mod event_loop; +mod monitor; +mod progress; +mod utils; +mod webview; +mod window; + +pub use application::setup_application; +pub(crate) use application::{AppDelegate, AppDelegateEvent, set_application_event_handler}; diff --git a/crates/tauri-runtime-cef/src/platform/macos/monitor.rs b/crates/tauri-runtime-cef/src/platform/macos/monitor.rs new file mode 100644 index 000000000000..4e424721fd24 --- /dev/null +++ b/crates/tauri-runtime-cef/src/platform/macos/monitor.rs @@ -0,0 +1,35 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use objc2_app_kit::NSScreen; +use tauri_runtime::dpi::{LogicalSize, PhysicalRect}; +use winit::{monitor::MonitorHandle, platform::macos::MonitorHandleExtMacOS}; + +use crate::platform::{MonitorExt, monitor_bounds}; + +impl MonitorExt for MonitorHandle { + fn work_area(&self) -> PhysicalRect { + let Some(ns_screen) = self.ns_screen() else { + return monitor_bounds(self); + }; + + let ns_screen: &NSScreen = unsafe { &*ns_screen.cast() }; + let screen_frame = ns_screen.frame(); + let visible_frame = ns_screen.visibleFrame(); + let scale_factor = self.scale_factor(); + + let position = self.position().unwrap_or_default(); + let mut position = position.to_logical::(scale_factor); + position.x += visible_frame.origin.x - screen_frame.origin.x; + position.y += (screen_frame.origin.y + screen_frame.size.height) + - (visible_frame.origin.y + visible_frame.size.height); + + let size = LogicalSize::new(visible_frame.size.width, visible_frame.size.height); + + PhysicalRect { + position: position.to_physical(scale_factor), + size: size.to_physical(scale_factor), + } + } +} diff --git a/crates/tauri-runtime-cef/src/platform/macos/progress.rs b/crates/tauri-runtime-cef/src/platform/macos/progress.rs new file mode 100644 index 000000000000..a67058c65bf8 --- /dev/null +++ b/crates/tauri-runtime-cef/src/platform/macos/progress.rs @@ -0,0 +1,138 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::cell::Cell; + +use objc2::{DefinedClass, MainThreadMarker, MainThreadOnly, define_class, msg_send, rc::Retained}; +use objc2_app_kit::{ + NSApplication, NSBezierPath, NSColor, NSDockTile, NSImageView, NSProgressIndicator, NSView, +}; +use objc2_foundation::{NSInsetRect, NSPoint, NSRect, NSSize}; +use tauri_runtime::{ProgressBarState, ProgressBarStatus}; + +struct DockProgressIndicatorIvars { + state: Cell, +} + +impl Default for DockProgressIndicatorIvars { + fn default() -> Self { + Self { + state: Cell::new(ProgressBarStatus::None), + } + } +} + +define_class!( + #[unsafe(super(NSProgressIndicator))] + #[ivars = DockProgressIndicatorIvars] + struct DockProgressIndicator; + + impl DockProgressIndicator { + #[unsafe(method(drawRect:))] + fn draw_rect(&self, rect: NSRect) { + let bar = NSRect::new( + NSPoint::new(0.0, 4.0), + NSSize::new(rect.size.width, 8.0), + ); + let bar_inner = NSInsetRect(bar, 0.5, 0.5); + let mut bar_progress = NSInsetRect(bar, 1.0, 1.0); + + let progress = (self.doubleValue() / 100.0).clamp(0.0, 1.0); + bar_progress.size.width *= progress; + + NSColor::colorWithWhite_alpha(1.0, 0.05).set(); + draw_rounded_rect(bar); + draw_rounded_rect(bar_inner); + + let progress_color = match self.ivars().state.get() { + ProgressBarStatus::Paused => NSColor::systemYellowColor(), + ProgressBarStatus::Error => NSColor::systemRedColor(), + _ => NSColor::systemBlueColor(), + }; + progress_color.set(); + draw_rounded_rect(bar_progress); + } + } +); + +impl DockProgressIndicator { + fn new(mtm: MainThreadMarker, frame: NSRect) -> Retained { + let this = Self::alloc(mtm).set_ivars(DockProgressIndicatorIvars::default()); + unsafe { msg_send![super(this), initWithFrame: frame] } + } + + fn set_state(&self, status: ProgressBarStatus) { + self.ivars().state.set(status); + } +} + +fn draw_rounded_rect(rect: NSRect) { + let radius = rect.size.height / 2.0; + NSBezierPath::bezierPathWithRoundedRect_xRadius_yRadius(rect, radius, radius).fill(); +} + +pub fn set_dock_progress_bar(state: ProgressBarState) { + let Some(mtm) = MainThreadMarker::new() else { + return; + }; + let app = NSApplication::sharedApplication(mtm); + let dock_tile = app.dockTile(); + let Some(progress_indicator) = dock_progress_indicator(&app, &dock_tile, mtm) else { + return; + }; + + if let Some(progress) = state.progress { + progress_indicator.setDoubleValue(progress.min(100) as f64); + progress_indicator.setHidden(false); + } + + if let Some(status) = state.status { + progress_indicator.set_state(status); + progress_indicator.setHidden(matches!(status, ProgressBarStatus::None)); + } + + dock_tile.display(); +} + +fn dock_progress_indicator( + app: &NSApplication, + dock_tile: &NSDockTile, + mtm: MainThreadMarker, +) -> Option> { + let content_view = match dock_tile.contentView(mtm) { + Some(content_view) => content_view, + None => { + let app_icon = app.applicationIconImage()?; + let image_view = NSImageView::imageViewWithImage(&app_icon, mtm); + dock_tile.setContentView(Some(&image_view)); + dock_tile.contentView(mtm)? + } + }; + + if let Some(progress_indicator) = existing_progress_indicator(&content_view) { + return Some(progress_indicator); + } + + let dock_tile_size = dock_tile.size(); + let frame = NSRect::new( + NSPoint::new(0.0, 0.0), + NSSize::new(dock_tile_size.width, 15.0), + ); + let progress_indicator = DockProgressIndicator::new(mtm, frame); + content_view.addSubview(&progress_indicator); + + Some(progress_indicator) +} + +fn existing_progress_indicator(content_view: &NSView) -> Option> { + let subviews = content_view.subviews(); + for idx in 0..subviews.count() { + let subview = subviews.objectAtIndex(idx); + if let Ok(progress_indicator) = subview.downcast::() { + return Some(progress_indicator); + } + } + + None +} diff --git a/crates/tauri-runtime-cef/src/platform/macos/utils.rs b/crates/tauri-runtime-cef/src/platform/macos/utils.rs new file mode 100644 index 000000000000..5770c2b5b8d2 --- /dev/null +++ b/crates/tauri-runtime-cef/src/platform/macos/utils.rs @@ -0,0 +1,47 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{sync::OnceLock, time::Instant}; + +use objc2::rc::Retained; +use objc2_app_kit::NSColor; +use objc2_application_services::{ + ProcessApplicationTransformState, TransformProcessType, kCurrentProcess, +}; +use tauri_utils::config::Color; + +#[repr(C)] +#[allow(non_snake_case)] +struct ProcessSerialNumber { + highLongOfPSN: u32, + lowLongOfPSN: u32, +} + +pub fn transform_process_type(transform_state: ProcessApplicationTransformState) { + let process_serial_number = ProcessSerialNumber { + highLongOfPSN: 0, + lowLongOfPSN: kCurrentProcess, + }; + + unsafe { + let serial = (&process_serial_number as *const ProcessSerialNumber).cast(); + let _ = TransformProcessType(serial, transform_state); + } +} + +pub fn ns_color_from_tauri_color(color: Color) -> Retained { + let Color(red, green, blue, alpha) = color; + let scale = u8::MAX as f64; + NSColor::colorWithSRGBRed_green_blue_alpha( + red as f64 / scale, + green as f64 / scale, + blue as f64 / scale, + alpha as f64 / scale, + ) +} + +pub fn instant_epoch() -> Instant { + static INSTANT_EPOCH: OnceLock = OnceLock::new(); + *INSTANT_EPOCH.get_or_init(Instant::now) +} diff --git a/crates/tauri-runtime-cef/src/platform/macos/webview.rs b/crates/tauri-runtime-cef/src/platform/macos/webview.rs new file mode 100644 index 000000000000..2b64f82b1763 --- /dev/null +++ b/crates/tauri-runtime-cef/src/platform/macos/webview.rs @@ -0,0 +1,109 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use cef::ImplBrowserHost; +use objc2::rc::Retained; +use objc2_app_kit::{NSColor, NSView}; +use objc2_foundation::{NSPoint, NSRect, NSSize}; +use tauri_runtime::dpi::{LogicalPosition, LogicalSize, Rect}; +use tauri_utils::config::Color; + +use crate::{webview::AppWebview, window::AppWindow}; + +use super::utils; + +impl AppWebview { + pub(crate) fn nsview(&self) -> Option> { + let handle = self.host.window_handle(); + let view = handle.cast::(); + unsafe { Retained::::retain(view) } + } + + pub(crate) fn set_background_color(&self, color: Option) { + let Some(nsview) = self.nsview() else { + return; + }; + + nsview.setWantsLayer(true); + + let Some(layer) = nsview.layer() else { + return; + }; + + let nscolor = color + .map(utils::ns_color_from_tauri_color) + .unwrap_or_else(NSColor::windowBackgroundColor); + + let cg_color = nscolor.CGColor(); + layer.setBackgroundColor(Some(&*cg_color)); + } + + pub(crate) fn bounds(&self) -> Option { + let Some(nsview) = self.nsview() else { + return None; + }; + + let parent = unsafe { nsview.superview()? }; + let parent_frame = parent.frame(); + let frame = nsview.frame(); + + let y = if parent.isFlipped() { + frame.origin.y + } else { + parent_frame.size.height - frame.origin.y - frame.size.height + }; + + let position = LogicalPosition::new(frame.origin.x, y); + let size = LogicalSize::new(frame.size.width, frame.size.height); + + Some(Rect { + position: position.into(), + size: size.into(), + }) + } + + pub(crate) fn reparent(&self, parent: &AppWindow) { + let Some(view) = self.nsview() else { + return; + }; + let Some(parent) = parent.nsview() else { + return; + }; + + parent.addSubview(&view); + } + + pub(crate) fn apply_visible(&self, visible: bool) { + let Some(nsview) = self.nsview() else { + return; + }; + + nsview.setHidden(!visible); + } + + pub(crate) fn apply_physical_bounds(&self, scale: f64, x: i32, y: i32, width: i32, height: i32) { + let Some(nsview) = self.nsview() else { + return; + }; + let Some(parent) = (unsafe { nsview.superview() }) else { + return; + }; + + // CEF provides child bounds as physical pixels, but NSView frames are logical pixels. + let x = x as f64 / scale; + let y = y as f64 / scale; + let width = width as f64 / scale; + let height = height as f64 / scale; + + let parent_frame = parent.frame(); + let y = if parent.isFlipped() { + y + } else { + parent_frame.size.height - (y + height) + }; + + let frame = NSRect::new(NSPoint::new(x, y), NSSize::new(width, height)); + nsview.setFrame(frame); + } +} diff --git a/crates/tauri-runtime-cef/src/platform/macos/window.rs b/crates/tauri-runtime-cef/src/platform/macos/window.rs new file mode 100644 index 000000000000..c88226012ed5 --- /dev/null +++ b/crates/tauri-runtime-cef/src/platform/macos/window.rs @@ -0,0 +1,165 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use cef::ImplBrowserHost; +use objc2::{MainThreadMarker, rc::Retained}; +use objc2_app_kit::{ + NSBackingStoreType, NSColor, NSView, NSWindow, NSWindowButton, NSWindowCollectionBehavior, + NSWindowStyleMask, +}; +use tauri_runtime::dpi::Position; +use tauri_utils::{TitleBarStyle, config::Color}; + +use crate::window::AppWindow; + +use super::utils; + +impl AppWindow { + pub(crate) fn nsview(&self) -> Option> { + let handle = self.raw_handle_as_cef_handle(); + let view = handle.cast::(); + unsafe { Retained::::retain(view) } + } + + pub(crate) fn set_enabled(&self, enabled: bool) { + let Some(nsview) = self.nsview() else { + return; + }; + let Some(nswindow) = nsview.window() else { + return; + }; + + if enabled { + if let Some(attached) = nswindow.attachedSheet() { + nswindow.endSheet(&attached); + } + } else { + if nswindow.attachedSheet().is_some() { + return; + } + + let Some(mtm) = MainThreadMarker::new() else { + return; + }; + let frame = nswindow.frame(); + let sheet = unsafe { + NSWindow::initWithContentRect_styleMask_backing_defer( + mtm.alloc(), + frame, + NSWindowStyleMask::Titled, + NSBackingStoreType::Buffered, + false, + ) + }; + sheet.setAlphaValue(0.5); + (&*nswindow).beginSheet_completionHandler(&*sheet, None); + } + } + + pub(crate) fn is_enabled(&self) -> bool { + self + .nsview() + .and_then(|nsview| nsview.window()) + .map(|nswindow| nswindow.attachedSheet().is_none()) + .unwrap_or(true) + } + + pub(crate) fn apply_traffic_light_position(&self, position: &Position) { + let Some(nsview) = self.nsview() else { + return; + }; + let Some(nswindow) = nsview.window() else { + return; + }; + + let Some(close) = nswindow.standardWindowButton(NSWindowButton::CloseButton) else { + return; + }; + let Some(miniaturize) = nswindow.standardWindowButton(NSWindowButton::MiniaturizeButton) else { + return; + }; + let Some(zoom) = nswindow.standardWindowButton(NSWindowButton::ZoomButton) else { + return; + }; + + let pos = position.to_logical::(nswindow.backingScaleFactor()); + let title_bar_container_view = unsafe { close.superview().and_then(|view| view.superview()) }; + let Some(title_bar_container_view) = title_bar_container_view else { + return; + }; + + let close_rect = close.frame(); + let title_bar_frame_height = close_rect.size.height + pos.y; + let mut title_bar_rect = title_bar_container_view.frame(); + title_bar_rect.size.height = title_bar_frame_height; + title_bar_rect.origin.y = nswindow.frame().size.height - title_bar_frame_height; + title_bar_container_view.setFrame(title_bar_rect); + + let space_between = miniaturize.frame().origin.x - close_rect.origin.x; + for (index, button) in [close, miniaturize, zoom].into_iter().enumerate() { + let mut origin = button.frame().origin; + origin.x = pos.x + (index as f64 * space_between); + button.setFrameOrigin(origin); + } + } + + pub(crate) fn set_title_bar_style(&self, style: TitleBarStyle) { + let Some(nsview) = self.nsview() else { + return; + }; + let Some(nswindow) = nsview.window() else { + return; + }; + + match style { + TitleBarStyle::Visible => { + nswindow.setTitlebarAppearsTransparent(false); + let mut mask = nswindow.styleMask(); + mask.remove(NSWindowStyleMask::FullSizeContentView); + nswindow.setStyleMask(mask); + } + TitleBarStyle::Transparent => { + nswindow.setTitlebarAppearsTransparent(true); + let mut mask = nswindow.styleMask(); + mask.remove(NSWindowStyleMask::FullSizeContentView); + nswindow.setStyleMask(mask); + } + TitleBarStyle::Overlay => { + nswindow.setTitlebarAppearsTransparent(true); + let mut mask = nswindow.styleMask(); + mask.insert(NSWindowStyleMask::FullSizeContentView); + nswindow.setStyleMask(mask); + } + _ => {} + } + } + + pub(crate) fn set_visible_on_all_workspaces(&self, visible: bool) { + let Some(nsview) = self.nsview() else { + return; + }; + let Some(nswindow) = nsview.window() else { + return; + }; + + let mut collection_behavior = nswindow.collectionBehavior(); + collection_behavior.set(NSWindowCollectionBehavior::CanJoinAllSpaces, visible); + nswindow.setCollectionBehavior(collection_behavior); + } + + pub(crate) fn set_background_color(&self, color: Option) { + let Some(nsview) = self.nsview() else { + return; + }; + let Some(nswindow) = nsview.window() else { + return; + }; + + let nscolor = color + .map(utils::ns_color_from_tauri_color) + .unwrap_or_else(NSColor::windowBackgroundColor); + nswindow.setOpaque(color.map(|color| color.3 == u8::MAX).unwrap_or(true)); + nswindow.setBackgroundColor(Some(&nscolor)); + } +} diff --git a/crates/tauri-runtime-cef/src/platform/mod.rs b/crates/tauri-runtime-cef/src/platform/mod.rs new file mode 100644 index 000000000000..ea3561741f48 --- /dev/null +++ b/crates/tauri-runtime-cef/src/platform/mod.rs @@ -0,0 +1,54 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#[cfg(windows)] +mod windows; + +#[cfg(target_os = "macos")] +pub mod macos; + +#[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] +mod linux; + +use tauri_runtime::dpi::PhysicalRect; +use winit::monitor::MonitorHandle; + +pub(crate) trait MonitorExt { + /// Get the work area of this monitor. + /// + /// TODO: upstream work-area support into winit and replace this native shim. + fn work_area(&self) -> PhysicalRect; +} + +fn monitor_bounds(monitor: &MonitorHandle) -> PhysicalRect { + PhysicalRect { + position: monitor.position().unwrap_or_default(), + size: monitor + .current_video_mode() + .map(|video_mode| video_mode.size()) + .unwrap_or_default(), + } +} + +pub trait EventLoopExt { + #[cfg(target_os = "macos")] + fn set_activation_policy(&self, policy: tauri_runtime::ActivationPolicy); + #[cfg(target_os = "macos")] + fn set_dock_visibility(&self, visible: bool); + #[cfg(target_os = "macos")] + fn show_application(&self); + #[cfg(target_os = "macos")] + fn hide_application(&self); + #[cfg(target_os = "macos")] + fn set_progress_bar(&self, state: tauri_runtime::ProgressBarState); + fn set_badge_count(&self, count: Option, desktop_filename: Option); + fn set_badge_label(&self, label: Option); + fn cursor_position(&self) -> tauri_runtime::Result>; +} diff --git a/crates/tauri-runtime-cef/src/platform/windows/event_loop.rs b/crates/tauri-runtime-cef/src/platform/windows/event_loop.rs new file mode 100644 index 000000000000..a3d43c5d1296 --- /dev/null +++ b/crates/tauri-runtime-cef/src/platform/windows/event_loop.rs @@ -0,0 +1,25 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use tauri_runtime::{Error, Result, dpi::PhysicalPosition}; +use windows::Win32::{Foundation::POINT, UI::WindowsAndMessaging::GetCursorPos}; +use winit::event_loop::ActiveEventLoop; + +use crate::platform::EventLoopExt; + +impl EventLoopExt for dyn ActiveEventLoop + '_ { + fn set_badge_count(&self, _count: Option, _desktop_filename: Option) { + // Unsupported on Windows + } + + fn set_badge_label(&self, _label: Option) { + // Unsupported on Windows + } + + fn cursor_position(&self) -> Result> { + let mut point = POINT::default(); + unsafe { GetCursorPos(&mut point) }.map_err(|_| Error::FailedToGetCursorPosition)?; + Ok(PhysicalPosition::new(point.x as f64, point.y as f64)) + } +} diff --git a/crates/tauri-runtime-cef/src/platform/windows/icon.rs b/crates/tauri-runtime-cef/src/platform/windows/icon.rs new file mode 100644 index 000000000000..4a9b1fcc7afe --- /dev/null +++ b/crates/tauri-runtime-cef/src/platform/windows/icon.rs @@ -0,0 +1,34 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use tauri_runtime::Icon; +use windows::Win32::UI::WindowsAndMessaging::{CreateIcon, HICON}; + +pub fn icon_to_hicon(icon: Icon<'static>) -> Option { + let width = icon.width; + let height = icon.height; + let mut rgba = icon.rgba.into_owned(); + if width == 0 || height == 0 || rgba.len() != width as usize * height as usize * 4 { + return None; + } + + let mut and_mask = Vec::with_capacity(width as usize * height as usize); + for pixel in rgba.chunks_exact_mut(4) { + and_mask.push(pixel[3].wrapping_sub(u8::MAX)); + pixel.swap(0, 2); + } + + unsafe { + CreateIcon( + None, + width as i32, + height as i32, + 1, + 32, + and_mask.as_ptr(), + rgba.as_ptr(), + ) + .ok() + } +} diff --git a/crates/tauri-runtime-cef/src/platform/windows/mod.rs b/crates/tauri-runtime-cef/src/platform/windows/mod.rs new file mode 100644 index 000000000000..25caa2f811fd --- /dev/null +++ b/crates/tauri-runtime-cef/src/platform/windows/mod.rs @@ -0,0 +1,9 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +mod event_loop; +mod icon; +mod monitor; +mod webview; +mod window; diff --git a/crates/tauri-runtime-cef/src/platform/windows/monitor.rs b/crates/tauri-runtime-cef/src/platform/windows/monitor.rs new file mode 100644 index 000000000000..062e7c9cfcb7 --- /dev/null +++ b/crates/tauri-runtime-cef/src/platform/windows/monitor.rs @@ -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_runtime::dpi::{PhysicalPosition, PhysicalRect, PhysicalSize}; +use windows::Win32::Graphics::Gdi::{GetMonitorInfoW, HMONITOR, MONITORINFO}; +use winit::monitor::MonitorHandle; + +use crate::platform::{MonitorExt, monitor_bounds}; + +impl MonitorExt for MonitorHandle { + fn work_area(&self) -> PhysicalRect { + let mut monitor_info = MONITORINFO { + cbSize: std::mem::size_of::() as u32, + ..Default::default() + }; + + let hmonitor = HMONITOR(self.native_id() as _); + + let status = unsafe { GetMonitorInfoW(hmonitor, &mut monitor_info) }; + if !status.as_bool() { + return monitor_bounds(self); + } + + let position = PhysicalPosition::new(monitor_info.rcWork.left, monitor_info.rcWork.top); + let size = PhysicalSize::new( + (monitor_info.rcWork.right - monitor_info.rcWork.left) as u32, + (monitor_info.rcWork.bottom - monitor_info.rcWork.top) as u32, + ); + PhysicalRect { position, size } + } +} diff --git a/crates/tauri-runtime-cef/src/platform/windows/webview.rs b/crates/tauri-runtime-cef/src/platform/windows/webview.rs new file mode 100644 index 000000000000..55c76556f024 --- /dev/null +++ b/crates/tauri-runtime-cef/src/platform/windows/webview.rs @@ -0,0 +1,88 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use cef::ImplBrowserHost; +use tauri_runtime::dpi::{PhysicalPosition, PhysicalSize, Rect}; +use tauri_utils::config::Color; +use windows::Win32::{ + Foundation::{HWND, POINT, RECT}, + Graphics::Gdi::MapWindowPoints, + UI::WindowsAndMessaging::{ + GetParent, GetWindowRect, SW_HIDE, SW_SHOW, SWP_NOACTIVATE, SWP_NOZORDER, SetParent, + SetWindowPos, ShowWindow, + }, +}; + +use crate::{webview::AppWebview, window::AppWindow}; + +impl AppWebview { + pub(crate) fn hwnd(&self) -> HWND { + let hwnd = self.host.window_handle(); + HWND(hwnd.0 as _) + } + + pub(crate) fn set_background_color(&self, _color: Option) { + // TODO: might not be supported on Windows + } + + pub(crate) fn bounds(&self) -> Option { + let hwnd = self.hwnd(); + + let mut rect = RECT::default(); + unsafe { + let parent = GetParent(hwnd).ok()?; + if parent.0.is_null() { + return None; + } + + GetWindowRect(hwnd, &mut rect).ok()?; + + let mut points = [ + POINT { + x: rect.left, + y: rect.top, + }, + POINT { + x: rect.right, + y: rect.bottom, + }, + ]; + if MapWindowPoints(None, Some(parent), &mut points) == 0 { + return None; + } + + let x = points[0].x; + let y = points[0].y; + let width = (points[1].x - points[0].x).max(0) as u32; + let height = (points[1].y - points[0].y).max(0) as u32; + Some(Rect { + position: PhysicalPosition::new(x, y).into(), + size: PhysicalSize::new(width, height).into(), + }) + } + } + + pub(crate) fn reparent(&self, parent: &AppWindow) { + let parent = parent.hwnd(); + let _ = unsafe { SetParent(self.hwnd(), Some(parent)) }; + } + + pub(crate) fn apply_visible(&self, visible: bool) { + let _ = unsafe { ShowWindow(self.hwnd(), if visible { SW_SHOW } else { SW_HIDE }) }; + } + + pub(crate) fn apply_physical_bounds(&self, _scale: f64, x: i32, y: i32, width: i32, height: i32) { + unsafe { + let _ = SetWindowPos( + self.hwnd(), + None, + x, + y, + width, + height, + SWP_NOZORDER | SWP_NOACTIVATE, + ); + } + } +} diff --git a/crates/tauri-runtime-cef/src/platform/windows/window.rs b/crates/tauri-runtime-cef/src/platform/windows/window.rs new file mode 100644 index 000000000000..f825853bafa7 --- /dev/null +++ b/crates/tauri-runtime-cef/src/platform/windows/window.rs @@ -0,0 +1,104 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use tauri_runtime::{Icon, ProgressBarState, ProgressBarStatus}; +use tauri_utils::config::Color; +use windows::Win32::{ + Foundation::{HWND, RECT}, + Graphics::Dwm::{DWMWA_EXTENDED_FRAME_BOUNDS, DwmGetWindowAttribute}, + System::Com::{CLSCTX_SERVER, CoCreateInstance}, + UI::{ + Input::KeyboardAndMouse::{EnableWindow, IsWindowEnabled}, + Shell::{ + ITaskbarList3, TBPF_ERROR, TBPF_INDETERMINATE, TBPF_NOPROGRESS, TBPF_NORMAL, TBPF_PAUSED, + TaskbarList, + }, + WindowsAndMessaging::DestroyIcon, + }, +}; + +use crate::window::AppWindow; + +use super::icon::icon_to_hicon; + +impl AppWindow { + pub(crate) fn hwnd(&self) -> HWND { + let hwnd = self.raw_handle_as_cef_handle(); + HWND(hwnd.0 as _) + } + + pub(crate) fn is_enabled(&self) -> bool { + unsafe { IsWindowEnabled(self.hwnd()) }.as_bool() + } + + pub(crate) fn set_enabled(&self, enabled: bool) { + let _ = unsafe { EnableWindow(self.hwnd(), enabled) }; + } + + pub(crate) fn set_overlay_icon(&self, icon: Option>) { + let Ok(taskbar) = + (unsafe { CoCreateInstance::<_, ITaskbarList3>(&TaskbarList, None, CLSCTX_SERVER) }) + else { + return; + }; + + let icon = icon.and_then(icon_to_hicon); + let hwnd = self.hwnd(); + + if let Some(icon) = icon { + let _ = unsafe { taskbar.SetOverlayIcon(hwnd, icon, None) }; + let _ = unsafe { DestroyIcon(icon) }; + } else { + let _ = unsafe { taskbar.SetOverlayIcon(hwnd, Default::default(), None) }; + } + } + + pub(crate) fn set_progress_bar(&self, state: ProgressBarState) { + let Ok(taskbar) = + (unsafe { CoCreateInstance::<_, ITaskbarList3>(&TaskbarList, None, CLSCTX_SERVER) }) + else { + return; + }; + + let hwnd = self.hwnd(); + if let Some(status) = state.status { + let flag = match status { + ProgressBarStatus::None => TBPF_NOPROGRESS, + ProgressBarStatus::Normal => TBPF_NORMAL, + ProgressBarStatus::Indeterminate => TBPF_INDETERMINATE, + ProgressBarStatus::Paused => TBPF_PAUSED, + ProgressBarStatus::Error => TBPF_ERROR, + }; + let _ = unsafe { taskbar.SetProgressState(hwnd, flag) }; + } + + if let Some(progress) = state.progress { + let _ = unsafe { taskbar.SetProgressValue(hwnd, progress.min(100), 100) }; + } + } + + pub(crate) fn set_background_color(&self, _color: Option) { + // TODO + } + + /// The visible frame height reported by DWM (`DWMWA_EXTENDED_FRAME_BOUNDS`). + /// + /// winit's `outer_size` includes the invisible resize/shadow border, which + /// throws off vertical centering for decorated windows. The DWM extended + /// frame bounds describe the actually-visible window rectangle, so its height + /// is what should be used when centering. Returns `None` on failure. + pub(crate) fn dwm_visible_frame_height(&self) -> Option { + let mut rect = RECT::default(); + let result = unsafe { + DwmGetWindowAttribute( + self.hwnd(), + DWMWA_EXTENDED_FRAME_BOUNDS, + &mut rect as *mut _ as *mut _, + std::mem::size_of::() as u32, + ) + }; + result.ok()?; + Some((rect.bottom - rect.top) as u32) + } +} diff --git a/crates/tauri-runtime-cef/src/runtime.rs b/crates/tauri-runtime-cef/src/runtime.rs new file mode 100644 index 000000000000..0ec3f8e1eef2 --- /dev/null +++ b/crates/tauri-runtime-cef/src/runtime.rs @@ -0,0 +1,1582 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#![allow(clippy::arc_with_non_send_sync)] +#![allow(clippy::too_many_arguments)] + +use std::{ + collections::HashMap, + fmt, + fs::create_dir_all, + path::PathBuf, + sync::{ + Arc, Mutex, + atomic::{AtomicBool, AtomicPtr, AtomicU32, Ordering}, + mpsc::{self, Receiver, Sender}, + }, + time::Duration, +}; + +use cef::*; +use raw_window_handle::{DisplayHandle, HasDisplayHandle}; +use tauri_runtime::{ + DeviceEventFilter, Error, EventLoopProxy, ExitRequestedEventAction, Result, RunEvent, Runtime, + RuntimeHandle, RuntimeInitArgs, UserEvent, + dpi::PhysicalPosition, + monitor::Monitor, + webview::{DetachedWebview, PendingWebview}, + window::{ + DetachedWindow, DragDropEvent, PendingWindow, RawWindow, WebviewEvent, WindowEvent, WindowId, + }, +}; +use tauri_utils::Theme; +use winit::{ + application::ApplicationHandler, + event::{StartCause, WindowEvent as WinitWindowEvent}, + event_loop::{ + ActiveEventLoop, EventLoop, EventLoopBuilder, EventLoopProxy as WinitEventLoopProxy, + }, + window::WindowId as WinitWindowId, +}; + +use crate::external_message_pump::CefExternalPump; +use crate::platform::EventLoopExt; +use crate::{ + cef_impl::{client as browser_client, ipc, request_handler}, + webview::{ + self, AppWebview, CefWebviewDispatcher, Webview, WebviewAtribute, WebviewMessage, + create_webview_detached, + }, + window::{ + AppWindow, CefWindowDispatcher, SendRawDisplayHandle, WindowMessage, create_window_detached, + winit_monitor_to_tauri_monitor, winit_theme_to_tauri_theme, + }, +}; +#[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] +use winit::platform::x11::EventLoopBuilderExtX11; + +/// The `cef` crate used by this runtime, re-exported for convenience. +/// +/// # Stability +/// +/// The cef crate follows the Chromium Embedded Framework interface and there is +/// no API stability guarantees. The crate will be updated frequently, usually +/// in minor releases when a known breaking change is discovered. +pub use cef; + +/// Platform-specific runtime init attributes. +#[derive(Clone, Debug)] +pub enum RuntimeInitAttribute { + /// Command line arguments passed to CEF. + CommandLineArgs { args: Vec<(String, Option)> }, + /// Deep link schemes. + DeepLinkSchemes { schemes: Vec }, + /// Directory used for CEF disk cache (`Settings::cache_path`). + /// + /// If unspecified, defaults to `{user cache}/{app identifier}/cef`. + CachePath { path: PathBuf }, +} + +impl tauri_runtime::InitAttribute for RuntimeInitAttribute { + fn new(config: &tauri_utils::config::Config) -> Result> { + let mut attrs = Vec::new(); + if let Some(plugin_config) = config + .plugins + .0 + .get("deep-link") + .and_then(|config| config.get("desktop").cloned()) + { + #[derive(serde::Deserialize)] + #[serde(untagged)] + enum DesktopDeepLinks { + One(tauri_utils::config::DeepLinkProtocol), + List(Vec), + } + + let protocols: DesktopDeepLinks = + serde_json::from_value(plugin_config).map_err(tauri_runtime::Error::Json)?; + let schemes = match protocols { + DesktopDeepLinks::One(protocol) => protocol.schemes, + DesktopDeepLinks::List(protocols) => protocols + .into_iter() + .flat_map(|protocol| protocol.schemes) + .collect(), + }; + + attrs.push(RuntimeInitAttribute::DeepLinkSchemes { schemes }); + } + Ok(attrs) + } +} + +#[derive(Debug)] +pub struct NewWindowOpener {} + +#[derive(Clone, Debug)] +pub struct EventProxy { + context: RuntimeContext, +} + +impl EventLoopProxy for EventProxy { + fn send_event(&self, event: T) -> Result<()> { + self.context.send_message(Message::UserEvent(event)) + } +} + +#[derive(Clone)] +pub(crate) struct RuntimeContext { + pub(crate) sender: Sender>, + pub(crate) proxy: WinitEventLoopProxy, + main_thread_id: std::thread::ThreadId, + next_window_id: Arc, + next_webview_id: Arc, + next_window_event_id: Arc, + next_webview_event_id: Arc, + current_dispatch: Arc>, + pub(crate) app_wide_theme: Arc>>, + pub(crate) cef_pump: CefExternalPump, + /// Root cache path passed to [`cef::Settings::cache_path`] during + /// [`cef::initialize`]. Per-webview `data_directory` profiles must resolve + /// under this root for CEF request contexts to be accepted. + pub(crate) cache_path: Arc, +} + +/// Scoped access to the current winit callback state. +/// +/// `ActiveEventLoop` is only borrowed during `ApplicationHandler` callbacks, but +/// setup-time runtime messages may synchronously need it. While a callback is +/// active, this slot lets main-thread `send_message` handle work immediately; +/// other threads still queue and wake the loop. The slot stores an atomic +/// pointer to guard-owned state so lookup is lock-free. The guard restores the +/// previous pointer before dropping that state, so the raw pointers are never +/// treated as valid beyond their callback. +#[derive(Clone, Copy)] +struct MainThreadDispatch { + app: *mut WinitCefApp, + event_loop: *const dyn ActiveEventLoop, +} + +struct MainThreadDispatchSlot { + current: AtomicPtr>, +} + +impl MainThreadDispatchSlot { + fn install(&self, dispatch: &mut MainThreadDispatch) -> *mut MainThreadDispatch { + self.current.swap(dispatch, Ordering::AcqRel) + } + + fn restore(&self, current: *mut MainThreadDispatch, previous: *mut MainThreadDispatch) { + let installed = self.current.swap(previous, Ordering::AcqRel); + debug_assert_eq!(installed, current); + } + + fn current(&self) -> Option<&MainThreadDispatch> { + let current = self.current.load(Ordering::Acquire); + if current.is_null() { + None + } else { + // SAFETY: the pointer targets the boxed dispatch state owned by + // `MainThreadDispatchGuard`, whose allocation remains stable while the + // guard is moved. The slot is restored before that state is dropped, and + // it is only read by `send_message` after verifying that it is running on + // the runtime main thread. + Some(unsafe { &*current }) + } + } +} + +impl Default for MainThreadDispatchSlot { + fn default() -> Self { + Self { + current: AtomicPtr::new(std::ptr::null_mut()), + } + } +} + +struct MainThreadDispatchGuard { + context: RuntimeContext, + dispatch: Box>, + previous: *mut MainThreadDispatch, +} + +impl Drop for MainThreadDispatchGuard { + fn drop(&mut self) { + self + .context + .current_dispatch + .restore(self.dispatch.as_mut(), self.previous); + } +} + +fn install_current_dispatch( + app: &mut WinitCefApp, + event_loop: &dyn ActiveEventLoop, +) -> MainThreadDispatchGuard { + let dispatch = MainThreadDispatch { + app: app as *mut _, + event_loop: event_loop as *const _, + }; + // Keep the installed pointer stable even if the guard is moved. + let mut dispatch = Box::new(dispatch); + + let previous = app.context.current_dispatch.install(dispatch.as_mut()); + + MainThreadDispatchGuard { + context: app.context.clone(), + dispatch, + previous, + } +} + +#[allow(clippy::result_large_err)] +fn handle_main_thread_message( + context: &RuntimeContext, + message: Message, +) -> std::result::Result<(), Message> { + let Some(dispatch) = context.current_dispatch.current() else { + return Err(message); + }; + + // SAFETY: `install_current_dispatch` stores pointers to the currently + // executing winit application handler and event-loop callback. This function + // is only called on the runtime main thread while that callback is active. + let app = unsafe { &mut *dispatch.app }; + let event_loop = unsafe { &*dispatch.event_loop }; + + app.handle_message(event_loop, message); + + Ok(()) +} + +impl fmt::Debug for RuntimeContext { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("RuntimeContext").finish() + } +} + +impl RuntimeContext { + pub(crate) fn send_message(&self, message: Message) -> Result<()> { + let message = if self.is_main_thread() { + match handle_main_thread_message(self, message) { + Ok(()) => return Ok(()), + Err(message) => message, + } + } else { + message + }; + + self + .sender + .send(message) + .map_err(|_| Error::FailedToSendMessage)?; + self.proxy.wake_up(); + Ok(()) + } + + pub(crate) fn is_main_thread(&self) -> bool { + std::thread::current().id() == self.main_thread_id + } + + /// Run `f` on the main (event-loop) thread. + /// + /// When called from the main thread we execute `f` inline instead of posting + /// it to the channel. Tauri implements several blocking getters as + /// `run_on_main_thread(|| { .. tx.send(..) }); rx.recv()` (e.g. + /// `Window::add_child`). Those run during `setup`, which the runtime drives + /// from winit's `can_create_surfaces`; posting the closure instead of running + /// it inline would deadlock because the loop cannot drain the task while the + /// main thread blocks on `rx.recv()`. + pub(crate) fn run_on_main_thread(&self, f: F) -> Result<()> { + if self.is_main_thread() { + f(); + Ok(()) + } else { + self.send_message(Message::Task(Box::new(f))) + } + } + + pub(crate) fn next_window_id(&self) -> WindowId { + self.next_window_id.fetch_add(1, Ordering::Relaxed).into() + } + + pub(crate) fn next_webview_id(&self) -> u32 { + self.next_webview_id.fetch_add(1, Ordering::Relaxed) + } + + pub(crate) fn next_window_event_id(&self) -> u32 { + self.next_window_event_id.fetch_add(1, Ordering::Relaxed) + } + + pub(crate) fn next_webview_event_id(&self) -> u32 { + self.next_webview_event_id.fetch_add(1, Ordering::Relaxed) + } +} + +pub(crate) type AfterWindowCreationCallback = Box Fn(RawWindow<'a>) + Send>; + +pub(crate) enum Message { + EventLoop(EventLoopMessage), + BrowserClosed(WindowId, u32), + Opened(Vec), + #[cfg(target_os = "macos")] + Reopen { + has_visible_windows: bool, + }, + #[cfg(target_os = "macos")] + AccessibilityChanged { + enabled: bool, + }, + CreateWindow { + window_id: WindowId, + webview_id: Option, + pending: Box>>, + after_window_creation: Option, + result_tx: Sender>, + }, + CreateWebview { + window_id: WindowId, + webview_id: u32, + pending: Box>>, + result_tx: Sender>, + }, + Window { + window_id: WindowId, + message: WindowMessage, + }, + Webview { + window_id: WindowId, + webview_id: u32, + message: WebviewMessage, + }, + NavigateFirstWebview { + window_id: WindowId, + url: String, + }, + DragDropScriptEvent { + window_id: WindowId, + webview_id: u32, + target: browser_client::DragDropEventTarget, + drag_drop_state: Arc>, + event: browser_client::DragDropScriptEvent, + }, + Task(Box), + RequestExit(i32), + UserEvent(T), +} + +fn device_event_filter_to_winit(filter: DeviceEventFilter) -> winit::event_loop::DeviceEvents { + match filter { + DeviceEventFilter::Always => winit::event_loop::DeviceEvents::Never, + DeviceEventFilter::Unfocused => winit::event_loop::DeviceEvents::WhenFocused, + DeviceEventFilter::Never => winit::event_loop::DeviceEvents::Always, + } +} + +pub(crate) enum EventLoopMessage { + SetTheme(Option), + SetDeviceEventFilter(DeviceEventFilter), + PrimaryMonitor(Sender>), + MonitorFromPoint(Sender>, f64, f64), + AvailableMonitors(Sender>), + CursorPosition(Sender>>), + DisplayHandle(Sender>), + #[cfg(target_os = "macos")] + SetActivationPolicy(tauri_runtime::ActivationPolicy), + #[cfg(target_os = "macos")] + SetDockVisibility(bool), + #[cfg(target_os = "macos")] + ShowApplication, + #[cfg(target_os = "macos")] + HideApplication, +} + +macro_rules! event_loop_getter { + ($self:ident, $variant:ident) => {{ + let (tx, rx) = mpsc::channel(); + match $self + .context + .send_message(Message::EventLoop(EventLoopMessage::$variant(tx))) + { + Ok(()) => rx.recv().map_err(|_| Error::FailedToReceiveMessage), + Err(error) => Err(error), + } + }}; +} + +fn find_monitor_from_point( + monitors: impl Iterator, + x: f64, + y: f64, +) -> Option { + monitors.into_iter().find(|monitor| { + let pos = monitor.position().unwrap_or_default(); + let size = monitor + .current_video_mode() + .map(|mode| mode.size()) + .unwrap_or_default(); + x >= pos.x as f64 + && x <= pos.x as f64 + size.width as f64 + && y >= pos.y as f64 + && y <= pos.y as f64 + size.height as f64 + }) +} + +#[cfg(target_os = "macos")] +fn is_cef_helper_process() -> bool { + const HELPER_SUFFIXES: &[&str] = &[ + " Helper (GPU)", + " Helper (Renderer)", + " Helper (Plugin)", + " Helper (Alerts)", + " Helper", + ]; + + std::env::current_exe() + .ok() + .and_then(|path| { + path + .file_name() + .and_then(|name| name.to_str()) + .map(|name| HELPER_SUFFIXES.iter().any(|suffix| name.ends_with(suffix))) + }) + .unwrap_or_default() +} + +pub(crate) struct AppState { + pub(crate) windows: HashMap, + pub(crate) winid_id_to_window_id_map: HashMap, + pub(crate) callback: Box)>, + pub(crate) live_browsers: usize, + pub(crate) exiting: bool, +} + +pub(crate) struct WinitCefApp { + pub(crate) context: RuntimeContext, + receiver: Receiver>, + pub(crate) state: AppState, + pub(crate) scheme_registry: request_handler::SchemeRegistry, +} + +impl WinitCefApp { + fn new( + context: RuntimeContext, + receiver: Receiver>, + callback: Box)>, + scheme_registry: request_handler::SchemeRegistry, + ) -> Self { + Self { + context, + receiver, + state: AppState { + windows: HashMap::new(), + winid_id_to_window_id_map: HashMap::new(), + callback, + live_browsers: 0, + exiting: false, + }, + scheme_registry, + } + } + + fn run_callback(&mut self, event: RunEvent) { + (self.state.callback)(event); + } + + fn drain_messages(&mut self, event_loop: &dyn ActiveEventLoop) { + while let Ok(message) = self.receiver.try_recv() { + self.handle_message(event_loop, message); + } + } + + fn handle_message(&mut self, event_loop: &dyn ActiveEventLoop, message: Message) { + match message { + Message::EventLoop(message) => self.handle_event_loop_message(event_loop, message), + Message::BrowserClosed(_window_id, webview_id) => { + // Standalone webview.close() keeps the child in state until this + // callback, so cleanup happens here. Window/app teardown removes child + // bookkeeping before asking CEF to close; then this message is only the + // lifecycle acknowledgement that lets live_browsers drain. + // + // The window_id baked into the browser's handlers can be stale after a + // reparent, so locate the webview by its process-unique id across every + // window rather than trusting the message's window_id — otherwise a + // reparented webview's scheme-handler entries would leak and its + // AppWebview would linger in the target window forever. + let child = self.state.windows.values_mut().find_map(|appwindow| { + appwindow + .children + .iter() + .position(|child| child.webview_id == webview_id) + .map(|index| appwindow.children.remove(index)) + }); + if let Some(child) = child { + self.remove_scheme_handler_entries(&child); + } + + self.state.live_browsers = self.state.live_browsers.saturating_sub(1); + self.exit_if_done(event_loop); + } + Message::CreateWindow { + window_id, + webview_id, + pending, + after_window_creation, + result_tx, + } => { + let result = self.create_window( + event_loop, + window_id, + webview_id, + pending, + after_window_creation, + ); + let _ = result_tx.send(result); + } + Message::CreateWebview { + window_id, + webview_id, + pending, + result_tx, + } => { + let _ = result_tx.send(self.create_webview(window_id, webview_id, *pending)); + } + Message::Window { window_id, message } => { + self.handle_window_message(event_loop, window_id, message) + } + Message::Webview { + window_id, + webview_id, + message, + } => self.handle_webview_message(window_id, webview_id, message), + Message::NavigateFirstWebview { window_id, url } => { + self.navigate_first_webview(window_id, &url) + } + Message::DragDropScriptEvent { + window_id, + webview_id, + target, + drag_drop_state, + event, + } => { + if let Some(event) = browser_client::event_from_script_event(&drag_drop_state, event) { + self.emit_drag_drop_event(window_id, webview_id, target, event); + } + } + Message::Task(task) => task(), + Message::RequestExit(code) => { + if self.request_exit(Some(code)) { + self.close_all_browsers(); + self.exit_if_done(event_loop); + } + } + Message::Opened(urls) => self.run_callback(RunEvent::Opened { urls }), + #[cfg(target_os = "macos")] + Message::Reopen { + has_visible_windows, + } => self.run_callback(RunEvent::Reopen { + has_visible_windows, + }), + #[cfg(target_os = "macos")] + Message::AccessibilityChanged { enabled } => self.set_browsers_accessibility_state(enabled), + Message::UserEvent(event) => self.run_callback(RunEvent::UserEvent(event)), + } + } + + fn handle_event_loop_message( + &mut self, + event_loop: &dyn ActiveEventLoop, + message: EventLoopMessage, + ) { + match message { + EventLoopMessage::SetTheme(theme) => { + *self.context.app_wide_theme.lock().unwrap() = theme; + for appwindow in self.state.windows.values_mut() { + appwindow.set_theme(theme); + } + } + EventLoopMessage::PrimaryMonitor(tx) => { + let monitor = event_loop + .primary_monitor() + .map(|monitor| winit_monitor_to_tauri_monitor(&monitor)); + let _ = tx.send(monitor); + } + EventLoopMessage::MonitorFromPoint(tx, x, y) => { + let monitor = find_monitor_from_point(event_loop.available_monitors(), x, y) + .map(|monitor| winit_monitor_to_tauri_monitor(&monitor)); + let _ = tx.send(monitor); + } + EventLoopMessage::AvailableMonitors(tx) => { + let monitors = event_loop + .available_monitors() + .map(|monitor| winit_monitor_to_tauri_monitor(&monitor)) + .collect(); + let _ = tx.send(monitors); + } + EventLoopMessage::SetDeviceEventFilter(filter) => { + event_loop.listen_device_events(device_event_filter_to_winit(filter)); + } + EventLoopMessage::CursorPosition(tx) => { + let _ = tx.send(event_loop.cursor_position()); + } + EventLoopMessage::DisplayHandle(tx) => { + let handle = event_loop + .display_handle() + .map(|handle| SendRawDisplayHandle(handle.as_raw())); + let _ = tx.send(handle); + } + #[cfg(target_os = "macos")] + EventLoopMessage::SetActivationPolicy(activation_policy) => { + event_loop.set_activation_policy(activation_policy) + } + #[cfg(target_os = "macos")] + EventLoopMessage::SetDockVisibility(visible) => event_loop.set_dock_visibility(visible), + #[cfg(target_os = "macos")] + EventLoopMessage::ShowApplication => event_loop.show_application(), + #[cfg(target_os = "macos")] + EventLoopMessage::HideApplication => event_loop.hide_application(), + } + } + + /// Removes the webview's `(browser_id, scheme)` entries from the scheme registry. + fn remove_scheme_handler_entries(&self, child: &AppWebview) { + let mut registry = self.scheme_registry.lock().unwrap(); + for scheme in child.uri_scheme_protocols.keys() { + registry.remove(&(child.browser_id, scheme.clone())); + } + } + + fn emit_drag_drop_event( + &mut self, + window_id: WindowId, + webview_id: u32, + target: browser_client::DragDropEventTarget, + event: DragDropEvent, + ) { + match target { + browser_client::DragDropEventTarget::Window => { + self.emit_window_event(window_id, WindowEvent::DragDrop(event)); + } + browser_client::DragDropEventTarget::Webview => { + self.emit_webview_event(window_id, webview_id, WebviewEvent::DragDrop(event)); + } + } + } + + fn emit_window_event(&mut self, window_id: WindowId, event: WindowEvent) { + let Some(appwindow) = self.state.windows.get(&window_id) else { + return; + }; + let label = appwindow.label.clone(); + let listeners = appwindow.listeners.clone(); + + self.run_callback(RunEvent::WindowEvent { + label, + event: event.clone(), + }); + + { + let listeners = listeners.lock().unwrap(); + for handler in listeners.values() { + handler(&event); + } + } + } + + fn emit_webview_event(&mut self, window_id: WindowId, webview_id: u32, event: WebviewEvent) { + let Some(appwindow) = self.state.windows.get(&window_id) else { + return; + }; + let Some(child) = appwindow + .children + .iter() + .find(|child| child.webview_id == webview_id) + else { + return; + }; + let label = child.label.clone(); + let listeners = child.listeners.clone(); + + self.run_callback(RunEvent::WebviewEvent { + label, + event: event.clone(), + }); + + { + let listeners = listeners.lock().unwrap(); + for handler in listeners.values() { + handler(&event); + } + } + } + + fn request_exit(&mut self, code: Option) -> bool { + // if we already exiting, don't request exit again + if self.state.exiting { + return false; + } + + let (tx, rx) = mpsc::channel(); + self.run_callback(RunEvent::ExitRequested { code, tx }); + + if matches!(rx.try_recv(), Ok(ExitRequestedEventAction::Prevent)) { + false + } else { + self.state.exiting = true; + true + } + } + + pub(crate) fn close_window(&mut self, window_id: WindowId, event_loop: &dyn ActiveEventLoop) { + let Some(appwindow) = self.state.windows.remove(&window_id) else { + return; + }; + self + .state + .winid_id_to_window_id_map + .remove(&appwindow.window.id()); + // The window is gone from state, so BrowserClosed will not find these + // children later. Clean registry entries while we still hold them; the CEF + // shutdown drain is still enforced by live_browsers. + for child in &appwindow.children { + self.remove_scheme_handler_entries(child); + child.host.close_browser(1); + } + self.exit_if_done(event_loop); + } + + pub(crate) fn request_window_close( + &mut self, + window_id: WindowId, + event_loop: &dyn ActiveEventLoop, + ) { + // Avoid requesting window close if we already exisitng + if self.state.exiting { + self.close_window(window_id, event_loop); + return; + } + + let (tx, rx) = mpsc::channel(); + let Some(appwindow) = self.state.windows.get(&window_id) else { + return; + }; + let label = appwindow.label.clone(); + let listeners = appwindow.listeners.clone(); + + { + let listeners = listeners.lock().unwrap(); + for handler in listeners.values() { + handler(&WindowEvent::CloseRequested { + signal_tx: tx.clone(), + }); + } + } + + self.run_callback(RunEvent::WindowEvent { + label, + event: WindowEvent::CloseRequested { signal_tx: tx }, + }); + + if !matches!(rx.try_recv(), Ok(true)) { + self.close_window(window_id, event_loop); + } + } + + fn navigate_first_webview(&self, window_id: WindowId, url: &str) { + let Some(frame) = self + .state + .windows + .get(&window_id) + .and_then(|window| window.children.first()) + .and_then(|webview| webview.browser.main_frame()) + else { + return; + }; + + frame.load_url(Some(&CefString::from(url))); + } + + fn close_all_browsers(&mut self) { + // App shutdown follows the same eager bookkeeping cleanup as window + // teardown. live_browsers keeps the loop alive until CEF confirms every + // browser close through BrowserClosed. + for appwindow in self.state.windows.values() { + for child in &appwindow.children { + self.remove_scheme_handler_entries(child); + child.host.close_browser(1); + } + } + self.state.windows.clear(); + self.state.winid_id_to_window_id_map.clear(); + } + + #[cfg(target_os = "macos")] + fn set_browsers_accessibility_state(&self, enabled: bool) { + let state = if enabled { + State::ENABLED + } else { + State::DISABLED + }; + for appwindow in self.state.windows.values() { + for child in &appwindow.children { + child.host.set_accessibility_state(state); + } + } + } + + fn exit_if_done(&mut self, event_loop: &dyn ActiveEventLoop) { + if self.state.live_browsers != 0 { + return; + } + + if self.state.exiting || (self.state.windows.is_empty() && self.request_exit(None)) { + self.run_callback(RunEvent::Exit); + event_loop.exit(); + } + } + + /// Service the default GLib main context so the external message pump's GLib + /// timeout (and any GTK work CEF schedules) gets dispatched, then arm winit to + /// wake when the next tick is due. CEF is driven by that timeout firing, not + /// from here. Windows/macOS need no equivalent: their pump timers live on the + /// native loop winit already runs. + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + fn service_glib(&self, event_loop: &dyn ActiveEventLoop) { + let context = gtk::glib::MainContext::default(); + while context.pending() { + context.iteration(false); + } + if let Some(deadline) = self.context.cef_pump.next_deadline() { + event_loop.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(deadline)); + } + } +} + +impl ApplicationHandler for WinitCefApp { + fn can_create_surfaces(&mut self, event_loop: &dyn ActiveEventLoop) { + let _guard = install_current_dispatch(self, event_loop); + self.drain_messages(event_loop); + } + + fn new_events(&mut self, event_loop: &dyn ActiveEventLoop, cause: StartCause) { + let _guard = install_current_dispatch(self, event_loop); + match cause { + StartCause::Init => { + self.run_callback(RunEvent::Ready); + self.context.cef_pump.do_message_loop_work(); + } + // Match wry/tao, which emit `Resumed` on each `Poll` start cause. + StartCause::Poll => self.run_callback(RunEvent::Resumed), + _ => {} + } + } + + fn proxy_wake_up(&mut self, event_loop: &dyn ActiveEventLoop) { + let _guard = install_current_dispatch(self, event_loop); + self.drain_messages(event_loop); + } + + fn about_to_wait(&mut self, event_loop: &dyn ActiveEventLoop) { + let _guard = install_current_dispatch(self, event_loop); + // TODO: remove once migrated to winit-gtk4 + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + self.service_glib(event_loop); + self.run_callback(RunEvent::MainEventsCleared); + } + + fn window_event( + &mut self, + event_loop: &dyn ActiveEventLoop, + winit_id: WinitWindowId, + event: WinitWindowEvent, + ) { + let _guard = install_current_dispatch(self, event_loop); + let Some(window_id) = self.state.winid_id_to_window_id_map.get(&winit_id).copied() else { + return; + }; + let Some(appwindow) = self.state.windows.get_mut(&window_id) else { + return; + }; + + match event { + WinitWindowEvent::CloseRequested => self.request_window_close(window_id, event_loop), + + WinitWindowEvent::Destroyed => { + if !self.state.exiting { + self.emit_window_event(window_id, WindowEvent::Destroyed); + } + self.close_window(window_id, event_loop); + } + WinitWindowEvent::SurfaceResized(size) => { + webview::layout_app_window(appwindow); + self.emit_window_event(window_id, WindowEvent::Resized(size)); + } + WinitWindowEvent::ScaleFactorChanged { + scale_factor, + surface_size_writer, + } => { + let new_inner_size = surface_size_writer + .surface_size() + .unwrap_or_else(|_| appwindow.window.surface_size()); + webview::layout_app_window(appwindow); + self.emit_window_event( + window_id, + WindowEvent::ScaleFactorChanged { + scale_factor, + new_inner_size, + }, + ); + } + WinitWindowEvent::Moved(pos) => { + self.emit_window_event( + window_id, + WindowEvent::Moved(PhysicalPosition::new(pos.x, pos.y)), + ); + } + WinitWindowEvent::Focused(focused) => { + self.emit_window_event(window_id, WindowEvent::Focused(focused)); + } + WinitWindowEvent::ThemeChanged(theme) => { + let system_theme = winit_theme_to_tauri_theme(theme); + if let Some(explicit_theme) = appwindow.preferred_theme() { + appwindow.set_theme(Some(explicit_theme)); + } + self.emit_window_event(window_id, WindowEvent::ThemeChanged(system_theme)); + } + WinitWindowEvent::DragEntered { paths, position } => { + let event = DragDropEvent::Enter { paths, position }; + self.emit_window_event(window_id, WindowEvent::DragDrop(event)); + } + WinitWindowEvent::DragMoved { position } => { + let event = DragDropEvent::Over { position }; + self.emit_window_event(window_id, WindowEvent::DragDrop(event)); + } + WinitWindowEvent::DragDropped { paths, position } => { + let event = DragDropEvent::Drop { paths, position }; + self.emit_window_event(window_id, WindowEvent::DragDrop(event)); + } + WinitWindowEvent::DragLeft { .. } => { + self.emit_window_event(window_id, WindowEvent::DragDrop(DragDropEvent::Leave)); + } + #[cfg(target_os = "macos")] + WinitWindowEvent::RedrawRequested => { + if let Some(position) = &appwindow.attrs.traffic_light_position { + appwindow.apply_traffic_light_position(position); + } + } + _ => {} + } + } +} + +wrap_app! { + struct TauriCefApp { + context: RuntimeContext, + context_initialized: Arc, + deep_link_schemes: Vec, + command_line_args: Vec<(String, Option)>, + } + + impl App { + fn render_process_handler(&self) -> Option { + Some(ipc::TauriRenderProcessHandler::new()) + } + + fn browser_process_handler(&self) -> Option { + Some(browser_client::TauriCefBrowserProcessHandler::new( + self.context.clone(), + self.context_initialized.clone(), + self.deep_link_schemes.clone(), + )) + } + + fn on_before_command_line_processing( + &self, + _process_type: Option<&CefString>, + command_line: Option<&mut CommandLine>, + ) { + if let Some(command_line) = command_line { + for (arg, value) in &self.command_line_args { + if let Some(value) = value { + command_line.append_switch_with_value( + Some(&CefString::from(arg.as_str())), + Some(&CefString::from(value.as_str())), + ); + } else if arg.starts_with("-") { + command_line.append_switch(Some(&CefString::from(arg.as_str()))); + } else { + command_line.append_argument(Some(&CefString::from(arg.as_str()))); + } + } + } + } + } +} + +pub fn run_cef_helper_process() { + let args = cef::args::Args::new(); + + #[cfg(all(target_os = "macos", feature = "sandbox"))] + let _sandbox = { + let mut sandbox = cef::sandbox::Sandbox::new(); + sandbox.initialize(args.as_main_args()); + sandbox + }; + + #[cfg(target_os = "macos")] + let _loader = { + let loader = cef::library_loader::LibraryLoader::new(&std::env::current_exe().unwrap(), true); + assert!(loader.load()); + loader + }; + + let _ = cef::api_hash(sys::CEF_API_VERSION_LAST, 0); + let mut app = TauriCefHelperApp::new(); + let _ = cef::execute_process( + Some(args.as_main_args()), + Some(&mut app), + std::ptr::null_mut(), + ); +} + +wrap_app! { + struct TauriCefHelperApp; + + impl App { + fn render_process_handler(&self) -> Option { + Some(ipc::TauriRenderProcessHandler::new()) + } + } +} + +#[derive(Debug, Clone)] +pub struct CefRuntimeHandle { + context: RuntimeContext, +} + +impl RuntimeHandle for CefRuntimeHandle { + type Runtime = CefRuntime; + + fn create_proxy(&self) -> >::EventLoopProxy { + EventProxy { + context: self.context.clone(), + } + } + + #[cfg(target_os = "macos")] + fn set_activation_policy( + &self, + activation_policy: tauri_runtime::ActivationPolicy, + ) -> Result<()> { + let message = Message::EventLoop(EventLoopMessage::SetActivationPolicy(activation_policy)); + self.context.send_message(message) + } + + #[cfg(target_os = "macos")] + fn set_dock_visibility(&self, visible: bool) -> Result<()> { + let message = Message::EventLoop(EventLoopMessage::SetDockVisibility(visible)); + self.context.send_message(message) + } + + fn request_exit(&self, code: i32) -> Result<()> { + self.context.send_message(Message::RequestExit(code)) + } + + fn create_window) + Send + 'static>( + &self, + pending: PendingWindow, + after_window_creation: Option, + ) -> Result> { + create_window_detached(&self.context, pending, after_window_creation) + } + + fn create_webview( + &self, + window_id: WindowId, + pending: PendingWebview, + ) -> Result> { + create_webview_detached(&self.context, window_id, pending) + } + + fn run_on_main_thread(&self, f: F) -> Result<()> { + self.context.run_on_main_thread(f) + } + + fn display_handle( + &self, + ) -> std::result::Result, raw_window_handle::HandleError> { + let raw = event_loop_getter!(self, DisplayHandle) + .map_err(|_| raw_window_handle::HandleError::Unavailable)??; + // SAFETY: the descriptor was produced by the live event loop on its own + // thread; the borrowed handle is valid for as long as the runtime is. + Ok(unsafe { DisplayHandle::borrow_raw(raw.0) }) + } + + fn primary_monitor(&self) -> Option { + event_loop_getter!(self, PrimaryMonitor).ok().flatten() + } + + fn monitor_from_point(&self, x: f64, y: f64) -> Option { + let (tx, rx) = mpsc::channel(); + self + .context + .send_message(Message::EventLoop(EventLoopMessage::MonitorFromPoint( + tx, x, y, + ))) + .and_then(|_| rx.recv().map_err(|_| Error::FailedToReceiveMessage)) + .ok() + .flatten() + } + + fn available_monitors(&self) -> Vec { + event_loop_getter!(self, AvailableMonitors).unwrap_or_default() + } + + fn cursor_position(&self) -> Result> { + event_loop_getter!(self, CursorPosition)? + } + + fn set_theme(&self, theme: Option) { + let message = Message::EventLoop(EventLoopMessage::SetTheme(theme)); + let _ = self.context.send_message(message); + } + + #[cfg(target_os = "macos")] + fn show(&self) -> Result<()> { + let message = Message::EventLoop(EventLoopMessage::ShowApplication); + self.context.send_message(message) + } + + #[cfg(target_os = "macos")] + fn hide(&self) -> Result<()> { + let message = Message::EventLoop(EventLoopMessage::HideApplication); + self.context.send_message(message) + } + + fn set_device_event_filter(&self, filter: DeviceEventFilter) { + let message = Message::EventLoop(EventLoopMessage::SetDeviceEventFilter(filter)); + let _ = self.context.send_message(message); + } + + #[cfg(any(target_os = "macos", target_os = "ios"))] + fn fetch_data_store_identifiers) + Send + 'static>( + &self, + cb: F, + ) -> Result<()> { + cb(Vec::new()); + Ok(()) + } + + #[cfg(any(target_os = "macos", target_os = "ios"))] + fn remove_data_store) + Send + 'static>( + &self, + _uuid: [u8; 16], + cb: F, + ) -> Result<()> { + cb(Ok(())); + Ok(()) + } +} + +pub struct CefRuntime { + event_loop: EventLoop, + receiver: Receiver>, + context: RuntimeContext, + scheme_registry: request_handler::SchemeRegistry, + #[cfg(target_os = "macos")] + _app_delegate: Option>, +} + +impl fmt::Debug for CefRuntime { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("CefRuntime").finish() + } +} + +impl CefRuntime { + fn init( + mut event_loop_builder: EventLoopBuilder, + runtime_args: RuntimeInitArgs, + ) -> Result { + let args = cef::args::Args::new(); + + #[cfg(target_os = "macos")] + let is_helper = is_cef_helper_process(); + + #[cfg(target_os = "macos")] + let (_sandbox, _loader) = { + #[cfg(feature = "sandbox")] + let sandbox = if is_helper { + let mut sandbox = cef::sandbox::Sandbox::new(); + sandbox.initialize(args.as_main_args()); + Some(sandbox) + } else { + None + }; + #[cfg(not(feature = "sandbox"))] + let sandbox = (); + + let loader = + cef::library_loader::LibraryLoader::new(&std::env::current_exe().unwrap(), is_helper); + assert!(loader.load()); + + (sandbox, loader) + }; + + #[cfg(target_os = "macos")] + if !is_helper { + crate::platform::macos::setup_application(); + } + + // The CEF API version table must be initialized before any other CEF call + // (e.g. `args.as_cmd_line()` below), otherwise the process crashes with no + // diagnostics. + let _ = cef::api_hash(sys::CEF_API_VERSION_LAST, 0); + + // Handle CEF subprocesses (renderer/GPU/utility) before any browser-only + // setup such as building the event loop, creating cache directories, or the + // runtime context. The browser (main) process has no `type` switch; + // subprocesses are launched with one (e.g. `--type=renderer`). + let is_browser_process = args + .as_cmd_line() + .map(|cmd| cmd.has_switch(Some(&CefString::from("type"))) != 1) + .unwrap_or(true); + + if !is_browser_process { + let mut helper_app = TauriCefHelperApp::new(); + let ret = cef::execute_process( + Some(args.as_main_args()), + Some(&mut helper_app), + std::ptr::null_mut(), + ); + // A subprocess finished its work; exit with its exit code instead of + // falling through to browser runtime initialization. + std::process::exit(ret.max(0)); + } + + let mut command_line_args = Vec::new(); + let mut deep_link_schemes = Vec::new(); + let mut cache_path_override = None::; + for arg in runtime_args.platform_specific_attributes { + match arg { + RuntimeInitAttribute::CommandLineArgs { args } => command_line_args.extend(args), + RuntimeInitAttribute::DeepLinkSchemes { schemes } => deep_link_schemes.extend(schemes), + RuntimeInitAttribute::CachePath { path } => cache_path_override = Some(path), + } + } + + let cache_path = cache_path_override.unwrap_or_else(|| { + let cache_base = dirs::cache_dir().unwrap_or_else(std::env::temp_dir); + cache_base.join(&runtime_args.identifier).join("cef") + }); + let _ = create_dir_all(&cache_path); + + // Force X11 usage on Linux + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + { + command_line_args.push(("ozone-platform".to_string(), Some("x11".to_string()))); + event_loop_builder.with_x11(); + } + + #[cfg(windows)] + if let Some(hook) = runtime_args.msg_hook { + use winit::platform::windows::EventLoopBuilderExtWindows; + event_loop_builder.with_msg_hook(hook); + } + + let event_loop = event_loop_builder + .build() + .map_err(|_| Error::CreateWindow)?; + let proxy = event_loop.create_proxy(); + let (sender, receiver) = mpsc::channel(); + let context_initialized = Arc::new(AtomicBool::new(false)); + let cef_pump = CefExternalPump::new( + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + proxy.clone(), + ); + let context = RuntimeContext { + sender: sender.clone(), + proxy: proxy.clone(), + main_thread_id: std::thread::current().id(), + next_window_id: Default::default(), + next_webview_id: Default::default(), + next_window_event_id: Default::default(), + next_webview_event_id: Default::default(), + current_dispatch: Default::default(), + app_wide_theme: Default::default(), + cef_pump, + cache_path: Arc::new(cache_path.clone()), + }; + + command_line_args.push(("--enable-media-stream".to_string(), None)); + let mut app = TauriCefApp::new( + context.clone(), + context_initialized.clone(), + deep_link_schemes, + command_line_args, + ); + + // Subprocesses already exited above, so this must be the browser process; + // `execute_process` returns -1 there to signal normal startup should follow. + let ret = cef::execute_process( + Some(args.as_main_args()), + Some(&mut app), + std::ptr::null_mut(), + ); + assert_eq!( + ret, -1, + "CEF browser process unexpectedly returned from execute_process" + ); + + let settings = cef::Settings { + no_sandbox: !cfg!(feature = "sandbox") as i32, + cache_path: cache_path.to_string_lossy().to_string().as_str().into(), + external_message_pump: 1, + ..Default::default() + }; + if cef::initialize( + Some(args.as_main_args()), + Some(&settings), + Some(&mut app), + std::ptr::null_mut(), + ) != 1 + { + return Err(Error::WebviewRuntimeNotInstalled); + } + + #[cfg(target_os = "macos")] + let app_delegate = if !is_helper { + use crate::platform::macos::AppDelegateEvent; + + let context_ = context.clone(); + let handler = Box::new(move |event| match event { + AppDelegateEvent::TryTerminate => { + let _ = context_.send_message(Message::RequestExit(0)); + } + AppDelegateEvent::Reopen { + has_visible_windows, + } => { + let _ = context_.send_message(Message::Reopen { + has_visible_windows, + }); + } + AppDelegateEvent::AccessibilityChanged { enabled } => { + let _ = context_.send_message(Message::AccessibilityChanged { enabled }); + } + AppDelegateEvent::OpenURLs { urls } => { + let _ = context_.send_message(Message::Opened(urls)); + } + }); + let app_delegate = crate::platform::macos::set_application_event_handler(handler); + Some(app_delegate) + } else { + None + }; + + // Wait for the CEF context to initialize before returning, so that the runtime is ready to create browsers. + while !context_initialized.load(Ordering::SeqCst) { + context.cef_pump.do_message_loop_work(); + std::thread::sleep(Duration::from_millis(1)); + } + + Ok(Self { + event_loop, + receiver, + context, + scheme_registry: Default::default(), + #[cfg(target_os = "macos")] + _app_delegate: app_delegate, + }) + } +} + +impl Runtime for CefRuntime { + type WindowDispatcher = CefWindowDispatcher; + type WebviewDispatcher = CefWebviewDispatcher; + type Handle = CefRuntimeHandle; + type EventLoopProxy = EventProxy; + type PlatformSpecificWebviewAttribute = WebviewAtribute; + type Webview = Webview; + type PlatformSpecificInitAttribute = RuntimeInitAttribute; + type WindowOpener = NewWindowOpener; + + fn new(args: RuntimeInitArgs) -> Result { + Self::init(EventLoopBuilder::default(), args) + } + + #[cfg(any( + windows, + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + fn new_any_thread(args: RuntimeInitArgs) -> Result { + #[cfg(windows)] + use winit::platform::windows::EventLoopBuilderExtWindows; + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + use winit::platform::x11::EventLoopBuilderExtX11; + + let mut event_loop_builder = EventLoopBuilder::default(); + event_loop_builder.with_any_thread(true); + Self::init(event_loop_builder, args) + } + + fn create_proxy(&self) -> Self::EventLoopProxy { + EventProxy { + context: self.context.clone(), + } + } + + fn handle(&self) -> Self::Handle { + CefRuntimeHandle { + context: self.context.clone(), + } + } + + fn create_window) + Send + 'static>( + &self, + pending: PendingWindow, + after_window_creation: Option, + ) -> Result> { + create_window_detached(&self.context, pending, after_window_creation) + } + + fn create_webview( + &self, + window_id: WindowId, + pending: PendingWebview, + ) -> Result> { + create_webview_detached(&self.context, window_id, pending) + } + + fn primary_monitor(&self) -> Option { + event_loop_getter!(self, PrimaryMonitor).ok().flatten() + } + + fn monitor_from_point(&self, x: f64, y: f64) -> Option { + let (tx, rx) = mpsc::channel(); + self + .context + .send_message(Message::EventLoop(EventLoopMessage::MonitorFromPoint( + tx, x, y, + ))) + .and_then(|_| rx.recv().map_err(|_| Error::FailedToReceiveMessage)) + .ok() + .flatten() + } + + fn available_monitors(&self) -> Vec { + event_loop_getter!(self, AvailableMonitors).unwrap_or_default() + } + + fn cursor_position(&self) -> Result> { + event_loop_getter!(self, CursorPosition)? + } + + fn set_theme(&self, theme: Option) { + let message = Message::EventLoop(EventLoopMessage::SetTheme(theme)); + let _ = self.context.send_message(message); + } + + #[cfg(target_os = "macos")] + fn set_activation_policy(&mut self, activation_policy: tauri_runtime::ActivationPolicy) { + let message = Message::EventLoop(EventLoopMessage::SetActivationPolicy(activation_policy)); + let _ = self.context.send_message(message); + } + + #[cfg(target_os = "macos")] + fn set_dock_visibility(&mut self, visible: bool) { + let message = Message::EventLoop(EventLoopMessage::SetDockVisibility(visible)); + let _ = self.context.send_message(message); + } + + #[cfg(target_os = "macos")] + fn show(&self) { + let message = Message::EventLoop(EventLoopMessage::ShowApplication); + let _ = self.context.send_message(message); + } + + #[cfg(target_os = "macos")] + fn hide(&self) { + let message = Message::EventLoop(EventLoopMessage::HideApplication); + let _ = self.context.send_message(message); + } + + fn set_device_event_filter(&mut self, filter: DeviceEventFilter) { + self + .event_loop + .listen_device_events(device_event_filter_to_winit(filter)); + } + + fn custom_scheme_url(scheme: &str, https: bool) -> String { + format!( + "{}://{scheme}.localhost", + if https { "https" } else { "http" } + ) + } + + fn run_iteration) + 'static>(&mut self, mut callback: F) { + while let Ok(message) = self.receiver.try_recv() { + if let Message::UserEvent(event) = message { + callback(RunEvent::UserEvent(event)); + } + } + self.context.cef_pump.do_message_loop_work(); + callback(RunEvent::MainEventsCleared); + } + + fn run_return) + 'static>(self, callback: F) -> i32 { + self.run(callback); + // TODO: return the exit code from the runtime, if possible. For now, always return 0 + 0 + } + + fn run) + 'static>(self, callback: F) { + let app = WinitCefApp::new( + self.context, + self.receiver, + Box::new(callback), + self.scheme_registry, + ); + let _ = self.event_loop.run_app(app); + cef::shutdown(); + } +} diff --git a/crates/tauri-runtime-cef/src/utils.rs b/crates/tauri-runtime-cef/src/utils.rs deleted file mode 100644 index 164ecd21c874..000000000000 --- a/crates/tauri-runtime-cef/src/utils.rs +++ /dev/null @@ -1,42 +0,0 @@ -#[cfg(windows)] -pub mod windows { - use tauri_runtime::dpi::PhysicalSize; - use windows::Win32::Foundation::*; - use windows::Win32::UI::WindowsAndMessaging::*; - - pub fn inner_size(hwnd: cef::sys::HWND) -> PhysicalSize { - let hwnd = HWND(hwnd.0 as _); - let mut rect = RECT::default(); - let _ = unsafe { GetClientRect(hwnd, &mut rect) }; - - PhysicalSize::new( - (rect.right - rect.left) as u32, - (rect.bottom - rect.top) as u32, - ) - } - - /// Adjusts the given size to account for window borders, so that the resulting inner size matches the requested size. - /// - /// Expects and returns a size in physical pixels. - pub fn adjust_size(hwnd: cef::sys::HWND, size: cef::Size) -> cef::Size { - let hwnd = HWND(hwnd.0 as _); - - let mut client_rect = RECT::default(); - let _ = unsafe { GetClientRect(hwnd, &mut client_rect) }; - let client_width = client_rect.right - client_rect.left; - let client_height = client_rect.bottom - client_rect.top; - - let mut window_rect = RECT::default(); - let _ = unsafe { GetWindowRect(hwnd, &mut window_rect) }; - let window_width = window_rect.right - window_rect.left; - let window_height = window_rect.bottom - window_rect.top; - - let width_diff = window_width - client_width; - let height_diff = window_height - client_height; - - cef::Size { - width: size.width + width_diff, - height: size.height + height_diff, - } - } -} diff --git a/crates/tauri-runtime-cef/src/webview.rs b/crates/tauri-runtime-cef/src/webview.rs new file mode 100644 index 000000000000..5f31461cfab8 --- /dev/null +++ b/crates/tauri-runtime-cef/src/webview.rs @@ -0,0 +1,1587 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::collections::HashMap; +use std::sync::Arc; +use std::sync::{ + Mutex, + atomic::{AtomicI32, Ordering}, + mpsc::{self, Receiver, Sender}, +}; + +use cef::*; +use sha2::{Digest, Sha256}; +use tauri_runtime::{ + Cookie, Error, Result, Runtime, UserEvent, WebviewDispatch, WebviewEventId, + dpi::{PhysicalPosition, PhysicalSize, Position, Rect, Size}, + webview::{ + DetachedWebview, InitializationScript, PendingWebview, UriSchemeProtocolHandler, + WebviewAttributes, + }, + window::{WebviewEvent, WindowId}, +}; +use tauri_utils::{Theme, config::Color, html::normalize_script_for_csp}; +use url::Url; + +use crate::cef_impl::{client as browser_client, cookie, request_context, request_handler}; +use crate::runtime::{CefRuntime, Message, RuntimeContext, WinitCefApp}; +use crate::window::AppWindow; + +/// A handle to the native CEF browser backing a Tauri webview. +/// +/// This is the runtime-specific webview object exposed through +/// [`tauri_runtime::WebviewDispatch::with_webview`]. +#[derive(Clone)] +pub struct Webview { + browser: cef::Browser, +} + +impl Webview { + pub(crate) fn new(browser: cef::Browser) -> Self { + Self { browser } + } + + /// Returns the [`cef::Browser`] backing this webview. + /// + /// From the browser you can reach the rest of the CEF API, such as the + /// browser host, the main frame or the native window handle. + pub fn browser(&self) -> cef::Browser { + self.browser.clone() + } +} + +pub fn webview_version() -> tauri_runtime::Result { + Ok(format!( + "{}.{}.{}.{}", + cef::sys::CHROME_VERSION_MAJOR, + cef::sys::CHROME_VERSION_MINOR, + cef::sys::CHROME_VERSION_PATCH, + cef::sys::CHROME_VERSION_BUILD + )) +} + +#[inline] +fn color_to_argb(color: Color) -> u32 { + let (r, g, b, a) = color.into(); + ((a as u32) << 24) | ((r as u32) << 16) | ((g as u32) << 8) | (b as u32) +} + +/// Maps the subset of [`WebviewAttributes`] that CEF's `BrowserSettings` +/// supports. +/// +/// The following Tauri webview attributes have no per-webview equivalent in CEF +/// and are intentionally ignored here: +/// - `user_agent`: CEF only exposes a process-global user agent via +/// `CefSettings.user_agent`, which is fixed before any webview is created. +/// - `additional_browser_args`, `scroll_bar_style`, `general_autofill_enabled`: +/// WebView2 (Windows)-only concepts. +/// - `allow_link_preview`, `accept_first_mouse`: WKWebView (macOS/iOS)-only. +/// - `browser_extensions_enabled`, `extensions_path`: CEF dropped extension +/// support in the Chrome runtime. +/// - `data_store_identifier`: a WKWebView data-store concept with no CEF analog +/// (per-webview isolation is done through the request context cache path). +/// - `zoom_hotkeys_enabled`: handled by Chromium's accelerator table, not a +/// browser setting. +/// +/// `proxy_url` is handled separately via the request context preference. +fn browser_settings_from_webview_attributes( + webview_attributes: &WebviewAttributes, +) -> cef::BrowserSettings { + cef::BrowserSettings { + javascript: cef::State::from(if webview_attributes.javascript_disabled { + cef::sys::cef_state_t::STATE_DISABLED + } else { + cef::sys::cef_state_t::STATE_ENABLED + }), + javascript_access_clipboard: cef::State::from(if webview_attributes.clipboard { + cef::sys::cef_state_t::STATE_ENABLED + } else { + cef::sys::cef_state_t::STATE_DISABLED + }), + background_color: webview_attributes + .background_color + .map(color_to_argb) + .unwrap_or(0), + ..Default::default() + } +} + +#[derive(Debug, Clone)] +pub enum DevToolsProtocol { + Message(Vec), + Event { + method: String, + params: Vec, + }, + MethodResult { + message_id: i32, + success: bool, + result: Vec, + }, +} + +pub(crate) type DevToolsProtocolHandler = dyn Fn(DevToolsProtocol) + Send + Sync; +pub(crate) type WebviewEventHandler = Box; +pub(crate) type WebviewEventListeners = Arc>>; + +pub(crate) enum WebviewMessage { + AddEventListener(WebviewEventId, Box), + EvaluateScript(String), + EvaluateScriptWithCallback(String, Box), + Navigate(Url), + Reload, + GoBack, + CanGoBack(Sender>), + GoForward, + CanGoForward(Sender>), + Print, + Close, + Show, + Hide, + SetPosition(Position), + SetSize(Size), + SetBounds(Rect), + SetFocus, + Reparent(WindowId, Sender>), + SetAutoResize(bool), + SetZoom(f64), + SetBackgroundColor(Option), + ClearAllBrowsingData, + Url(Sender>), + Bounds(Sender>), + Position(Sender>>), + Size(Sender>>), + WithWebview(Box), + CookiesForUrl(Url, Sender>>>), + Cookies(Sender>>>), + SetCookie(Cookie<'static>), + DeleteCookie(Cookie<'static>), + #[cfg(any(debug_assertions, feature = "devtools"))] + OpenDevTools, + #[cfg(any(debug_assertions, feature = "devtools"))] + CloseDevTools, + #[cfg(any(debug_assertions, feature = "devtools"))] + IsDevToolsOpen(Sender), + SendDevToolsMessage(Vec, Sender>), + OnDevToolsProtocol(Arc, Sender>), +} + +/// A webview's bounds expressed as a fraction of its parent window, used to +/// reposition/resize auto-resize webviews when the parent window changes size. +#[derive(Clone, Copy)] +pub(crate) struct BoundsRate { + pub(crate) x: f32, + pub(crate) y: f32, + pub(crate) width: f32, + pub(crate) height: f32, +} + +impl Default for BoundsRate { + fn default() -> Self { + Self { + x: 0., + y: 0., + width: 1., + height: 1., + } + } +} + +pub(crate) struct AppWebview { + pub(crate) webview_id: u32, + pub(crate) label: String, + pub(crate) browser: cef::Browser, + pub(crate) browser_id: i32, + pub(crate) host: cef::BrowserHost, + pub(crate) uri_scheme_protocols: Arc>>>, + pub(crate) devtools_protocol_handlers: Arc>>>, + /// Keeps the DevTools message observer registered. Dropping this unregisters the observer. + pub(crate) devtools_observer_registration: Arc>>, + pub(crate) listeners: WebviewEventListeners, + pub(crate) bounds_rate: Option, +} + +impl AppWebview { + pub(crate) fn set_bounds(&mut self, parent_size: PhysicalSize, scale: f64, bounds: Rect) { + let position = bounds.position.to_physical::(scale); + let size = bounds.size.to_physical::(scale); + + let x = position.x; + let y = position.y; + let w = size.width as i32; + let h = size.height as i32; + + if self.bounds_rate.is_some() { + let win_w = parent_size.width.max(1) as f32; + let win_h = parent_size.height.max(1) as f32; + self.bounds_rate = Some(BoundsRate { + x: x as f32 / win_w, + y: y as f32 / win_h, + width: w as f32 / win_w, + height: h as f32 / win_h, + }); + } + + self.host.notify_move_or_resize_started(); + self.apply_physical_bounds(scale, x, y, w, h); + self.host.was_resized(); + } + + pub(crate) fn set_visible(&self, visible: bool) { + self.host.was_hidden(if visible { 0 } else { 1 }); + self.apply_visible(visible); + } + + pub fn url(&self) -> Option { + self + .browser + .main_frame() + .map(|frame| cef::CefString::from(&frame.url()).to_string()) + } +} + +impl WinitCefApp { + pub(crate) fn create_webview( + &mut self, + window_id: WindowId, + webview_id: u32, + pending: PendingWebview>, + ) -> Result<()> { + let Self { + context, + scheme_registry, + state, + .. + } = self; + let Some(appwindow) = state.windows.get_mut(&window_id) else { + return Err(Error::CreateWebview( + format!("window {window_id:?} does not exist").into(), + )); + }; + Self::build_and_attach_webview( + context, + scheme_registry, + &mut state.live_browsers, + appwindow, + webview_id, + browser_client::DragDropEventTarget::Webview, + pending, + ) + } + + /// Builds a webview and attaches it to `appwindow`, bumping `live_browsers` + /// and relaying it out. Works whether `appwindow` already lives in `state` or + /// is still being assembled, so window and child creation share one path. + pub(crate) fn build_and_attach_webview( + context: &RuntimeContext, + scheme_registry: &request_handler::SchemeRegistry, + live_browsers: &mut usize, + appwindow: &mut AppWindow, + webview_id: u32, + drag_drop_event_target: browser_client::DragDropEventTarget, + pending: PendingWebview>, + ) -> Result<()> { + let parent = appwindow.raw_handle_as_cef_handle(); + let parent_size = appwindow.window.surface_size(); + let scale = appwindow.window.scale_factor(); + let app_wide_theme = *context.app_wide_theme.lock().unwrap(); + let theme = appwindow.resolved_theme(app_wide_theme); + let Some(child) = Self::build_browser_child( + context, + scheme_registry, + appwindow.id, + webview_id, + parent, + parent_size, + scale, + theme, + drag_drop_event_target, + pending, + ) else { + return Err(Error::CreateWebview( + "failed to create CEF browser".to_string().into(), + )); + }; + + *live_browsers += 1; + appwindow.children.push(child); + layout_app_window(appwindow); + Ok(()) + } + + pub(crate) fn build_browser_child( + context: &RuntimeContext, + scheme_registry: &request_handler::SchemeRegistry, + window_id: WindowId, + webview_id: u32, + parent: cef::sys::cef_window_handle_t, + parent_size: PhysicalSize, + scale: f64, + theme: Option, + drag_drop_event_target: browser_client::DragDropEventTarget, + mut pending: PendingWebview>, + ) -> Option { + let bounds_rate = compute_child_bounds_rate( + pending.webview_attributes.bounds.as_ref(), + pending.webview_attributes.auto_resize, + parent_size, + scale, + ); + let initialization_scripts = initialization_scripts(&mut pending.webview_attributes); + let uri_scheme_protocols: Arc> = Arc::new( + pending + .uri_scheme_protocols + .into_iter() + .map(|(scheme, handler)| (scheme, Arc::new(handler))) + .collect(), + ); + let on_page_load_handler = pending.on_page_load_handler.take().map(Arc::from); + let document_title_changed_handler = + pending.document_title_changed_handler.take().map(Arc::from); + let address_changed_handler = pending.address_changed_handler.take().map(Arc::from); + let devtools_enabled = (cfg!(debug_assertions) || cfg!(feature = "devtools")) + && pending.webview_attributes.devtools.unwrap_or(true); + let drag_drop_handler_enabled = pending.webview_attributes.drag_drop_handler_enabled; + let drag_drop_state = Arc::new(Mutex::new(browser_client::DragDropState::default())); + #[cfg(any(target_os = "macos", target_os = "ios"))] + let web_content_process_terminate_handler = pending + .on_web_content_process_terminate_handler + .take() + .map(|handler| Arc::from(handler) as Arc); + #[cfg(not(any(target_os = "macos", target_os = "ios")))] + let web_content_process_terminate_handler: Option> = None; + let handlers = browser_client::TauriCefBrowserClientHandlers { + ipc_handler: pending.ipc_handler.map(Arc::from), + on_page_load_handler, + document_title_changed_handler, + navigation_handler: pending.navigation_handler.map(Arc::from), + address_changed_handler, + new_window_handler: pending.new_window_handler.map(Arc::from), + download_handler: pending.download_handler.take(), + web_content_process_terminate_handler, + }; + + let mut client = browser_client::TauriCefBrowserClient::new( + context.clone(), + window_id, + webview_id, + pending.label.clone(), + Some(pending.url.as_str().to_string()), + devtools_enabled, + drag_drop_event_target, + drag_drop_handler_enabled, + drag_drop_state, + handlers, + context.proxy.clone(), + context.sender.clone(), + ); + + // If the bounds are not specified, default to the parent window's size and position. + // aka full-window webview. + let bounds = pending.webview_attributes.bounds.unwrap_or_else(|| Rect { + position: PhysicalPosition::new(0, 0).into(), + size: parent_size.into(), + }); + #[cfg(not(target_os = "macos"))] + let bounds = bounds.to_physical::(scale); + #[cfg(target_os = "macos")] + let bounds = bounds.to_logical::(scale); + let bounds = cef::Rect { + x: bounds.position.x, + y: bounds.position.y, + width: bounds.size.width, + height: bounds.size.height, + }; + + // Let CEF pick the runtime style unless overridden per-webview. + let cef_runtime_style = pending + .platform_specific_attributes + .iter() + .map(|attr| match attr { + WebviewAtribute::RuntimeStyle { style } => match style { + RuntimeStyle::Alloy => cef::RuntimeStyle::ALLOY, + RuntimeStyle::Chrome => cef::RuntimeStyle::CHROME, + }, + }) + .next() + .unwrap_or(cef::RuntimeStyle::DEFAULT); + + let mut window_info = cef::WindowInfo::default().set_as_child(parent, &bounds); + window_info.runtime_style = cef_runtime_style; + let settings = browser_settings_from_webview_attributes(&pending.webview_attributes); + + let custom_protocol_scheme = if pending.webview_attributes.use_https_scheme { + "https" + } else { + "http" + } + .to_string(); + let custom_scheme_domain_names: Vec = uri_scheme_protocols + .keys() + .map(|scheme| format!("{scheme}.localhost")) + .collect(); + let real_initial_url = pending.url.as_str().to_string(); + let (browser_tx, browser_rx) = mpsc::channel(); + let (init_done, on_initialized) = request_context::deferred_init_continuation({ + let scheme_registry = scheme_registry.clone(); + let uri_scheme_protocols = uri_scheme_protocols.clone(); + let initialization_scripts = initialization_scripts.clone(); + let custom_protocol_scheme = custom_protocol_scheme.clone(); + let custom_scheme_domain_names = custom_scheme_domain_names.clone(); + let label = pending.label.clone(); + move |mut request_context| { + request_context::apply_theme_scheme(request_context.as_ref(), theme); + + // Create with an inert document so the BrowserHost exists before the real + // navigation; the real URL is loaded once the document-start script is set. + let initial_url = CefString::from(INITIAL_LOAD_URL); + let Some(browser) = cef::browser_host_create_browser_sync( + Some(&window_info), + Some(&mut client), + Some(&initial_url), + Some(&settings), + None, + request_context.as_mut(), + ) else { + log::error!("failed to create CEF browser for webview {label:?}"); + return; + }; + let Some(host) = browser.host() else { + log::error!("CEF browser for webview {label:?} has no host"); + return; + }; + let browser_id = browser.identifier(); + + { + let mut registry = scheme_registry.lock().unwrap(); + for (scheme, handler) in uri_scheme_protocols.iter() { + registry.insert( + (browser_id, scheme.clone()), + ( + label.clone(), + handler.clone(), + initialization_scripts.clone(), + ), + ); + } + } + + let devtools_protocol_handlers = Arc::new(Mutex::new(Vec::new())); + let pending_initial_loads: PendingInitialLoads = Arc::new(Mutex::new(HashMap::new())); + let devtools_observer_registration = Arc::new(Mutex::new(add_dev_tools_observer( + &browser, + devtools_protocol_handlers.clone(), + pending_initial_loads.clone(), + ))); + load_initial_url_after_registering_initialization_scripts( + &browser, + &initialization_scripts, + &custom_protocol_scheme, + &custom_scheme_domain_names, + &real_initial_url, + &pending_initial_loads, + ); + + browser_tx + .send(AppWebview { + webview_id, + label, + browser, + browser_id, + host, + uri_scheme_protocols, + devtools_protocol_handlers, + devtools_observer_registration, + listeners: Default::default(), + bounds_rate, + }) + .expect("failed to send initialized CEF browser"); + } + }); + let request_context = request_context::request_context_from_webview_attributes( + &context.cache_path, + &pending.webview_attributes, + uri_scheme_protocols.keys(), + &custom_protocol_scheme, + scheme_registry.clone(), + on_initialized, + ); + if request_context.is_none() { + init_done.store(true, Ordering::SeqCst); + } + request_context::wait_for_deferred_init(&init_done); + + // `None` here means browser creation failed (or the request context never + // initialized); the continuation logs the reason. Soft-fail instead of + // taking down the whole process. + browser_rx.recv().ok() + } + + pub(crate) fn handle_webview_message( + &mut self, + window_id: WindowId, + webview_id: u32, + message: WebviewMessage, + ) { + // If the runtime is exiting, don't process any more messages to avoid macOS crash on exit. + if self.state.exiting { + return; + } + + let Some(appwindow) = self.state.windows.get_mut(&window_id) else { + return; + }; + let Some(child) = appwindow + .children + .iter_mut() + .find(|child| child.webview_id == webview_id) + else { + return; + }; + + match message { + WebviewMessage::EvaluateScript(script) => { + if let Some(frame) = child.browser.main_frame() { + let script = cef::CefString::from(script.as_str()); + let url = cef::CefString::from(""); + frame.execute_java_script(Some(&script), Some(&url), 0); + } + } + WebviewMessage::EvaluateScriptWithCallback(script, callback) => { + let host = &child.host; + let message_id = self.context.next_webview_event_id() as i32 + 1; + let message_id = Arc::new(AtomicI32::new(message_id)); + let callback = Arc::new(Mutex::new(Some(callback))); + let registration = Arc::new(Mutex::new(None)); + let mut observer = EvalScriptWithCallbackDevToolsObserver::new( + message_id.clone(), + callback.clone(), + registration.clone(), + ); + + if let Some(observer_registration) = + host.add_dev_tools_message_observer(Some(&mut observer)) + { + *registration.lock().unwrap() = Some(observer_registration); + + let message = serde_json::json!({ + "id": message_id.load(Ordering::Relaxed), + "method": "Runtime.evaluate", + "params": { + "expression": script, + "returnByValue": true, + } + }) + .to_string(); + + if host.send_dev_tools_message(Some(message.as_bytes())) != 1 { + let _ = registration.lock().unwrap().take(); + if let Some(callback) = callback.lock().unwrap().take() { + callback(String::new()); + } + } + } else if let Some(callback) = callback.lock().unwrap().take() { + callback(String::new()); + } + } + WebviewMessage::Navigate(url) => { + if let Some(frame) = child.browser.main_frame() { + frame.load_url(Some(&cef::CefString::from(url.as_str()))); + } + } + WebviewMessage::Reload => child.browser.reload(), + WebviewMessage::GoBack => child.browser.go_back(), + WebviewMessage::CanGoBack(tx) => _ = tx.send(Ok(child.browser.can_go_back() == 1)), + WebviewMessage::GoForward => child.browser.go_forward(), + WebviewMessage::CanGoForward(tx) => _ = tx.send(Ok(child.browser.can_go_forward() == 1)), + WebviewMessage::Close => child.host.close_browser(0), + WebviewMessage::SetBounds(bounds) => { + let parent_size = appwindow.window.surface_size(); + let scale = appwindow.window.scale_factor(); + child.set_bounds(parent_size, scale, bounds); + } + WebviewMessage::SetSize(size) => { + let parent_size = appwindow.window.surface_size(); + let scale = appwindow.window.scale_factor(); + let bounds = child.bounds().unwrap_or_default(); + let new_bounds = Rect { + position: bounds.position, + size, + }; + child.set_bounds(parent_size, scale, new_bounds); + } + WebviewMessage::SetPosition(position) => { + let parent_size = appwindow.window.surface_size(); + let scale = appwindow.window.scale_factor(); + let bounds = child.bounds().unwrap_or_default(); + let new_bounds = Rect { + position, + size: bounds.size, + }; + child.set_bounds(parent_size, scale, new_bounds); + } + WebviewMessage::SetFocus => child.host.set_focus(1), + WebviewMessage::Url(tx) => { + let url = child.url().unwrap_or_default(); + let _ = tx.send(Ok(url)); + } + WebviewMessage::Bounds(tx) => { + let bounds = child.bounds().ok_or(Error::FailedToSendMessage); + let _ = tx.send(bounds); + } + WebviewMessage::Position(tx) => { + let bounds = child.bounds().ok_or(Error::FailedToSendMessage); + let position = bounds.map(|b| b.position); + let position = position.map(|p| p.to_physical::(appwindow.window.scale_factor())); + let _ = tx.send(position); + } + WebviewMessage::Size(tx) => { + let bounds = child.bounds().ok_or(Error::FailedToSendMessage); + let size = bounds.map(|b| b.size.to_physical::(appwindow.window.scale_factor())); + let _ = tx.send(size); + } + WebviewMessage::WithWebview(f) => f(Webview::new(child.browser.clone())), + WebviewMessage::Print => child.host.print(), + WebviewMessage::AddEventListener(event_id, handler) => { + child.listeners.lock().unwrap().insert(event_id, handler); + } + WebviewMessage::Show => child.set_visible(true), + WebviewMessage::Hide => child.set_visible(false), + WebviewMessage::SetZoom(scale_factor) => { + // CEF uses a logarithmic zoom level where percentage = 1.2^level + // (Chromium's kTextSizeMultiplierRatio). Convert from Tauri linear + // scale factor (1.0 = 100%) to CEF's level (0.0 = 100%) + const CEF_ZOOM_BASE: f64 = 1.2; + let zoom_level = if scale_factor > 0.0 { + scale_factor.ln() / CEF_ZOOM_BASE.ln() + } else { + 0.0 + }; + child.host.set_zoom_level(zoom_level); + } + WebviewMessage::SetAutoResize(auto_resize) => { + if auto_resize { + let bounds = child.bounds(); + let parent_size = appwindow.window.surface_size(); + let scale = appwindow.window.scale_factor(); + child.bounds_rate = compute_child_bounds_rate(bounds.as_ref(), true, parent_size, scale); + } else { + child.bounds_rate = None; + } + } + WebviewMessage::SetBackgroundColor(color) => child.set_background_color(color), + WebviewMessage::ClearAllBrowsingData => { + if let Some(manager) = child.cookie_manager() { + manager.delete_cookies(None, None, None); + manager.flush_store(None); + } + if let Some(request_context) = child.host.request_context() { + request_context.clear_http_cache(None); + } + } + WebviewMessage::CookiesForUrl(url, tx) => { + if let Some(manager) = child.cookie_manager() { + cookie::visit_url_cookies(manager, url, tx); + } else { + let _ = tx.send(Ok(Vec::new())); + } + } + WebviewMessage::Cookies(tx) => { + if let Some(manager) = child.cookie_manager() { + cookie::visit_all_cookies(manager, tx); + } else { + let _ = tx.send(Ok(Vec::new())); + } + } + WebviewMessage::SetCookie(cookie) => { + if let Some(manager) = child.cookie_manager() { + let url = child.url(); + cookie::set_cookie(manager, url, cookie); + } + } + WebviewMessage::DeleteCookie(cookie) => { + if let Some(manager) = child.cookie_manager() { + let url = child.url(); + cookie::delete_cookie(manager, url, cookie); + } + } + WebviewMessage::Reparent(target_window_id, tx) => { + if window_id == target_window_id { + let _ = tx.send(Ok(())); + return; + } + + if !self.state.windows.contains_key(&target_window_id) { + let _ = tx.send(Err(Error::WindowNotFound)); + return; + } + + let Some(mut child) = self + .state + .windows + .get_mut(&window_id) + .and_then(|appwindow| { + appwindow + .children + .iter() + .position(|child| child.webview_id == webview_id) + .map(|index| appwindow.children.remove(index)) + }) + else { + let _ = tx.send(Err(Error::WindowNotFound)); + return; + }; + + let Some(target_appwindow) = self.state.windows.get_mut(&target_window_id) else { + let _ = tx.send(Err(Error::WindowNotFound)); + return; + }; + + let bounds = child.bounds().unwrap_or_else(|| Rect { + position: PhysicalPosition::new(0, 0).into(), + size: target_appwindow.window.surface_size().into(), + }); + child.reparent(target_appwindow); + child.set_bounds( + target_appwindow.window.surface_size(), + target_appwindow.window.scale_factor(), + bounds, + ); + + target_appwindow.children.push(child); + let _ = tx.send(Ok(())); + } + #[cfg(any(debug_assertions, feature = "devtools"))] + WebviewMessage::OpenDevTools => child.host.show_dev_tools(None, None, None, None), + #[cfg(any(debug_assertions, feature = "devtools"))] + WebviewMessage::CloseDevTools => child.host.close_dev_tools(), + #[cfg(any(debug_assertions, feature = "devtools"))] + WebviewMessage::IsDevToolsOpen(tx) => _ = tx.send(child.host.has_dev_tools() == 1), + WebviewMessage::SendDevToolsMessage(message, tx) => { + let result = child.host.send_dev_tools_message(Some(&message)); + let _ = tx.send(if result == 1 { + Ok(()) + } else { + Err(Error::FailedToSendMessage) + }); + } + WebviewMessage::OnDevToolsProtocol(handler, tx) => { + child + .devtools_protocol_handlers + .lock() + .unwrap() + .push(handler); + + let needs_devtools_observer = child + .devtools_observer_registration + .lock() + .unwrap() + .is_none(); + if needs_devtools_observer { + if let Some(registration) = add_dev_tools_observer( + &child.browser, + child.devtools_protocol_handlers.clone(), + Arc::new(Mutex::new(HashMap::new())), + ) { + *child.devtools_observer_registration.lock().unwrap() = Some(registration); + let _ = tx.send(Ok(())); + } else { + let _ = tx.send(Err(Error::FailedToSendMessage)); + } + } else { + let _ = tx.send(Ok(())); + } + } + } + } +} + +#[derive(Clone, Copy, Debug)] +pub enum RuntimeStyle { + Alloy, + Chrome, +} + +#[derive(Debug)] +pub enum WebviewAtribute { + RuntimeStyle { style: RuntimeStyle }, +} + +unsafe impl Send for WebviewAtribute {} +unsafe impl Sync for WebviewAtribute {} + +#[derive(Debug, Clone)] +pub struct CefInitScript { + pub(crate) script: String, + pub(crate) hash: String, + for_main_frame_only: bool, +} + +impl CefInitScript { + fn new(script: InitializationScript) -> Self { + let mut hasher = Sha256::new(); + hasher.update(normalize_script_for_csp(script.script.as_bytes())); + let hash = format!( + "'sha256-{}'", + base64::Engine::encode( + &base64::engine::general_purpose::STANDARD, + hasher.finalize() + ) + ); + Self { + script: script.script, + hash, + for_main_frame_only: script.for_main_frame_only, + } + } +} + +pub(crate) fn initialization_scripts(attrs: &mut WebviewAttributes) -> Arc> { + let mut initialization_scripts = Vec::new(); + + if attrs.drag_drop_handler_enabled { + let drag_script = browser_client::drag_drop_initialization_script(); + initialization_scripts.push(CefInitScript::new(drag_script)); + } + + initialization_scripts.extend( + std::mem::take(&mut attrs.initialization_scripts) + .into_iter() + .map(CefInitScript::new), + ); + + Arc::new(initialization_scripts) +} + +#[derive(Debug, Clone)] +pub struct CefWebviewDispatcher { + pub(crate) window_id: Arc>, + pub(crate) webview_id: u32, + pub(crate) context: RuntimeContext, +} + +impl CefWebviewDispatcher { + pub fn send_dev_tools_message(&self, message: &[u8]) -> Result<()> { + let (tx, rx) = mpsc::channel(); + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::SendDevToolsMessage(message.to_vec(), tx), + })?; + rx.recv().map_err(|_| Error::FailedToReceiveMessage)? + } + + pub fn on_dev_tools_protocol( + &self, + f: F, + ) -> Result<()> { + let (tx, rx) = mpsc::channel(); + let handler = + Arc::new(move |protocol: DevToolsProtocol| f(protocol)) as Arc; + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::OnDevToolsProtocol(handler, tx), + })?; + rx.recv().map_err(|_| Error::FailedToReceiveMessage)? + } +} + +pub(crate) fn create_webview_detached( + context: &RuntimeContext, + window_id: WindowId, + pending: PendingWebview>, +) -> Result>> { + let label = pending.label.clone(); + let webview_id = context.next_webview_id(); + let (result_tx, result_rx) = mpsc::channel(); + context.send_message(Message::CreateWebview { + window_id, + webview_id, + pending: Box::new(pending), + result_tx, + })?; + // Block until the event loop has created the browser so a creation failure + // is surfaced to the caller instead of leaving a detached, dead webview. + result_rx + .recv() + .map_err(|_| Error::FailedToReceiveMessage)??; + Ok(DetachedWebview { + label, + dispatcher: CefWebviewDispatcher { + window_id: Arc::new(Mutex::new(window_id)), + webview_id, + context: context.clone(), + }, + }) +} + +fn getter( + context: &RuntimeContext, + message: Message, + receiver: Receiver>, +) -> Result { + context.send_message(message)?; + receiver.recv().map_err(|_| Error::FailedToReceiveMessage)? +} + +macro_rules! webview_getter { + ($self:ident, $variant:ident) => {{ + let (tx, rx) = mpsc::channel(); + getter( + &$self.context, + Message::Webview { + window_id: *$self.window_id.lock().unwrap(), + webview_id: $self.webview_id, + message: WebviewMessage::$variant(tx), + }, + rx, + ) + }}; +} + +impl WebviewDispatch for CefWebviewDispatcher { + type Runtime = CefRuntime; + + fn run_on_main_thread(&self, f: F) -> Result<()> { + self.context.run_on_main_thread(f) + } + + fn on_webview_event(&self, f: F) -> WebviewEventId { + let id = self.context.next_webview_event_id(); + let _ = self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::AddEventListener(id, Box::new(f)), + }); + id + } + + fn with_webview>::Webview) + Send + 'static>( + &self, + f: F, + ) -> Result<()> { + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::WithWebview(Box::new(f)), + }) + } + + #[cfg(any(debug_assertions, feature = "devtools"))] + fn open_devtools(&self) { + let _ = self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::OpenDevTools, + }); + } + + #[cfg(any(debug_assertions, feature = "devtools"))] + fn close_devtools(&self) { + let _ = self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::CloseDevTools, + }); + } + + #[cfg(any(debug_assertions, feature = "devtools"))] + fn is_devtools_open(&self) -> Result { + let (tx, rx) = mpsc::channel(); + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::IsDevToolsOpen(tx), + })?; + rx.recv().map_err(|_| Error::FailedToReceiveMessage) + } + + fn url(&self) -> Result { + webview_getter!(self, Url) + } + + fn bounds(&self) -> Result { + webview_getter!(self, Bounds) + } + + fn position(&self) -> Result> { + webview_getter!(self, Position) + } + + fn size(&self) -> Result> { + webview_getter!(self, Size) + } + + fn navigate(&self, url: Url) -> Result<()> { + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::Navigate(url), + }) + } + + fn reload(&self) -> Result<()> { + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::Reload, + }) + } + + fn go_back(&self) -> Result<()> { + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::GoBack, + }) + } + + fn can_go_back(&self) -> Result { + webview_getter!(self, CanGoBack) + } + + fn go_forward(&self) -> Result<()> { + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::GoForward, + }) + } + + fn can_go_forward(&self) -> Result { + webview_getter!(self, CanGoForward) + } + + fn print(&self) -> Result<()> { + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::Print, + }) + } + + fn close(&self) -> Result<()> { + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::Close, + }) + } + + fn set_bounds(&self, bounds: Rect) -> Result<()> { + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::SetBounds(bounds), + }) + } + + fn set_size(&self, size: Size) -> Result<()> { + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::SetSize(size), + }) + } + + fn set_position(&self, position: Position) -> Result<()> { + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::SetPosition(position), + }) + } + + fn set_focus(&self) -> Result<()> { + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::SetFocus, + }) + } + + fn hide(&self) -> Result<()> { + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::Hide, + }) + } + + fn show(&self) -> Result<()> { + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::Show, + }) + } + + fn eval_script>(&self, script: S) -> Result<()> { + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::EvaluateScript(script.into()), + }) + } + + fn eval_script_with_callback>( + &self, + script: S, + callback: impl Fn(String) + Send + 'static, + ) -> Result<()> { + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::EvaluateScriptWithCallback(script.into(), Box::new(callback)), + }) + } + + fn reparent(&self, window_id: WindowId) -> Result<()> { + let (tx, rx) = mpsc::channel(); + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::Reparent(window_id, tx), + })?; + let result = rx.recv().map_err(|_| Error::FailedToReceiveMessage)?; + if result.is_ok() { + *self.window_id.lock().unwrap() = window_id; + } + result + } + + fn cookies_for_url(&self, url: Url) -> Result>> { + let (tx, rx) = mpsc::channel(); + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::CookiesForUrl(url, tx), + })?; + rx.recv().map_err(|_| Error::FailedToReceiveMessage)? + } + + fn cookies(&self) -> Result>> { + webview_getter!(self, Cookies) + } + + fn set_cookie(&self, cookie: Cookie<'_>) -> Result<()> { + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::SetCookie(cookie.into_owned()), + }) + } + + fn delete_cookie(&self, cookie: Cookie<'_>) -> Result<()> { + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::DeleteCookie(cookie.into_owned()), + }) + } + + fn set_auto_resize(&self, auto_resize: bool) -> Result<()> { + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::SetAutoResize(auto_resize), + }) + } + + fn set_zoom(&self, scale_factor: f64) -> Result<()> { + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::SetZoom(scale_factor), + }) + } + + fn set_background_color(&self, color: Option) -> Result<()> { + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::SetBackgroundColor(color), + }) + } + + fn clear_all_browsing_data(&self) -> Result<()> { + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::ClearAllBrowsingData, + }) + } +} + +/// Reposition every child webview to follow the parent window size. +/// +/// Children with a bounds rate (auto-resize / window-filling) are recomputed +/// from the current window size; children with fixed bounds keep whatever bounds +/// they were last given. +pub(crate) fn layout_app_window(appwindow: &AppWindow) { + let parent_size = appwindow.window.surface_size(); + let win_w = parent_size.width as f32; + let win_h = parent_size.height as f32; + let scale = appwindow.window.scale_factor(); + for child in &appwindow.children { + let Some(rate) = child.bounds_rate else { + continue; + }; + let x = (rate.x * win_w).round() as i32; + let y = (rate.y * win_h).round() as i32; + let w = (rate.width * win_w).round() as i32; + let h = (rate.height * win_h).round() as i32; + child.host.notify_move_or_resize_started(); + child.apply_physical_bounds(scale, x, y, w, h); + child.host.was_resized(); + } +} + +/// Compute the bounds rate of a child webview relative to its parent window. +/// +/// For webiews filling the window, default rate is used, otherwise the rate is computed from the current bounds and parent size +/// if auto_resize is enabled, otherwise None is returned. +pub(crate) fn compute_child_bounds_rate( + bounds: Option<&Rect>, + auto_resize: bool, + parent_size: PhysicalSize, + scale: f64, +) -> Option { + let Some(bounds) = bounds else { + return Some(BoundsRate::default()); + }; + + if !auto_resize { + return None; + } + + let min_w = parent_size.width.max(1) as i32; + let min_h = parent_size.height.max(1) as i32; + + let pos = bounds.position.to_physical::(scale); + let size = bounds.size.to_physical::(scale); + + let x = pos.x; + let y = pos.y; + let w = size.width; + let h = size.height; + + Some(BoundsRate { + x: x as f32 / min_w as f32, + y: y as f32 / min_h as f32, + width: w as f32 / min_w as f32, + height: h as f32 / min_h as f32, + }) +} + +pub(crate) const INITIAL_LOAD_URL: &str = concat!( + "data:text/html;charset=utf-8,", + "%3C!doctype%20html%3E", + "%3Chtml%20data-tauri-cef-internal%3D%22initial-load%22%3E", + "%3Chead%3E", + "%3Cmeta%20charset%3D%22utf-8%22%3E", + "%3Ctitle%3ETauri%20CEF%20Initial%20Load%3C%2Ftitle%3E", + "%3C%2Fhead%3E", + "%3Cbody%20data-tauri-cef-internal%3D%22initial-load%22%3E", + "%3C!--%20Tauri%20CEF%20internal%20initial%20load%20placeholder%20--%3E", + "%3C%2Fbody%3E", + "%3C%2Fhtml%3E", +); +static NEXT_INIT_SCRIPT_DEVTOOLS_MESSAGE_ID: AtomicI32 = AtomicI32::new(1_000_000); + +/// Maps a pending `Page.addScriptToEvaluateOnNewDocument` CDP message id to the +/// `(browser, real_url)` whose real navigation is deferred until that message is +/// acknowledged. +pub(crate) type PendingInitialLoads = Arc>>; + +cef::wrap_dev_tools_message_observer! { + struct TauriDevToolsProtocolObserver { + handlers: Arc>>>, + pending_initial_loads: PendingInitialLoads, + } + + impl DevToolsMessageObserver { + fn on_dev_tools_message( + &self, + _browser: Option<&mut cef::Browser>, + message: Option<&[u8]>, + ) -> std::os::raw::c_int { + if let Some(message) = message { + let protocol = DevToolsProtocol::Message(message.to_vec()); + if let Ok(handlers) = self.handlers.lock() { + for handler in handlers.iter() { + handler(protocol.clone()); + } + } + } + 0 + } + + fn on_dev_tools_method_result( + &self, + _browser: Option<&mut Browser>, + message_id: std::os::raw::c_int, + success: std::os::raw::c_int, + result: Option<&[u8]>, + ) { + // The real navigation was deferred until the document-start script was + // registered; this result acknowledges that, so kick off the real load. + if let Some((browser, initial_url)) = self + .pending_initial_loads + .lock() + .unwrap() + .remove(&message_id) + { + post_load_initial_url(browser, initial_url); + } + + let protocol = DevToolsProtocol::MethodResult { + message_id, + success: success != 0, + result: result.map(|r| r.to_vec()).unwrap_or_default(), + }; + if let Ok(handlers) = self.handlers.lock() { + for handler in handlers.iter() { + handler(protocol.clone()); + } + } + } + + fn on_dev_tools_event( + &self, + _browser: Option<&mut Browser>, + method: Option<&CefString>, + params: Option<&[u8]>, + ) { + let protocol = DevToolsProtocol::Event { + method: method.map(|m| format!("{m}")).unwrap_or_default(), + params: params.map(|p| p.to_vec()).unwrap_or_default(), + }; + if let Ok(handlers) = self.handlers.lock() { + for handler in handlers.iter() { + handler(protocol.clone()); + } + } + } + } +} + +fn runtime_evaluate_result_to_json(result: Option<&[u8]>) -> String { + let Some(result) = result else { + return String::new(); + }; + let Ok(result) = serde_json::from_slice::(result) else { + return String::new(); + }; + + if result.get("exceptionDetails").is_some() { + return String::new(); + } + + let remote_object = result.get("result").unwrap_or(&result); + remote_object + .get("value") + .and_then(|value| serde_json::to_string(value).ok()) + .unwrap_or_default() +} + +type EvalScriptCallback = Box; + +cef::wrap_dev_tools_message_observer! { + struct EvalScriptWithCallbackDevToolsObserver { + message_id: Arc, + callback: Arc>>, + registration: Arc>>, + } + + impl DevToolsMessageObserver { + fn on_dev_tools_method_result( + &self, + _browser: Option<&mut Browser>, + message_id: std::os::raw::c_int, + success: std::os::raw::c_int, + result: Option<&[u8]>, + ) { + if message_id != self.message_id.load(Ordering::Relaxed) { + return; + } + + let Some(callback) = self.callback.lock().unwrap().take() else { + return; + }; + + let result = if success != 0 { + runtime_evaluate_result_to_json(result) + } else { + String::new() + }; + callback(result); + + let _ = self.registration.lock().unwrap().take(); + } + } +} + +/// Registers a DevTools protocol observer. Returns the [`cef::Registration`] which must be +/// kept alive for the observer to stay registered. The observer is unregistered when +/// the Registration is dropped. +pub(crate) fn add_dev_tools_observer( + browser: &Browser, + handlers: Arc>>>, + pending_initial_loads: PendingInitialLoads, +) -> Option { + browser.host().and_then(|host| { + let mut observer = TauriDevToolsProtocolObserver::new(handlers, pending_initial_loads); + host.add_dev_tools_message_observer(Some(&mut observer)) + }) +} + +fn devtools_initialization_script_source( + initialization_scripts: &[CefInitScript], + custom_protocol_scheme: &str, + custom_scheme_domain_names: &[String], +) -> Option { + if initialization_scripts.is_empty() { + return None; + } + + let custom_protocol = serde_json::to_string(&format!("{custom_protocol_scheme}:")).ok()?; + let custom_domains = serde_json::to_string(custom_scheme_domain_names).ok()?; + let mut source = format!( + r#"{{ + const __TAURI_CEF_INIT_CUSTOM_PROTOCOL__ = {custom_protocol}; + const __TAURI_CEF_INIT_CUSTOM_DOMAINS__ = new Set({custom_domains}); + const __TAURI_CEF_INIT_IS_CUSTOM_PROTOCOL__ = + location.protocol === __TAURI_CEF_INIT_CUSTOM_PROTOCOL__ + && __TAURI_CEF_INIT_CUSTOM_DOMAINS__.has(location.hostname); + const __TAURI_CEF_INIT_IS_MAIN_FRAME__ = (() => {{ + try {{ + return window.top === window; + }} catch (_) {{ + return false; + }} + }})(); +"# + ); + + for init_script in initialization_scripts { + source.push_str(" if (!__TAURI_CEF_INIT_IS_CUSTOM_PROTOCOL__"); + if init_script.for_main_frame_only { + source.push_str(" && __TAURI_CEF_INIT_IS_MAIN_FRAME__"); + } + source.push_str(") {\n"); + source.push_str(init_script.script.as_str()); + source.push_str("\n }\n"); + } + + source.push_str("}\n"); + Some(source) +} + +fn register_initialization_scripts( + browser: &Browser, + initialization_scripts: &[CefInitScript], + custom_protocol_scheme: &str, + custom_scheme_domain_names: &[String], + initial_url: String, + pending_initial_loads: &PendingInitialLoads, +) -> bool { + let Some(source) = devtools_initialization_script_source( + initialization_scripts, + custom_protocol_scheme, + custom_scheme_domain_names, + ) else { + return false; + }; + let Some(host) = browser.host() else { + return false; + }; + + let page_enable_message_id = NEXT_INIT_SCRIPT_DEVTOOLS_MESSAGE_ID.fetch_add(1, Ordering::Relaxed); + let page_enable_message = serde_json::json!({ + "id": page_enable_message_id, + "method": "Page.enable", + "params": {} + }) + .to_string(); + let _ = host.send_dev_tools_message(Some(page_enable_message.as_bytes())); + + let message_id = NEXT_INIT_SCRIPT_DEVTOOLS_MESSAGE_ID.fetch_add(1, Ordering::Relaxed); + let message = serde_json::json!({ + "id": message_id, + "method": "Page.addScriptToEvaluateOnNewDocument", + "params": { + "source": source, + } + }) + .to_string(); + + pending_initial_loads + .lock() + .unwrap() + .insert(message_id, (browser.clone(), initial_url)); + if host.send_dev_tools_message(Some(message.as_bytes())) == 1 { + true + } else { + pending_initial_loads.lock().unwrap().remove(&message_id); + false + } +} + +wrap_task! { + struct LoadInitialUrlTask { + browser: Browser, + initial_url: String, + } + + impl Task { + fn execute(&self) { + load_initial_url(&self.browser, &self.initial_url); + } + } +} + +fn post_load_initial_url(browser: Browser, initial_url: String) { + let mut task = LoadInitialUrlTask::new(browser, initial_url); + cef::post_task(sys::cef_thread_id_t::TID_UI.into(), Some(&mut task)); +} + +// Browsers are created with an inert internal document so the BrowserHost exists +// before the app's real first navigation starts. That gives us a chance to +// register the CDP document-start script for remote/cross-site navigations; the +// custom-protocol path still injects into HTML because CEF does not apply this +// CDP hook to those documents reliably. +// +// The real load is posted as a CEF UI task instead of performed inline. This +// keeps the browser creation/CDP setup stack from re-entering navigation. +pub(crate) fn load_initial_url_after_registering_initialization_scripts( + browser: &Browser, + initialization_scripts: &[CefInitScript], + custom_protocol_scheme: &str, + custom_scheme_domain_names: &[String], + initial_url: &str, + pending_initial_loads: &PendingInitialLoads, +) { + let browser_for_callback = browser.clone(); + let initial_url = initial_url.to_string(); + let is_waiting_for_initialization_scripts = register_initialization_scripts( + browser, + initialization_scripts, + custom_protocol_scheme, + custom_scheme_domain_names, + initial_url.clone(), + pending_initial_loads, + ); + + if !is_waiting_for_initialization_scripts { + post_load_initial_url(browser_for_callback, initial_url); + } +} + +fn load_initial_url(browser: &Browser, initial_url: &str) { + if let Some(frame) = browser.main_frame() { + frame.load_url(Some(&CefString::from(initial_url))); + } +} diff --git a/crates/tauri-runtime-cef/src/window.rs b/crates/tauri-runtime-cef/src/window.rs new file mode 100644 index 000000000000..2066a3a96058 --- /dev/null +++ b/crates/tauri-runtime-cef/src/window.rs @@ -0,0 +1,1465 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{ + collections::HashMap, + sync::{ + Arc, Mutex, + mpsc::{self, Receiver, Sender}, + }, +}; + +use cef::ImplBrowserHost; +use raw_window_handle::HasWindowHandle; +use raw_window_handle::RawWindowHandle; +use tauri_runtime::{ + Error, Icon, ProgressBarState, Result, UserAttentionType, UserEvent, WindowDispatch, + WindowEventId, + dpi::{PhysicalPosition, PhysicalSize, Position, Size}, + monitor::Monitor, + webview::{DetachedWebview, PendingWebview}, + window::{ + CursorIcon, DetachedWindow, DetachedWindowWebview, PendingWindow, RawWindow, WindowEvent, + WindowId, WindowSizeConstraints, + }, +}; +use tauri_utils::{Theme, config::Color}; +use winit::{ + event_loop::ActiveEventLoop, + monitor::{Fullscreen, MonitorHandle}, + window::{Window as WinitWindow, WindowAttributes, WindowLevel}, +}; + +use crate::platform::{EventLoopExt, MonitorExt}; +#[cfg(any(windows, target_os = "macos"))] +use std::marker::PhantomData; +#[cfg(target_os = "macos")] +use winit::platform::macos::WindowExtMacOS; +#[cfg(windows)] +use winit::platform::windows::WindowExtWindows; + +use crate::{ + cef_impl::{client as browser_client, request_context}, + runtime::{AfterWindowCreationCallback, CefRuntime, Message, RuntimeContext, WinitCefApp}, + webview::{AppWebview, CefWebviewDispatcher, create_webview_detached}, + window_builder::WindowBuilderWrapper, +}; + +pub(crate) struct SendRawWindowHandle(pub raw_window_handle::RawWindowHandle); +unsafe impl Send for SendRawWindowHandle {} + +pub(crate) struct SendRawDisplayHandle(pub raw_window_handle::RawDisplayHandle); +unsafe impl Send for SendRawDisplayHandle {} + +type WindowEventListener = Box; +type WindowEventListeners = Arc>>; + +pub(crate) fn tauri_theme_to_winit_theme(theme: Option) -> Option { + theme.map(|theme| match theme { + Theme::Light => winit::window::Theme::Light, + Theme::Dark => winit::window::Theme::Dark, + _ => winit::window::Theme::Light, + }) +} + +pub(crate) fn winit_theme_to_tauri_theme(theme: winit::window::Theme) -> Theme { + match theme { + winit::window::Theme::Light => Theme::Light, + winit::window::Theme::Dark => Theme::Dark, + } +} + +fn tauri_resize_direction_to_winit( + direction: tauri_runtime::ResizeDirection, +) -> winit::window::ResizeDirection { + match direction { + tauri_runtime::ResizeDirection::East => winit::window::ResizeDirection::East, + tauri_runtime::ResizeDirection::North => winit::window::ResizeDirection::North, + tauri_runtime::ResizeDirection::NorthEast => winit::window::ResizeDirection::NorthEast, + tauri_runtime::ResizeDirection::NorthWest => winit::window::ResizeDirection::NorthWest, + tauri_runtime::ResizeDirection::South => winit::window::ResizeDirection::South, + tauri_runtime::ResizeDirection::SouthEast => winit::window::ResizeDirection::SouthEast, + tauri_runtime::ResizeDirection::SouthWest => winit::window::ResizeDirection::SouthWest, + tauri_runtime::ResizeDirection::West => winit::window::ResizeDirection::West, + } +} + +fn calculate_window_center_position( + window_size: PhysicalSize, + monitor: &MonitorHandle, +) -> PhysicalPosition { + let work_area = monitor.work_area(); + PhysicalPosition::new( + work_area.position.x + ((work_area.size.width as i32 - window_size.width as i32).max(0) / 2), + work_area.position.y + ((work_area.size.height as i32 - window_size.height as i32).max(0) / 2), + ) +} + +fn find_monitor_for_position( + monitors: impl Iterator, + position: Position, +) -> Option { + monitors.into_iter().find(|monitor| { + let Some(monitor_position) = monitor.position() else { + return false; + }; + let Some(video_mode) = monitor.current_video_mode() else { + return false; + }; + + let monitor_size = video_mode.size(); + let position = position.to_physical::(monitor.scale_factor()); + + monitor_position.x <= position.x + && position.x < monitor_position.x + monitor_size.width as i32 + && monitor_position.y <= position.y + && position.y < monitor_position.y + monitor_size.height as i32 + }) +} + +fn clamp_surface_size(attrs: &WindowAttributes, scale_factor: f64) -> PhysicalSize { + let mut size = attrs + .surface_size + .unwrap_or_else(|| PhysicalSize::new(800, 600).into()) + .to_physical::(scale_factor); + + if let Some(min_size) = attrs.min_surface_size { + let min_size = min_size.to_physical::(scale_factor); + size.width = size.width.max(min_size.width); + size.height = size.height.max(min_size.height); + } + + if let Some(max_size) = attrs.max_surface_size { + let max_size = max_size.to_physical::(scale_factor); + size.width = size.width.min(max_size.width); + size.height = size.height.min(max_size.height); + } + + size +} + +fn apply_prevent_overflow( + attrs: &mut WindowAttributes, + window_size: &mut PhysicalSize, + monitor: &MonitorHandle, + margin: Size, +) { + let work_area = monitor.work_area(); + let margin = margin.to_physical::(monitor.scale_factor()); + let constraint = PhysicalSize::new( + work_area.size.width.saturating_sub(margin.width), + work_area.size.height.saturating_sub(margin.height), + ); + + if window_size.width > constraint.width || window_size.height > constraint.height { + window_size.width = window_size.width.min(constraint.width); + window_size.height = window_size.height.min(constraint.height); + attrs.surface_size = Some((*window_size).into()); + } +} + +fn prepare_window_attributes(event_loop: &dyn ActiveEventLoop, attrs: &mut AppWindowAttrs) { + if !attrs.center && attrs.prevent_overflow.is_none() { + return; + } + + let monitor = attrs + .inner + .position + .and_then(|position| { + let monitors = event_loop.available_monitors(); + find_monitor_for_position(monitors, position) + }) + .or_else(|| event_loop.primary_monitor()); + + let Some(monitor) = monitor else { + return; + }; + + // `clamp_surface_size` is the requested client/surface size; the window + // winit creates will be larger by the non-client frame (title bar + borders). + // To center the *visible* window we have to account for that frame. + let mut window_size = clamp_surface_size(&attrs.inner, monitor.scale_factor()); + + // Left and right borders count toward the outer width (and so toward the + // centered x position) but not toward the surface size. The title bar adds to + // the visible height. Mirrors `tauri-runtime-wry`'s creation-time centering. + #[allow(unused_mut)] + let mut shadow_width: u32 = 0; + #[cfg(windows)] + if attrs.inner.decorations { + use windows::Win32::{ + Foundation::RECT, + UI::WindowsAndMessaging::{AdjustWindowRect, WS_OVERLAPPEDWINDOW}, + }; + + let mut rect = RECT::default(); + if unsafe { AdjustWindowRect(&mut rect, WS_OVERLAPPEDWINDOW, false) }.is_ok() { + shadow_width = (rect.right - rect.left) as u32; + // `rect.top` is negative (the title bar above the client area); + // `rect.bottom` is the bottom shadow, which we intentionally ignore. + window_size.height += (-rect.top) as u32; + } + } + + if let Some(margin) = attrs.prevent_overflow { + apply_prevent_overflow(&mut attrs.inner, &mut window_size, &monitor, margin); + } + + if attrs.center { + window_size.width += shadow_width; + let position = calculate_window_center_position(window_size, &monitor); + attrs.inner.position = Some(position.into()); + } +} + +pub(crate) fn paired_size_constraint( + width: Option, + height: Option, +) -> Option { + match (width, height) { + ( + Some(tauri_runtime::dpi::PixelUnit::Logical(width)), + Some(tauri_runtime::dpi::PixelUnit::Logical(height)), + ) => Some(Size::Logical(tauri_runtime::dpi::LogicalSize::new( + width.into(), + height.into(), + ))), + ( + Some(tauri_runtime::dpi::PixelUnit::Physical(width)), + Some(tauri_runtime::dpi::PixelUnit::Physical(height)), + ) => Some(Size::Physical(PhysicalSize::new( + width.into(), + height.into(), + ))), + _ => None, + } +} + +pub(crate) enum WindowMessage { + AddEventListener(WindowEventId, WindowEventListener), + Close, + Destroy, + ScaleFactor(Sender>), + InnerPosition(Sender>>), + OuterPosition(Sender>>), + InnerSize(Sender>>), + OuterSize(Sender>>), + IsFullscreen(Sender>), + IsMinimized(Sender>), + IsMaximized(Sender>), + IsFocused(Sender>), + IsDecorated(Sender>), + IsResizable(Sender>), + IsMaximizable(Sender>), + IsMinimizable(Sender>), + IsClosable(Sender>), + IsVisible(Sender>), + IsEnabled(Sender>), + IsAlwaysOnTop(Sender>), + Title(Sender>), + CurrentMonitor(Sender>>), + PrimaryMonitor(Sender>>), + MonitorFromPoint(Sender>>, f64, f64), + AvailableMonitors(Sender>>), + RawWindowHandle(Sender>), + Theme(Sender>), + Center, + RequestUserAttention(Option), + SetEnabled(bool), + SetResizable(bool), + SetMaximizable(bool), + SetMinimizable(bool), + SetClosable(bool), + SetTitle(String), + Maximize, + Unmaximize, + Minimize, + Unminimize, + Show, + Hide, + SetDecorations(bool), + SetShadow(bool), + SetAlwaysOnBottom(bool), + SetAlwaysOnTop(bool), + SetVisibleOnAllWorkspaces(bool), + SetContentProtected(bool), + SetSize(Size), + SetMinSize(Option), + SetMaxSize(Option), + SetSizeConstraints(WindowSizeConstraints), + SetPosition(Position), + SetFullscreen(bool), + #[cfg(target_os = "macos")] + SetSimpleFullscreen(bool), + SetFocus, + // TODO: Implement SetFocusable, winit currently doesn't expose an API for it + #[allow(unused)] + SetFocusable(bool), + SetIcon(Icon<'static>), + SetSkipTaskbar(bool), + SetCursorGrab(bool), + SetCursorVisible(bool), + SetCursorIcon(CursorIcon), + SetCursorPosition(Position), + SetIgnoreCursorEvents(bool), + SetProgressBar(ProgressBarState), + SetBadgeCount(Option, Option), + SetBadgeLabel(Option), + SetOverlayIcon(Option>), + SetTitleBarStyle(tauri_utils::TitleBarStyle), + SetTrafficLightPosition(Position), + SetTheme(Option), + SetBackgroundColor(Option), + StartDragging, + StartResizeDragging(tauri_runtime::ResizeDirection), +} + +pub(crate) struct AppWindow { + #[allow(unused)] + pub(crate) id: WindowId, + pub(crate) label: String, + pub(crate) window: Box, + pub(crate) attrs: AppWindowAttrs, + pub(crate) children: Vec, + pub(crate) listeners: WindowEventListeners, +} + +#[derive(Clone, Debug, Default)] +pub(crate) struct AppWindowAttrs { + pub(crate) inner: WindowAttributes, + pub(crate) center: bool, + pub(crate) background_color: Option, + pub(crate) prevent_overflow: Option, + #[cfg(target_os = "macos")] + pub(crate) traffic_light_position: Option, + #[cfg(any( + target_os = "macos", + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + pub(crate) visible_on_all_workspaces: bool, + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + pub(crate) skip_taskbar: bool, +} + +impl AppWindow { + pub(crate) fn center(&self) { + let monitor = self.window.current_monitor(); + let monitor = monitor.or_else(|| self.window.primary_monitor()); + let Some(monitor) = monitor else { + return; + }; + + #[allow(unused_mut)] + let mut window_size = self.window.outer_size(); + + // On Windows `outer_size` includes the invisible resize/shadow border, so + // centering by it pushes the visible window down. Substitute the visible + // frame height reported by DWM. Mirrors `tauri-runtime-wry`'s `center`. + #[cfg(windows)] + if self.window.is_decorated() + && let Some(visible_height) = self.dwm_visible_frame_height() + { + window_size.height = visible_height; + } + + let position = calculate_window_center_position(window_size, &monitor); + self.window.set_outer_position(Position::Physical(position)); + } + + pub(crate) fn preferred_theme(&self) -> Option { + self + .attrs + .inner + .preferred_theme + .map(winit_theme_to_tauri_theme) + } + + pub(crate) fn resolved_theme(&self, app_wide_theme: Option) -> Option { + self.preferred_theme().or(app_wide_theme) + } + + pub(crate) fn set_theme(&mut self, theme: Option) { + self.attrs.inner.preferred_theme = tauri_theme_to_winit_theme(theme); + self.window.set_theme(tauri_theme_to_winit_theme(theme)); + self.apply_cef_theme(theme); + } + + fn apply_cef_theme(&self, theme: Option) { + for child in &self.children { + let request_context = child.host.request_context(); + request_context::apply_theme_scheme(request_context.as_ref(), theme); + } + } + + pub(crate) fn raw_handle_as_cef_handle(&self) -> cef::sys::cef_window_handle_t { + let handle = self + .window + .window_handle() + .expect("failed to get window handle"); + match handle.as_raw() { + #[cfg(windows)] + RawWindowHandle::Win32(handle) => cef::sys::HWND(handle.hwnd.get() as *mut _), + #[cfg(target_os = "macos")] + RawWindowHandle::AppKit(handle) => handle.ns_view.as_ptr().cast(), + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + RawWindowHandle::Xlib(handle) => handle.window as cef::sys::cef_window_handle_t, + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + RawWindowHandle::Xcb(handle) => handle.window.get() as cef::sys::cef_window_handle_t, + other => panic!("expected platform window handle, got {other:?}"), + } + } +} + +impl WinitCefApp { + pub(crate) fn create_window( + &mut self, + event_loop: &dyn ActiveEventLoop, + window_id: WindowId, + webview_id: Option, + pending: Box>>, + _after_window_creation: Option, + ) -> Result<()> { + let mut attrs = pending.window_builder.attrs.clone(); + if attrs.inner.preferred_theme.is_none() { + attrs.inner.preferred_theme = + tauri_theme_to_winit_theme(*self.context.app_wide_theme.lock().unwrap()); + } + prepare_window_attributes(event_loop, &mut attrs); + + let window = event_loop + .create_window(attrs.inner.clone()) + .map_err(|_| Error::CreateWindow)?; + + let winit_id = window.id(); + let mut appwindow = AppWindow { + id: window_id, + label: pending.label.clone(), + window, + attrs, + children: Vec::new(), + listeners: Default::default(), + }; + + #[cfg(target_os = "macos")] + { + if let Some(position) = &appwindow.attrs.traffic_light_position { + appwindow.apply_traffic_light_position(position); + } + + appwindow.set_visible_on_all_workspaces(appwindow.attrs.visible_on_all_workspaces); + } + + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + { + appwindow.set_visible_on_all_workspaces(appwindow.attrs.visible_on_all_workspaces); + appwindow.set_skip_taskbar(appwindow.attrs.skip_taskbar); + } + + if appwindow.attrs.background_color.is_some() { + appwindow.set_background_color(appwindow.attrs.background_color); + } + + #[cfg(any(windows, target_os = "macos"))] + if let Some(after_window_creation) = _after_window_creation { + after_window_creation(RawWindow { + #[cfg(windows)] + hwnd: appwindow.hwnd().0 as isize, + _marker: &PhantomData, + }); + } + + // Build the initial webview against the not-yet-registered window so a + // creation failure surfaces to the caller without leaving the window in + // state to roll back. + if let (Some(webview_id), Some(webview)) = (webview_id, pending.webview) { + Self::build_and_attach_webview( + &self.context, + &self.scheme_registry, + &mut self.state.live_browsers, + &mut appwindow, + webview_id, + browser_client::DragDropEventTarget::Window, + webview, + )?; + } + + self + .state + .winid_id_to_window_id_map + .insert(winit_id, window_id); + self.state.windows.insert(window_id, appwindow); + + Ok(()) + } + + pub(crate) fn handle_window_message( + &mut self, + event_loop: &dyn ActiveEventLoop, + window_id: WindowId, + message: WindowMessage, + ) { + // Handle Close and Destroy messages first to avoid borrowing issues with the window. + match message { + WindowMessage::Close => { + self.request_window_close(window_id, event_loop); + return; + } + WindowMessage::Destroy => { + self.close_window(window_id, event_loop); + return; + } + _ => {} + } + + let Some(app_window) = self.state.windows.get_mut(&window_id) else { + return; + }; + let window = &app_window.window; + + match message { + WindowMessage::AddEventListener(id, listener) => { + app_window.listeners.lock().unwrap().insert(id, listener); + } + WindowMessage::Close | WindowMessage::Destroy => unreachable!("handled before borrowing"), + WindowMessage::ScaleFactor(tx) => _ = tx.send(Ok(window.scale_factor())), + WindowMessage::InnerSize(tx) => _ = tx.send(Ok(window.surface_size())), + WindowMessage::OuterSize(tx) => _ = tx.send(Ok(window.outer_size())), + WindowMessage::IsFullscreen(tx) => _ = tx.send(Ok(window.fullscreen().is_some())), + WindowMessage::IsMinimized(tx) => _ = tx.send(Ok(window.is_minimized().unwrap_or(false))), + WindowMessage::IsMaximized(tx) => _ = tx.send(Ok(window.is_maximized())), + WindowMessage::IsFocused(tx) => _ = tx.send(Ok(window.has_focus())), + WindowMessage::IsDecorated(tx) => _ = tx.send(Ok(window.is_decorated())), + WindowMessage::IsResizable(tx) => _ = tx.send(Ok(window.is_resizable())), + WindowMessage::IsMaximizable(tx) => { + let is_maximizable = app_window + .window + .enabled_buttons() + .contains(winit::window::WindowButtons::MAXIMIZE); + let _ = tx.send(Ok(is_maximizable)); + } + WindowMessage::IsMinimizable(tx) => { + let is_minimizable = app_window + .window + .enabled_buttons() + .contains(winit::window::WindowButtons::MINIMIZE); + let _ = tx.send(Ok(is_minimizable)); + } + WindowMessage::IsClosable(tx) => { + let is_closable = app_window + .window + .enabled_buttons() + .contains(winit::window::WindowButtons::CLOSE); + let _ = tx.send(Ok(is_closable)); + } + WindowMessage::IsVisible(tx) => _ = tx.send(Ok(window.is_visible().unwrap_or(true))), + WindowMessage::IsEnabled(tx) => _ = tx.send(Ok(app_window.is_enabled())), + WindowMessage::IsAlwaysOnTop(tx) => { + let is_on_top = app_window.attrs.inner.window_level == WindowLevel::AlwaysOnTop; + let _ = tx.send(Ok(is_on_top)); + } + WindowMessage::Title(tx) => _ = tx.send(Ok(window.title())), + WindowMessage::InnerPosition(tx) => _ = tx.send(Ok(app_window.window.surface_position())), + WindowMessage::OuterPosition(tx) => { + let pos = app_window + .window + .outer_position() + .map_err(|_| Error::FailedToGetMonitor); + let _ = tx.send(pos); + } + WindowMessage::CurrentMonitor(tx) => { + let current = window.current_monitor(); + let current = current.map(|m| winit_monitor_to_tauri_monitor(&m)); + let _ = tx.send(Ok(current)); + } + WindowMessage::PrimaryMonitor(tx) => { + let primary = window.primary_monitor(); + let primary = primary.map(|m| winit_monitor_to_tauri_monitor(&m)); + let _ = tx.send(Ok(primary)); + } + WindowMessage::MonitorFromPoint(tx, x, y) => { + let mut available_monitors = window.available_monitors(); + let monitor = available_monitors + .find(|m| { + let pos = m.position().unwrap_or_default(); + let vm = m.current_video_mode(); + let size = vm.map(|v| v.size()).unwrap_or_default(); + x >= pos.x as f64 + && x <= pos.x as f64 + size.width as f64 + && y >= pos.y as f64 + && y <= pos.y as f64 + size.height as f64 + }) + .map(|m| winit_monitor_to_tauri_monitor(&m)); + let _ = tx.send(Ok(monitor)); + } + WindowMessage::AvailableMonitors(tx) => { + let monitors = app_window + .window + .available_monitors() + .map(|m| winit_monitor_to_tauri_monitor(&m)) + .collect(); + let _ = tx.send(Ok(monitors)); + } + WindowMessage::RawWindowHandle(tx) => { + let handle = window.window_handle(); + let send_handle = handle + .map(|h| SendRawWindowHandle(h.as_raw())) + .map_err(|_| Error::FailedToSendMessage); + let _ = tx.send(send_handle); + } + WindowMessage::Theme(tx) => { + let theme = window.theme(); + let theme = theme.map(winit_theme_to_tauri_theme); + let theme = theme.unwrap_or(Theme::Light); + let _ = tx.send(Ok(theme)); + } + WindowMessage::Center => { + app_window.center(); + } + WindowMessage::RequestUserAttention(attention) => { + window.request_user_attention(match attention { + Some(UserAttentionType::Critical) => Some(winit::window::UserAttentionType::Critical), + Some(UserAttentionType::Informational) => { + Some(winit::window::UserAttentionType::Informational) + } + None => None, + }) + } + WindowMessage::SetEnabled(value) => app_window.set_enabled(value), + WindowMessage::SetResizable(value) => window.set_resizable(value), + WindowMessage::SetTitle(title) => window.set_title(&title), + WindowMessage::Maximize => window.set_maximized(true), + WindowMessage::Unmaximize => window.set_maximized(false), + WindowMessage::Minimize => window.set_minimized(true), + WindowMessage::Unminimize => window.set_minimized(false), + WindowMessage::Show => window.set_visible(true), + WindowMessage::Hide => window.set_visible(false), + WindowMessage::SetDecorations(value) => window.set_decorations(value), + WindowMessage::SetSize(size) => _ = window.request_surface_size(size), + WindowMessage::SetPosition(position) => window.set_outer_position(position), + WindowMessage::SetFullscreen(value) => { + app_window + .window + .set_fullscreen(value.then_some(Fullscreen::Borderless(None))); + } + #[cfg(target_os = "macos")] + WindowMessage::SetSimpleFullscreen(value) => { + window.set_simple_fullscreen(value); + } + WindowMessage::SetFocus => window.focus_window(), + WindowMessage::SetMinSize(min_size) => window.set_min_surface_size(min_size), + WindowMessage::SetMaxSize(max_size) => window.set_max_surface_size(max_size), + WindowMessage::SetMaximizable(value) => { + let mut buttons = window.enabled_buttons(); + buttons.set(winit::window::WindowButtons::MAXIMIZE, value); + window.set_enabled_buttons(buttons); + } + WindowMessage::SetMinimizable(value) => { + let mut buttons = window.enabled_buttons(); + buttons.set(winit::window::WindowButtons::MINIMIZE, value); + window.set_enabled_buttons(buttons); + } + WindowMessage::SetClosable(value) => { + let mut buttons = window.enabled_buttons(); + buttons.set(winit::window::WindowButtons::CLOSE, value); + window.set_enabled_buttons(buttons); + } + WindowMessage::SetAlwaysOnBottom(value) => { + let level = match value { + true => WindowLevel::AlwaysOnBottom, + false => WindowLevel::Normal, + }; + app_window.attrs.inner.window_level = level; + window.set_window_level(level); + } + WindowMessage::SetAlwaysOnTop(value) => { + let level = match value { + true => WindowLevel::AlwaysOnTop, + false => WindowLevel::Normal, + }; + app_window.attrs.inner.window_level = level; + window.set_window_level(level); + } + WindowMessage::SetVisibleOnAllWorkspaces(_value) => { + #[cfg(target_os = "macos")] + { + app_window.attrs.visible_on_all_workspaces = _value; + app_window.set_visible_on_all_workspaces(_value); + } + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + { + app_window.attrs.visible_on_all_workspaces = _value; + app_window.set_visible_on_all_workspaces(_value); + } + } + WindowMessage::SetContentProtected(value) => window.set_content_protected(value), + WindowMessage::SetIcon(icon) => { + if let Ok(icon) = super::window::tauri_icon_to_winit_icon(icon) { + window.set_window_icon(Some(icon)) + } + } + WindowMessage::SetSkipTaskbar(_value) => { + #[cfg(windows)] + window.set_skip_taskbar(_value); + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + app_window.set_skip_taskbar(_value); + } + WindowMessage::SetShadow(_value) => { + #[cfg(windows)] + window.set_undecorated_shadow(_value); + #[cfg(target_os = "macos")] + window.set_has_shadow(_value); + } + WindowMessage::SetCursorGrab(value) => { + let _ = window.set_cursor_grab(match value { + true => winit::window::CursorGrabMode::Confined, + false => winit::window::CursorGrabMode::None, + }); + } + WindowMessage::SetCursorVisible(value) => window.set_cursor_visible(value), + WindowMessage::SetCursorIcon(value) => { + let cursor_icon = tauri_cursor_to_winit_cursor(value); + window.set_cursor(cursor_icon.into()) + } + WindowMessage::SetCursorPosition(value) => _ = window.set_cursor_position(value), + WindowMessage::SetIgnoreCursorEvents(value) => _ = window.set_cursor_hittest(!value), + + WindowMessage::SetTrafficLightPosition(_position) => { + #[cfg(target_os = "macos")] + { + app_window.attrs.traffic_light_position = Some(_position.clone()); + app_window.apply_traffic_light_position(&_position); + } + } + WindowMessage::SetTitleBarStyle(_style) => { + #[cfg(target_os = "macos")] + app_window.set_title_bar_style(_style); + } + WindowMessage::SetBackgroundColor(color) => { + app_window.attrs.background_color = color; + app_window.set_background_color(color); + } + WindowMessage::SetTheme(theme) => app_window.set_theme(theme), + WindowMessage::SetBadgeCount(count, desktop_filename) => { + event_loop.set_badge_count(count, desktop_filename) + } + WindowMessage::SetBadgeLabel(label) => event_loop.set_badge_label(label), + WindowMessage::SetOverlayIcon(_icon) => { + #[cfg(windows)] + app_window.set_overlay_icon(_icon); + } + WindowMessage::StartDragging => _ = window.drag_window(), + WindowMessage::StartResizeDragging(direction) => { + let _ = window.drag_resize_window(tauri_resize_direction_to_winit(direction)); + } + WindowMessage::SetSizeConstraints(constraints) => { + // TODO: upstream individual width/height size constraints to winit. + let min_size = paired_size_constraint(constraints.min_width, constraints.min_height); + let max_size = paired_size_constraint(constraints.max_width, constraints.max_height); + window.set_min_surface_size(min_size); + window.set_max_surface_size(max_size); + } + + WindowMessage::SetFocusable(_) => { + // TODO + } + WindowMessage::SetProgressBar(state) => { + #[cfg(target_os = "macos")] + event_loop.set_progress_bar(state); + #[cfg(not(target_os = "macos"))] + app_window.set_progress_bar(state); + } + } + } +} + +#[derive(Debug, Clone)] +pub struct CefWindowDispatcher { + pub(crate) window_id: WindowId, + pub(crate) context: RuntimeContext, +} + +fn getter( + context: &RuntimeContext, + message: Message, + receiver: Receiver>, +) -> Result { + context.send_message(message)?; + receiver.recv().map_err(|_| Error::FailedToReceiveMessage)? +} + +macro_rules! window_getter { + ($self:ident, $variant:ident) => {{ + let (tx, rx) = mpsc::channel(); + getter( + &$self.context, + Message::Window { + window_id: $self.window_id, + message: WindowMessage::$variant(tx), + }, + rx, + ) + }}; +} + +impl WindowDispatch for CefWindowDispatcher { + type Runtime = CefRuntime; + type WindowBuilder = WindowBuilderWrapper; + + fn run_on_main_thread(&self, f: F) -> Result<()> { + self.context.run_on_main_thread(f) + } + + fn on_window_event(&self, f: F) -> WindowEventId { + let id = self.context.next_window_event_id(); + let _ = self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::AddEventListener(id, Box::new(f)), + }); + id + } + + fn scale_factor(&self) -> Result { + window_getter!(self, ScaleFactor) + } + + fn inner_position(&self) -> Result> { + window_getter!(self, InnerPosition) + } + + fn outer_position(&self) -> Result> { + window_getter!(self, OuterPosition) + } + + fn inner_size(&self) -> Result> { + window_getter!(self, InnerSize) + } + + fn outer_size(&self) -> Result> { + window_getter!(self, OuterSize) + } + + fn is_fullscreen(&self) -> Result { + window_getter!(self, IsFullscreen) + } + + fn is_minimized(&self) -> Result { + window_getter!(self, IsMinimized) + } + + fn is_maximized(&self) -> Result { + window_getter!(self, IsMaximized) + } + + fn is_focused(&self) -> Result { + window_getter!(self, IsFocused) + } + + fn is_decorated(&self) -> Result { + window_getter!(self, IsDecorated) + } + + fn is_resizable(&self) -> Result { + window_getter!(self, IsResizable) + } + + fn is_maximizable(&self) -> Result { + window_getter!(self, IsMaximizable) + } + + fn is_minimizable(&self) -> Result { + window_getter!(self, IsMinimizable) + } + + fn is_closable(&self) -> Result { + window_getter!(self, IsClosable) + } + + fn is_visible(&self) -> Result { + window_getter!(self, IsVisible) + } + + fn is_enabled(&self) -> Result { + window_getter!(self, IsEnabled) + } + + fn is_always_on_top(&self) -> Result { + window_getter!(self, IsAlwaysOnTop) + } + + fn title(&self) -> Result { + window_getter!(self, Title) + } + + fn current_monitor(&self) -> Result> { + window_getter!(self, CurrentMonitor) + } + + fn primary_monitor(&self) -> Result> { + window_getter!(self, PrimaryMonitor) + } + + fn monitor_from_point(&self, x: f64, y: f64) -> Result> { + let (tx, rx) = mpsc::channel(); + getter( + &self.context, + Message::Window { + window_id: self.window_id, + message: WindowMessage::MonitorFromPoint(tx, x, y), + }, + rx, + ) + } + + fn available_monitors(&self) -> Result> { + window_getter!(self, AvailableMonitors) + } + + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + fn gtk_window(&self) -> Result { + Err(Error::FailedToSendMessage) + } + + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + fn default_vbox(&self) -> Result { + Err(Error::FailedToSendMessage) + } + + fn window_handle( + &self, + ) -> std::result::Result, raw_window_handle::HandleError> { + let handle: Result = window_getter!(self, RawWindowHandle); + let handle = handle.map_err(|_| raw_window_handle::HandleError::Unavailable)?; + Ok(unsafe { raw_window_handle::WindowHandle::borrow_raw(handle.0) }) + } + + fn theme(&self) -> Result { + window_getter!(self, Theme) + } + + fn center(&self) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::Center, + }) + } + + fn request_user_attention(&self, request_type: Option) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::RequestUserAttention(request_type), + }) + } + + fn create_window) + Send + 'static>( + &mut self, + pending: PendingWindow, + after_window_creation: Option, + ) -> Result> { + create_window_detached(&self.context, pending, after_window_creation) + } + + fn create_webview( + &mut self, + pending: PendingWebview, + ) -> Result> { + create_webview_detached(&self.context, self.window_id, pending) + } + + fn set_resizable(&self, resizable: bool) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetResizable(resizable), + }) + } + + fn set_enabled(&self, enabled: bool) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetEnabled(enabled), + }) + } + + fn set_maximizable(&self, maximizable: bool) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetMaximizable(maximizable), + }) + } + + fn set_minimizable(&self, minimizable: bool) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetMinimizable(minimizable), + }) + } + + fn set_closable(&self, closable: bool) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetClosable(closable), + }) + } + + fn set_title>(&self, title: S) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetTitle(title.into()), + }) + } + + fn maximize(&self) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::Maximize, + }) + } + + fn unmaximize(&self) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::Unmaximize, + }) + } + + fn minimize(&self) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::Minimize, + }) + } + + fn unminimize(&self) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::Unminimize, + }) + } + + fn show(&self) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::Show, + }) + } + + fn hide(&self) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::Hide, + }) + } + + fn close(&self) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::Close, + }) + } + + fn destroy(&self) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::Destroy, + }) + } + + fn set_decorations(&self, decorations: bool) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetDecorations(decorations), + }) + } + + fn set_shadow(&self, enable: bool) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetShadow(enable), + }) + } + + fn set_always_on_bottom(&self, value: bool) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetAlwaysOnBottom(value), + }) + } + + fn set_always_on_top(&self, value: bool) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetAlwaysOnTop(value), + }) + } + + fn set_visible_on_all_workspaces(&self, value: bool) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetVisibleOnAllWorkspaces(value), + }) + } + + fn set_content_protected(&self, protected: bool) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetContentProtected(protected), + }) + } + + fn set_size(&self, size: Size) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetSize(size), + }) + } + + fn set_min_size(&self, size: Option) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetMinSize(size), + }) + } + + fn set_max_size(&self, size: Option) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetMaxSize(size), + }) + } + + fn set_size_constraints(&self, constraints: WindowSizeConstraints) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetSizeConstraints(constraints), + }) + } + + fn set_position(&self, position: Position) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetPosition(position), + }) + } + + fn set_fullscreen(&self, fullscreen: bool) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetFullscreen(fullscreen), + }) + } + + #[cfg(target_os = "macos")] + fn set_simple_fullscreen(&self, enable: bool) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetSimpleFullscreen(enable), + }) + } + + fn set_focus(&self) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetFocus, + }) + } + + fn set_focusable(&self, focusable: bool) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetFocusable(focusable), + }) + } + + fn set_icon(&self, icon: Icon) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetIcon(icon.into_owned()), + }) + } + + fn set_skip_taskbar(&self, skip: bool) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetSkipTaskbar(skip), + }) + } + + fn set_cursor_grab(&self, grab: bool) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetCursorGrab(grab), + }) + } + + fn set_cursor_visible(&self, visible: bool) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetCursorVisible(visible), + }) + } + + fn set_cursor_icon(&self, icon: CursorIcon) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetCursorIcon(icon), + }) + } + + fn set_cursor_position>(&self, position: Pos) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetCursorPosition(position.into()), + }) + } + + fn set_ignore_cursor_events(&self, ignore: bool) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetIgnoreCursorEvents(ignore), + }) + } + + fn start_dragging(&self) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::StartDragging, + }) + } + + fn start_resize_dragging(&self, direction: tauri_runtime::ResizeDirection) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::StartResizeDragging(direction), + }) + } + + fn set_badge_count(&self, count: Option, desktop_filename: Option) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetBadgeCount(count, desktop_filename), + }) + } + + fn set_badge_label(&self, label: Option) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetBadgeLabel(label), + }) + } + + fn set_overlay_icon(&self, icon: Option) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetOverlayIcon(icon.map(Icon::into_owned)), + }) + } + + fn set_progress_bar(&self, progress_state: ProgressBarState) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetProgressBar(progress_state), + }) + } + + fn set_title_bar_style(&self, style: tauri_utils::TitleBarStyle) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetTitleBarStyle(style), + }) + } + + fn set_traffic_light_position(&self, position: Position) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetTrafficLightPosition(position), + }) + } + + fn set_theme(&self, theme: Option) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetTheme(theme), + }) + } + + fn set_background_color(&self, color: Option) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetBackgroundColor(color), + }) + } +} + +pub(crate) fn create_window_detached( + context: &RuntimeContext, + pending: PendingWindow>, + after_window_creation: Option, +) -> Result>> +where + T: UserEvent, + F: Fn(RawWindow<'_>) + Send + 'static, +{ + let label = pending.label.clone(); + let window_id = context.next_window_id(); + let (webview_id, use_https_scheme, devtools) = pending + .webview + .as_ref() + .map(|w| { + ( + Some(context.next_webview_id()), + w.webview_attributes.use_https_scheme, + w.webview_attributes.devtools, + ) + }) + .unwrap_or((None, false, None)); + + let (result_tx, result_rx) = mpsc::channel(); + context.send_message(Message::CreateWindow { + window_id, + webview_id, + pending: Box::new(pending), + after_window_creation: after_window_creation.map(|f| Box::new(f) as _), + result_tx, + })?; + // Block until the event loop has created the window so a creation failure is + // surfaced to the caller instead of leaving a detached, dead window. + result_rx + .recv() + .map_err(|_| Error::FailedToReceiveMessage)??; + + let webview = webview_id.map(|webview_id| DetachedWindowWebview { + webview: DetachedWebview { + label: label.clone(), + dispatcher: CefWebviewDispatcher { + window_id: Arc::new(Mutex::new(window_id)), + webview_id, + context: context.clone(), + }, + }, + use_https_scheme, + devtools, + }); + + Ok(DetachedWindow { + id: window_id, + label, + dispatcher: CefWindowDispatcher { + window_id, + context: context.clone(), + }, + webview, + }) +} + +pub(crate) fn winit_monitor_to_tauri_monitor(monitor: &winit::monitor::MonitorHandle) -> Monitor { + Monitor { + name: monitor.name().map(|s| s.to_string()), + scale_factor: monitor.scale_factor(), + position: monitor.position().unwrap_or_default(), + size: monitor + .current_video_mode() + .map(|v| v.size()) + .unwrap_or_default(), + work_area: monitor.work_area(), + } +} + +pub(crate) fn tauri_icon_to_winit_icon(icon: Icon) -> Result { + winit::icon::RgbaIcon::new(icon.rgba.into_owned(), icon.width, icon.height) + .map(Into::into) + .map_err(|e| tauri_runtime::Error::InvalidIcon(e.into())) +} + +fn tauri_cursor_to_winit_cursor(cursor: CursorIcon) -> winit::cursor::CursorIcon { + match cursor { + CursorIcon::Default => winit::cursor::CursorIcon::Default, + CursorIcon::Crosshair => winit::cursor::CursorIcon::Crosshair, + CursorIcon::Hand => winit::cursor::CursorIcon::Grab, + CursorIcon::Arrow => winit::cursor::CursorIcon::Default, + CursorIcon::Move => winit::cursor::CursorIcon::Move, + CursorIcon::Text => winit::cursor::CursorIcon::Text, + CursorIcon::Wait => winit::cursor::CursorIcon::Wait, + CursorIcon::Help => winit::cursor::CursorIcon::Help, + CursorIcon::Progress => winit::cursor::CursorIcon::Progress, + CursorIcon::NotAllowed => winit::cursor::CursorIcon::NotAllowed, + CursorIcon::ContextMenu => winit::cursor::CursorIcon::ContextMenu, + CursorIcon::Cell => winit::cursor::CursorIcon::Cell, + CursorIcon::VerticalText => winit::cursor::CursorIcon::VerticalText, + CursorIcon::Alias => winit::cursor::CursorIcon::Alias, + CursorIcon::Copy => winit::cursor::CursorIcon::Copy, + CursorIcon::NoDrop => winit::cursor::CursorIcon::NoDrop, + CursorIcon::Grab => winit::cursor::CursorIcon::Grab, + CursorIcon::Grabbing => winit::cursor::CursorIcon::Grabbing, + CursorIcon::AllScroll => winit::cursor::CursorIcon::AllScroll, + CursorIcon::ZoomIn => winit::cursor::CursorIcon::ZoomIn, + CursorIcon::ZoomOut => winit::cursor::CursorIcon::ZoomOut, + CursorIcon::EResize => winit::cursor::CursorIcon::EResize, + CursorIcon::NResize => winit::cursor::CursorIcon::NResize, + CursorIcon::NeResize => winit::cursor::CursorIcon::NeResize, + CursorIcon::NwResize => winit::cursor::CursorIcon::NwResize, + CursorIcon::SResize => winit::cursor::CursorIcon::SResize, + CursorIcon::SeResize => winit::cursor::CursorIcon::SeResize, + CursorIcon::SwResize => winit::cursor::CursorIcon::SwResize, + CursorIcon::WResize => winit::cursor::CursorIcon::WResize, + CursorIcon::EwResize => winit::cursor::CursorIcon::EwResize, + CursorIcon::NsResize => winit::cursor::CursorIcon::NsResize, + CursorIcon::NeswResize => winit::cursor::CursorIcon::NeswResize, + CursorIcon::NwseResize => winit::cursor::CursorIcon::NwseResize, + CursorIcon::ColResize => winit::cursor::CursorIcon::ColResize, + CursorIcon::RowResize => winit::cursor::CursorIcon::RowResize, + _ => winit::cursor::CursorIcon::Default, + } +} diff --git a/crates/tauri-runtime-cef/src/window_builder.rs b/crates/tauri-runtime-cef/src/window_builder.rs new file mode 100644 index 000000000000..9164cf019519 --- /dev/null +++ b/crates/tauri-runtime-cef/src/window_builder.rs @@ -0,0 +1,526 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use tauri_runtime::{ + Icon, Result, + dpi::{PhysicalSize, Size}, + window::{WindowBuilder, WindowBuilderBase, WindowSizeConstraints}, +}; +use tauri_utils::{ + Theme, + config::{Color, PreventOverflowConfig, WindowConfig}, +}; +use winit::{ + dpi::{LogicalPosition, LogicalSize}, + monitor::Fullscreen, + window::{WindowAttributes, WindowButtons}, +}; + +use crate::window::{ + AppWindowAttrs, paired_size_constraint, tauri_theme_to_winit_theme, winit_theme_to_tauri_theme, +}; + +#[cfg(any(windows, target_os = "macos"))] +use winit::raw_window_handle::RawWindowHandle; + +#[cfg(target_os = "macos")] +use std::ptr::NonNull; +#[cfg(target_os = "macos")] +use tauri_runtime::dpi::Position; +#[cfg(target_os = "macos")] +use tauri_utils::TitleBarStyle; + +#[cfg(windows)] +use std::num::NonZeroIsize; +#[cfg(windows)] +use windows::Win32::Foundation::HWND; + +#[derive(Clone, Default, Debug)] +pub struct WindowBuilderWrapper { + pub(crate) attrs: AppWindowAttrs, +} + +unsafe impl Send for WindowBuilderWrapper {} + +impl WindowBuilderBase for WindowBuilderWrapper {} + +impl WindowBuilder for WindowBuilderWrapper { + fn new() -> Self { + #[allow(unused_mut)] + let mut builder = Self { + attrs: AppWindowAttrs { + inner: WindowAttributes::default() + .with_title("Tauri App") + .with_visible(true), + ..Default::default() + }, + } + .focused(true); + + #[cfg(windows)] + { + builder = builder.window_classname("Tauri Window"); + } + + builder + } + + fn with_config(config: &WindowConfig) -> Self { + let mut builder = Self::new() + .title(config.title.to_string()) + .inner_size(config.width, config.height) + .resizable(config.resizable) + .fullscreen(config.fullscreen) + .focused(config.focus) + .focusable(config.focusable) + .visible(config.visible) + .decorations(config.decorations) + .maximized(config.maximized) + .content_protected(config.content_protected) + .closable(config.closable) + .maximizable(config.maximizable) + .minimizable(config.minimizable) + .skip_taskbar(config.skip_taskbar) + .shadow(config.shadow) + .visible_on_all_workspaces(config.visible_on_all_workspaces) + .theme(config.theme); + if config.always_on_bottom { + builder = builder.always_on_bottom(true); + } else if config.always_on_top { + builder = builder.always_on_top(true); + } + #[cfg(any(not(target_os = "macos"), feature = "macos-private-api"))] + { + builder = builder.transparent(config.transparent); + } + if let (Some(min_width), Some(min_height)) = (config.min_width, config.min_height) { + builder = builder.min_inner_size(min_width, min_height); + } + if let (Some(max_width), Some(max_height)) = (config.max_width, config.max_height) { + builder = builder.max_inner_size(max_width, max_height); + } + if let Some(color) = config.background_color { + builder = builder.background_color(color); + } + if let (Some(x), Some(y)) = (config.x, config.y) { + builder = builder.position(x, y); + } + #[cfg(target_os = "macos")] + { + builder = builder + .hidden_title(config.hidden_title) + .title_bar_style(config.title_bar_style); + if let Some(position) = &config.traffic_light_position { + builder = builder.traffic_light_position(LogicalPosition::new(position.x, position.y)); + } + if let Some(identifier) = &config.tabbing_identifier { + builder = builder.tabbing_identifier(identifier); + } + let pl_attrs = (*platform_attrs(&mut builder.attrs.inner)) + .with_accepts_first_mouse(config.accept_first_mouse); + + builder.attrs.inner = builder + .attrs + .inner + .with_platform_attributes(Box::new(pl_attrs)); + } + if config.center { + builder = builder.center(); + } + if let Some(window_classname) = &config.window_classname { + builder = builder.window_classname(window_classname); + } + if let Some(prevent_overflow) = &config.prevent_overflow { + builder = match prevent_overflow { + PreventOverflowConfig::Enable(true) => builder.prevent_overflow(), + PreventOverflowConfig::Margin(margin) => { + let margin = PhysicalSize::new(margin.width, margin.height); + builder.prevent_overflow_with_margin(margin.into()) + } + _ => builder, + }; + } + builder + } + + fn center(mut self) -> Self { + self.attrs.center = true; + self + } + + fn position(mut self, x: f64, y: f64) -> Self { + self.attrs.inner = self.attrs.inner.with_position(LogicalPosition::new(x, y)); + self + } + + fn inner_size(mut self, width: f64, height: f64) -> Self { + self.attrs.inner = self + .attrs + .inner + .with_surface_size(LogicalSize::new(width, height)); + self + } + + fn min_inner_size(mut self, min_width: f64, min_height: f64) -> Self { + self.attrs.inner = self + .attrs + .inner + .with_min_surface_size(LogicalSize::new(min_width, min_height)); + self + } + + fn max_inner_size(mut self, max_width: f64, max_height: f64) -> Self { + self.attrs.inner = self + .attrs + .inner + .with_max_surface_size(LogicalSize::new(max_width, max_height)); + self + } + + fn inner_size_constraints(mut self, constraints: WindowSizeConstraints) -> Self { + // TODO: upstream individual width/height size constraints to winit. + self.attrs.inner.min_surface_size = + paired_size_constraint(constraints.min_width, constraints.min_height); + self.attrs.inner.max_surface_size = + paired_size_constraint(constraints.max_width, constraints.max_height); + self + } + + fn prevent_overflow(mut self) -> Self { + self.attrs.prevent_overflow = Some(PhysicalSize::new(0, 0).into()); + self + } + + fn prevent_overflow_with_margin(mut self, margin: Size) -> Self { + self.attrs.prevent_overflow = Some(margin); + self + } + + fn resizable(mut self, resizable: bool) -> Self { + self.attrs.inner = self.attrs.inner.with_resizable(resizable); + self + } + + fn maximizable(mut self, maximizable: bool) -> Self { + self + .attrs + .inner + .enabled_buttons + .set(WindowButtons::MAXIMIZE, maximizable); + self + } + + fn minimizable(mut self, minimizable: bool) -> Self { + self + .attrs + .inner + .enabled_buttons + .set(WindowButtons::MINIMIZE, minimizable); + self + } + + fn closable(mut self, closable: bool) -> Self { + self + .attrs + .inner + .enabled_buttons + .set(WindowButtons::CLOSE, closable); + self + } + + fn title>(mut self, title: S) -> Self { + self.attrs.inner = self.attrs.inner.with_title(title); + self + } + + fn fullscreen(mut self, fullscreen: bool) -> Self { + self.attrs.inner = self + .attrs + .inner + .with_fullscreen(fullscreen.then_some(Fullscreen::Borderless(None))); + self + } + + fn focused(mut self, focused: bool) -> Self { + self.attrs.inner = self.attrs.inner.with_active(focused); + self + } + + fn focusable(self, _focusable: bool) -> Self { + // TODO + self + } + + fn maximized(mut self, maximized: bool) -> Self { + self.attrs.inner = self.attrs.inner.with_maximized(maximized); + self + } + + fn visible(mut self, visible: bool) -> Self { + self.attrs.inner = self.attrs.inner.with_visible(visible); + self + } + + #[cfg(any(not(target_os = "macos"), feature = "macos-private-api"))] + fn transparent(mut self, transparent: bool) -> Self { + self.attrs.inner = self.attrs.inner.with_transparent(transparent); + self + } + + fn decorations(mut self, decorations: bool) -> Self { + self.attrs.inner = self.attrs.inner.with_decorations(decorations); + self + } + + fn always_on_bottom(mut self, always_on_bottom: bool) -> Self { + self.attrs.inner = self.attrs.inner.with_window_level(if always_on_bottom { + winit::window::WindowLevel::AlwaysOnBottom + } else { + winit::window::WindowLevel::Normal + }); + self + } + + fn always_on_top(mut self, always_on_top: bool) -> Self { + self.attrs.inner = self.attrs.inner.with_window_level(if always_on_top { + winit::window::WindowLevel::AlwaysOnTop + } else { + winit::window::WindowLevel::Normal + }); + self + } + + #[cfg_attr(windows, allow(unused))] + fn visible_on_all_workspaces(mut self, visible_on_all_workspaces: bool) -> Self { + #[cfg(any( + target_os = "macos", + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + { + self.attrs.visible_on_all_workspaces = visible_on_all_workspaces; + } + + self + } + + fn content_protected(mut self, protected: bool) -> Self { + self.attrs.inner = self.attrs.inner.with_content_protected(protected); + self + } + + fn icon(mut self, icon: Icon) -> Result { + let icon = super::window::tauri_icon_to_winit_icon(icon)?; + self.attrs.inner = self.attrs.inner.with_window_icon(Some(icon)); + Ok(self) + } + + #[allow(unused_mut)] + fn skip_taskbar(mut self, skip: bool) -> Self { + #[cfg(windows)] + { + let pl_attrs = platform_attrs(&mut self.attrs.inner).with_skip_taskbar(skip); + self.attrs.inner = self + .attrs + .inner + .with_platform_attributes(Box::new(pl_attrs)); + } + + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + { + self.attrs.skip_taskbar = skip; + } + + self + } + + fn background_color(mut self, color: Color) -> Self { + self.attrs.background_color = Some(color); + self + } + + #[cfg_attr( + not(any(windows, target_os = "macos")), + allow(unused_mut, unused_variables) + )] + fn shadow(mut self, enable: bool) -> Self { + #[cfg(windows)] + { + let pl_attrs = platform_attrs(&mut self.attrs.inner).with_undecorated_shadow(enable); + self.attrs.inner = self + .attrs + .inner + .with_platform_attributes(Box::new(pl_attrs)); + } + + #[cfg(target_os = "macos")] + { + let pl_attrs = platform_attrs(&mut self.attrs.inner).with_has_shadow(enable); + self.attrs.inner = self + .attrs + .inner + .with_platform_attributes(Box::new(pl_attrs)); + } + + self + } + + #[cfg(windows)] + fn owner(mut self, owner: HWND) -> Self { + let pl_attrs = platform_attrs(&mut self.attrs.inner).with_owner_window(owner.0); + self.attrs.inner = self + .attrs + .inner + .with_platform_attributes(Box::new(pl_attrs)); + self + } + + #[cfg(windows)] + fn parent(mut self, parent: HWND) -> Self { + if let Some(hwnd) = NonZeroIsize::new(parent.0 as isize) { + let handle = RawWindowHandle::Win32(winit::raw_window_handle::Win32WindowHandle::new(hwnd)); + // SAFETY: Tauri passes a live parent HWND owned by the application. + self.attrs.inner = unsafe { self.attrs.inner.with_parent_window(Some(handle)) }; + } + self + } + + #[cfg(windows)] + fn drag_and_drop(mut self, enabled: bool) -> Self { + let pl_attrs = platform_attrs(&mut self.attrs.inner).with_drag_and_drop(enabled); + self.attrs.inner = self + .attrs + .inner + .with_platform_attributes(Box::new(pl_attrs)); + self + } + + #[cfg(target_os = "macos")] + fn parent(mut self, parent: *mut std::ffi::c_void) -> Self { + if let Some(ns_view) = NonNull::new(parent) { + let handle = + RawWindowHandle::AppKit(winit::raw_window_handle::AppKitWindowHandle::new(ns_view)); + // SAFETY: Tauri passes a live parent NSView owned by the application. + self.attrs.inner = unsafe { self.attrs.inner.with_parent_window(Some(handle)) }; + } + self + } + + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + fn transient_for(self, _parent: &impl gtk::glib::IsA) -> Self { + self + } + + #[cfg(target_os = "macos")] + fn title_bar_style(mut self, style: TitleBarStyle) -> Self { + let pl_attrs = *platform_attrs(&mut self.attrs.inner); + let pl_attrs = match style { + TitleBarStyle::Visible => pl_attrs + .with_titlebar_transparent(false) + .with_fullsize_content_view(false), + TitleBarStyle::Transparent => pl_attrs + .with_titlebar_transparent(true) + .with_fullsize_content_view(false), + TitleBarStyle::Overlay => pl_attrs + .with_titlebar_transparent(true) + .with_fullsize_content_view(true), + _ => pl_attrs, + }; + self.attrs.inner = self + .attrs + .inner + .with_platform_attributes(Box::new(pl_attrs)); + self + } + + #[cfg(target_os = "macos")] + fn traffic_light_position>(mut self, position: P) -> Self { + self.attrs.traffic_light_position = Some(position.into()); + self + } + + #[cfg(target_os = "macos")] + fn hidden_title(mut self, hidden: bool) -> Self { + let pl_attrs = platform_attrs(&mut self.attrs.inner).with_title_hidden(hidden); + self.attrs.inner = self + .attrs + .inner + .with_platform_attributes(Box::new(pl_attrs)); + self + } + + #[cfg(target_os = "macos")] + fn tabbing_identifier(mut self, identifier: &str) -> Self { + let pl_attrs = platform_attrs(&mut self.attrs.inner).with_tabbing_identifier(identifier); + self.attrs.inner = self + .attrs + .inner + .with_platform_attributes(Box::new(pl_attrs)); + self + } + + fn theme(mut self, theme: Option) -> Self { + self.attrs.inner = self + .attrs + .inner + .with_theme(tauri_theme_to_winit_theme(theme)); + self + } + + #[allow(unused_mut)] + fn window_classname>(mut self, _window_classname: S) -> Self { + #[cfg(windows)] + { + let pl_attrs = + platform_attrs(&mut self.attrs.inner).with_class_name(_window_classname.into()); + self.attrs.inner = self + .attrs + .inner + .with_platform_attributes(Box::new(pl_attrs)); + } + + self + } + + fn has_icon(&self) -> bool { + self.attrs.inner.window_icon.is_some() + } + + fn get_theme(&self) -> Option { + self + .attrs + .inner + .preferred_theme + .map(winit_theme_to_tauri_theme) + } +} + +#[cfg(windows)] +type PlatformAttributes = winit::platform::windows::WindowAttributesWindows; +#[cfg(target_os = "macos")] +type PlatformAttributes = winit::platform::macos::WindowAttributesMacOS; + +#[cfg(any(windows, target_os = "macos"))] +fn platform_attrs(attrs: &mut WindowAttributes) -> Box { + attrs + .platform + .take() + .and_then(|attrs| attrs.cast::().ok()) + .unwrap_or_default() +} diff --git a/crates/tauri-runtime-wry/CHANGELOG.md b/crates/tauri-runtime-wry/CHANGELOG.md index 3d01fa27e84b..8dacff6c100e 100644 --- a/crates/tauri-runtime-wry/CHANGELOG.md +++ b/crates/tauri-runtime-wry/CHANGELOG.md @@ -1,5 +1,44 @@ # Changelog +## \[2.11.2] + +### Dependencies + +- Upgraded to `tauri-utils@2.9.2` +- Upgraded to `tauri-runtime@2.11.2` + +## \[2.11.1] + +### Dependencies + +- Upgraded to `tauri-utils@2.9.1` +- Upgraded to `tauri-runtime@2.11.1` + +## \[2.11.0] + +### New Features + +- [`001c8fe3d`](https://www.github.com/tauri-apps/tauri/commit/001c8fe3d288802de9a8c29cfd2f46f9220d97c5) ([#14722](https://www.github.com/tauri-apps/tauri/pull/14722)) Add a WebView option to control browser-level general autofill behavior. This option does not disable password or credit card autofill. On Windows (WebView2), setting it to true disables the general autofill "Suggestions" UI, which may appear even when `autocomplete="off"` is specified on input elements. On Linux, macOS, iOS, and Android, this option is currently unsupported and performs no operation. +- [`b27be063f`](https://www.github.com/tauri-apps/tauri/commit/b27be063ff3052cb1071ac3ec719cfa104460fa4) ([#14925](https://www.github.com/tauri-apps/tauri/pull/14925)) Add `eval_with_callback` to the Tauri webview APIs and runtime dispatch layers. +- [`cc5c97602`](https://www.github.com/tauri-apps/tauri/commit/cc5c976027b0ab2431c13ec5b2e201d4414a8a6e) ([#14486](https://www.github.com/tauri-apps/tauri/pull/14486)) Trigger `RunEvent::Opened` on Android. +- [`eb0312ea9`](https://www.github.com/tauri-apps/tauri/commit/eb0312ea9e493954298ac0b3fdaae7eafb52750e) ([#15199](https://www.github.com/tauri-apps/tauri/pull/15199)) Propagates the `Event::Suspended` and `Event::Resumed` events from `tao` when they are emitted on mobile targets. +- [`093e2b47c`](https://www.github.com/tauri-apps/tauri/commit/093e2b47c01361c18783e9ff18750388e41650c5) ([#14484](https://www.github.com/tauri-apps/tauri/pull/14484)) Support creating multiple windows on Android (activity embedding) and iOS (scenes). +- [`1063c48c5`](https://www.github.com/tauri-apps/tauri/commit/1063c48c5e7d099ad74d28a937edf42e3f5c9f03) ([#14523](https://www.github.com/tauri-apps/tauri/pull/14523)) Add handler for web content process termination on macOS and iOS. + +### Bug Fixes + +- [`110336c88`](https://www.github.com/tauri-apps/tauri/commit/110336c88a8c0a04476619db0a5c8f7694d969a5) ([#15250](https://www.github.com/tauri-apps/tauri/pull/15250)) Fix initial window position when positioning it to another monitor. +- [`9808236eb`](https://www.github.com/tauri-apps/tauri/commit/9808236ebf7755d498d674b614f3fc75eeac1ec4) ([#14655](https://www.github.com/tauri-apps/tauri/pull/14655)) Fix monitor work area Y position on macOS. + +### What's Changed + +- [`d34497ef1`](https://www.github.com/tauri-apps/tauri/commit/d34497ef154eddcc36327a30dda06dc4748f6b20) ([#14862](https://www.github.com/tauri-apps/tauri/pull/14862)) The new window handler passed to `on_new_window` no longer requires `Sync`, and runs on main thread on Windows, aligning with other platforms + +### Dependencies + +- Upgraded to `tauri-runtime@2.11.0` +- Upgraded to `tauri-utils@2.9.0` + ## \[2.10.1] ### Dependencies diff --git a/crates/tauri-runtime-wry/Cargo.toml b/crates/tauri-runtime-wry/Cargo.toml index 7a46b4adf9e2..8cd209e4ce19 100644 --- a/crates/tauri-runtime-wry/Cargo.toml +++ b/crates/tauri-runtime-wry/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-runtime-wry" -version = "2.10.1" +version = "2.11.2" description = "Wry bindings to the Tauri runtime" exclude = ["CHANGELOG.md", "/target"] readme = "README.md" @@ -13,15 +13,14 @@ edition.workspace = true rust-version.workspace = true [dependencies] -wry = { version = "0.54.0", default-features = false, features = [ - "drag-drop", +wry = { version = "0.55.0", default-features = false, features = [ "protocol", "os-webview", "linux-body", ] } -tao = { version = "0.34.5", default-features = false, features = ["rwh_06"] } -tauri-runtime = { version = "2.10.1", path = "../tauri-runtime" } -tauri-utils = { version = "2.8.3", path = "../tauri-utils" } +tao = { version = "0.35.0", default-features = false, features = ["rwh_06"] } +tauri-runtime = { version = "2.11.2", path = "../tauri-runtime" } +tauri-utils = { version = "2.9.2", path = "../tauri-utils" } raw-window-handle = "0.6" http = "1" url = "2" @@ -61,7 +60,7 @@ objc2-web-kit = { version = "0.3", features = ["objc2-app-kit", "WKWebView"] } jni = "0.21" [features] -default = ["x11"] +default = ["x11", "dbus"] devtools = ["wry/devtools", "tauri-runtime/devtools"] x11 = ["tao/x11", "wry/x11"] macos-private-api = [ @@ -75,3 +74,4 @@ tracing = ["dep:tracing", "wry/tracing"] macos-proxy = ["wry/mac-proxy"] unstable = [] common-controls-v6 = [] +dbus = ["tao/dbus"] diff --git a/crates/tauri-runtime-wry/src/dialog/mod.rs b/crates/tauri-runtime-wry/src/dialog/mod.rs index 156d6a6d2a4c..3befa7b8a0bf 100644 --- a/crates/tauri-runtime-wry/src/dialog/mod.rs +++ b/crates/tauri-runtime-wry/src/dialog/mod.rs @@ -5,7 +5,9 @@ #[cfg(windows)] mod windows; -pub fn error>(err: S) { +// Takes a `&'static str` here since we convert clickable hyperlinks, +// DO NOT pass in untrusted input +pub fn error(err: &'static str) { #[cfg(windows)] windows::error(err); diff --git a/crates/tauri-runtime-wry/src/dialog/windows.rs b/crates/tauri-runtime-wry/src/dialog/windows.rs index faa8fab29a99..26850eb6e502 100644 --- a/crates/tauri-runtime-wry/src/dialog/windows.rs +++ b/crates/tauri-runtime-wry/src/dialog/windows.rs @@ -12,8 +12,8 @@ enum Level { Info, } -pub fn error>(err: S) { - dialog_inner(err.as_ref(), Level::Error); +pub fn error(err: &'static str) { + dialog_inner(err, Level::Error); } fn dialog_inner(err: &str, level: Level) { diff --git a/crates/tauri-runtime-wry/src/lib.rs b/crates/tauri-runtime-wry/src/lib.rs index 490741625366..e437fb45b7ec 100644 --- a/crates/tauri-runtime-wry/src/lib.rs +++ b/crates/tauri-runtime-wry/src/lib.rs @@ -35,6 +35,8 @@ use tauri_runtime::{ #[cfg(target_vendor = "apple")] use objc2::rc::Retained; +#[cfg(target_os = "android")] +use tao::platform::android::{WindowBuilderExtAndroid, WindowExtAndroid}; #[cfg(target_os = "macos")] use tao::platform::macos::{EventLoopWindowTargetExtMacOS, WindowBuilderExtMacOS}; #[cfg(any( @@ -78,12 +80,9 @@ use tao::{ UserAttentionType as TaoUserAttentionType, }, }; -#[cfg(target_os = "macos")] -use tauri_utils::TitleBarStyle; -#[cfg(desktop)] use tauri_utils::config::PreventOverflowConfig; use tauri_utils::{ - Theme, + Theme, TitleBarStyle, config::{Color, WindowConfig}, }; use url::Url; @@ -115,7 +114,7 @@ use wry::{ use wry::{WebViewBuilderExtUnix, WebViewExtUnix}; #[cfg(target_os = "ios")] -pub use tao::platform::ios::WindowExtIOS; +pub use tao::platform::ios::{WindowBuilderExtIOS, WindowExtIOS}; #[cfg(target_os = "macos")] pub use tao::platform::macos::{ ActivationPolicy as TaoActivationPolicy, EventLoopExtMacOS, WindowExtMacOS, @@ -756,6 +755,25 @@ impl From for PositionWrapper { } } +#[cfg(desktop)] +fn find_monitor_for_position( + monitors: impl Iterator, + window_position: TaoPosition, +) -> Option { + monitors.into_iter().find(|m| { + let monitor_pos = m.position(); + let monitor_size = m.size(); + + // type annotations required for 32bit targets. + let window_position = window_position.to_physical::(m.scale_factor()); + + monitor_pos.x <= window_position.x + && window_position.x < monitor_pos.x + monitor_size.width as i32 + && monitor_pos.y <= window_position.y + && window_position.y < monitor_pos.y + monitor_size.height as i32 + }) +} + #[derive(Debug, Clone)] pub struct UserAttentionTypeWrapper(pub TaoUserAttentionType); @@ -882,7 +900,7 @@ impl WindowBuilder for WindowBuilderWrapper { #[cfg(target_os = "macos")] { // TODO: find a proper way to prevent webview being pushed out of the window. - // Workround for issue: https://github.com/tauri-apps/tauri/issues/10225 + // Workaround for issue: https://github.com/tauri-apps/tauri/issues/10225 // The window requires `NSFullSizeContentViewWindowMask` flag to prevent devtools // pushing the content view out of the window. // By setting the default style to `TitleBarStyle::Visible` should fix the issue for most of the users. @@ -945,68 +963,91 @@ impl WindowBuilder for WindowBuilderWrapper { window.inner = window.inner.with_cursor_moved_event(false); } - #[cfg(desktop)] + #[cfg(target_os = "android")] { - window = window - .title(config.title.to_string()) - .inner_size(config.width, config.height) - .focused(config.focus) - .focusable(config.focusable) - .visible(config.visible) - .resizable(config.resizable) - .fullscreen(config.fullscreen) - .decorations(config.decorations) - .maximized(config.maximized) - .always_on_bottom(config.always_on_bottom) - .always_on_top(config.always_on_top) - .visible_on_all_workspaces(config.visible_on_all_workspaces) - .content_protected(config.content_protected) - .skip_taskbar(config.skip_taskbar) - .theme(config.theme) - .closable(config.closable) - .maximizable(config.maximizable) - .minimizable(config.minimizable) - .shadow(config.shadow); - - let mut constraints = WindowSizeConstraints::default(); - - if let Some(min_width) = config.min_width { - constraints.min_width = Some(tao::dpi::LogicalUnit::new(min_width).into()); - } - if let Some(min_height) = config.min_height { - constraints.min_height = Some(tao::dpi::LogicalUnit::new(min_height).into()); - } - if let Some(max_width) = config.max_width { - constraints.max_width = Some(tao::dpi::LogicalUnit::new(max_width).into()); + if let Some(activity_name) = &config.activity_name { + window.inner = window.inner.with_activity_name(activity_name.clone()); } - if let Some(max_height) = config.max_height { - constraints.max_height = Some(tao::dpi::LogicalUnit::new(max_height).into()); + if let Some(activity_name) = &config.created_by_activity_name { + window.inner = window + .inner + .with_created_by_activity_name(activity_name.clone()); } - if let Some(color) = config.background_color { - window = window.background_color(color); - } - window = window.inner_size_constraints(constraints); + } - if let (Some(x), Some(y)) = (config.x, config.y) { - window = window.position(x, y); + #[cfg(target_os = "ios")] + { + if let Some(scene_identifier) = &config.requested_by_scene_identifier { + window.inner = window + .inner + .with_requesting_scene_identifier(scene_identifier.clone()); } + } - if config.center { - window = window.center(); - } + // ignore size from config for mobile for backward compatibility + #[cfg(not(any(target_os = "ios", target_os = "android")))] + { + window = window.inner_size(config.width, config.height); + } - if let Some(window_classname) = &config.window_classname { - window = window.window_classname(window_classname); - } + window = window + .title(config.title.to_string()) + .focused(config.focus) + .focusable(config.focusable) + .visible(config.visible) + .resizable(config.resizable) + .fullscreen(config.fullscreen) + .decorations(config.decorations) + .maximized(config.maximized) + .always_on_bottom(config.always_on_bottom) + .always_on_top(config.always_on_top) + .visible_on_all_workspaces(config.visible_on_all_workspaces) + .content_protected(config.content_protected) + .skip_taskbar(config.skip_taskbar) + .theme(config.theme) + .closable(config.closable) + .maximizable(config.maximizable) + .minimizable(config.minimizable) + .shadow(config.shadow); + + let mut constraints = WindowSizeConstraints::default(); + + if let Some(min_width) = config.min_width { + constraints.min_width = Some(tao::dpi::LogicalUnit::new(min_width).into()); + } + if let Some(min_height) = config.min_height { + constraints.min_height = Some(tao::dpi::LogicalUnit::new(min_height).into()); + } + if let Some(max_width) = config.max_width { + constraints.max_width = Some(tao::dpi::LogicalUnit::new(max_width).into()); + } + if let Some(max_height) = config.max_height { + constraints.max_height = Some(tao::dpi::LogicalUnit::new(max_height).into()); + } + if let Some(color) = config.background_color { + window = window.background_color(color); + } + window = window.inner_size_constraints(constraints); - if let Some(prevent_overflow) = &config.prevent_overflow { - window = match prevent_overflow { - PreventOverflowConfig::Enable(true) => window.prevent_overflow(), - PreventOverflowConfig::Margin(margin) => window - .prevent_overflow_with_margin(TaoPhysicalSize::new(margin.width, margin.height).into()), - _ => window, - }; - } + if let (Some(x), Some(y)) = (config.x, config.y) { + window = window.position(x, y); + } + + if config.center { + window = window.center(); + } + + if let Some(window_classname) = &config.window_classname { + window = window.window_classname(window_classname); + } + + if let Some(prevent_overflow) = &config.prevent_overflow { + window = match prevent_overflow { + PreventOverflowConfig::Enable(true) => window.prevent_overflow(), + PreventOverflowConfig::Margin(margin) => window + .prevent_overflow_with_margin(TaoPhysicalSize::new(margin.width, margin.height).into()), + _ => window, + }; } window @@ -1322,6 +1363,26 @@ impl WindowBuilder for WindowBuilderWrapper { fn window_classname>(self, _window_classname: S) -> Self { self } + + #[cfg(target_os = "android")] + fn activity_name>(mut self, class_name: S) -> Self { + self.inner = self.inner.with_activity_name(class_name.into()); + self + } + + #[cfg(target_os = "android")] + fn created_by_activity_name>(mut self, class_name: S) -> Self { + self.inner = self.inner.with_created_by_activity_name(class_name.into()); + self + } + + #[cfg(target_os = "ios")] + fn requested_by_scene_identifier>(mut self, identifier: S) -> Self { + self.inner = self + .inner + .with_requesting_scene_identifier(identifier.into()); + self + } } #[cfg(any( @@ -1413,6 +1474,10 @@ pub enum WindowMessage { target_os = "openbsd" ))] GtkBox(Sender), + #[cfg(target_os = "android")] + ActivityName(Sender), + #[cfg(target_os = "ios")] + SceneIdentifier(Sender), RawWindowHandle(Sender>), Theme(Sender), IsEnabled(Sender), @@ -1461,7 +1526,7 @@ pub enum WindowMessage { SetBadgeLabel(Option), SetOverlayIcon(Option), SetProgressBar(ProgressBarState), - SetTitleBarStyle(tauri_utils::TitleBarStyle), + SetTitleBarStyle(TitleBarStyle), SetTrafficLightPosition(Position), SetTheme(Option), SetBackgroundColor(Option), @@ -1492,6 +1557,15 @@ pub enum WebviewMessage { EvaluateScript(String), #[cfg(all(feature = "tracing", not(target_os = "android")))] EvaluateScript(String, Sender<()>, tracing::Span), + #[cfg(not(all(feature = "tracing", not(target_os = "android"))))] + EvaluateScriptWithCallback(String, Box), + #[cfg(all(feature = "tracing", not(target_os = "android")))] + EvaluateScriptWithCallback( + String, + Box, + Sender<()>, + tracing::Span, + ), CookiesForUrl(Url, Sender>>>), Cookies(Sender>>>), SetCookie(tauri_runtime::Cookie<'static>), @@ -1599,13 +1673,13 @@ impl WebviewDispatch for WryWebviewDispatcher { id } - fn with_webview) + Send + 'static>(&self, f: F) -> Result<()> { + fn with_webview(&self, f: F) -> Result<()> { send_user_message( &self.context, Message::Webview( *self.window_id.lock().unwrap(), self.webview_id, - WebviewMessage::WithWebview(Box::new(move |webview| f(Box::new(webview)))), + WebviewMessage::WithWebview(Box::new(f)), ), ) } @@ -1867,6 +1941,46 @@ impl WebviewDispatch for WryWebviewDispatcher { ) } + #[cfg(all(feature = "tracing", not(target_os = "android")))] + fn eval_script_with_callback>( + &self, + script: S, + callback: impl Fn(String) + Send + 'static, + ) -> Result<()> { + // use a channel so the EvaluateScript task uses the current span as parent + let (tx, rx) = channel(); + getter!( + self, + rx, + Message::Webview( + *self.window_id.lock().unwrap(), + self.webview_id, + WebviewMessage::EvaluateScriptWithCallback( + script.into(), + Box::new(callback), + tx, + tracing::Span::current(), + ), + ) + ) + } + + #[cfg(not(all(feature = "tracing", not(target_os = "android"))))] + fn eval_script_with_callback>( + &self, + script: S, + callback: impl Fn(String) + Send + 'static, + ) -> Result<()> { + send_user_message( + &self.context, + Message::Webview( + *self.window_id.lock().unwrap(), + self.webview_id, + WebviewMessage::EvaluateScriptWithCallback(script.into(), Box::new(callback)), + ), + ) + } + fn set_zoom(&self, scale_factor: f64) -> Result<()> { send_user_message( &self.context, @@ -2094,6 +2208,18 @@ impl WindowDispatch for WryWindowDispatcher { window_getter!(self, WindowMessage::GtkBox).map(|w| w.0) } + /// Returns the name of the Android activity associated with this window. + #[cfg(target_os = "android")] + fn activity_name(&self) -> Result { + window_getter!(self, WindowMessage::ActivityName) + } + + /// Returns the identifier of the UIScene tied to this UIWindow. + #[cfg(target_os = "ios")] + fn scene_identifier(&self) -> Result { + window_getter!(self, WindowMessage::SceneIdentifier) + } + fn window_handle( &self, ) -> std::result::Result, raw_window_handle::HandleError> { @@ -2465,7 +2591,7 @@ impl WindowDispatch for WryWindowDispatcher { ) } - fn set_title_bar_style(&self, style: tauri_utils::TitleBarStyle) -> Result<()> { + fn set_title_bar_style(&self, style: TitleBarStyle) -> Result<()> { send_user_message( &self.context, Message::Window(self.window_id, WindowMessage::SetTitleBarStyle(style)), @@ -2918,6 +3044,7 @@ impl Runtime for Wry { type PlatformSpecificWebviewAttribute = WebviewAttribute; type PlatformSpecificInitAttribute = (); type WindowOpener = NewWindowOpener; + type Webview = Webview; fn new(args: RuntimeInitArgs<()>) -> Result { Self::init_with_builder(EventLoopBuilder::>::with_user_event(), args) @@ -3422,6 +3549,14 @@ fn handle_user_message( WindowMessage::GtkBox(tx) => tx .send(GtkBox(window.default_vbox().unwrap().clone())) .unwrap(), + #[cfg(target_os = "android")] + WindowMessage::ActivityName(tx) => { + tx.send(window.activity_name()).unwrap(); + } + #[cfg(target_os = "ios")] + WindowMessage::SceneIdentifier(tx) => { + tx.send(window.scene_identifier()).unwrap(); + } WindowMessage::RawWindowHandle(tx) => tx .send( window @@ -3749,6 +3884,20 @@ fn handle_user_message( log::error!("{e}"); } } + #[cfg(all(feature = "tracing", not(target_os = "android")))] + WebviewMessage::EvaluateScriptWithCallback(script, callback, tx, span) => { + let _span = span.entered(); + if let Err(e) = webview.evaluate_script_with_callback(&script, callback) { + log::error!("{e}"); + } + tx.send(()).unwrap(); + } + #[cfg(not(all(feature = "tracing", not(target_os = "android"))))] + WebviewMessage::EvaluateScriptWithCallback(script, callback) => { + if let Err(e) = webview.evaluate_script_with_callback(&script, callback) { + log::error!("{e}"); + } + } WebviewMessage::Navigate(url) => { if let Err(e) = webview.load_url(url.as_str()) { log::error!("failed to navigate to url {}: {}", url, e); @@ -3959,44 +4108,39 @@ fn handle_user_message( target_os = "openbsd" ))] { - f(webview.webview()); + f(Webview::new(webview.webview())); } #[cfg(target_os = "macos")] { use wry::WebViewExtMacOS; - f(Webview { - webview: Retained::into_raw(webview.webview()) as *mut objc2::runtime::AnyObject + f(Webview::new( + Retained::into_raw(webview.webview()) as *mut objc2::runtime::AnyObject as *mut std::ffi::c_void, - manager: Retained::into_raw(webview.manager()) as *mut objc2::runtime::AnyObject + Retained::into_raw(webview.manager()) as *mut objc2::runtime::AnyObject as *mut std::ffi::c_void, - ns_window: Retained::into_raw(webview.ns_window()) as *mut objc2::runtime::AnyObject + Retained::into_raw(webview.ns_window()) as *mut objc2::runtime::AnyObject as *mut std::ffi::c_void, - }); + )); } #[cfg(target_os = "ios")] { use wry::WebViewExtIOS; - f(Webview { - webview: Retained::into_raw(webview.inner.webview()) - as *mut objc2::runtime::AnyObject + f(Webview::new( + Retained::into_raw(webview.inner.webview()) as *mut objc2::runtime::AnyObject as *mut std::ffi::c_void, - manager: Retained::into_raw(webview.inner.manager()) - as *mut objc2::runtime::AnyObject + Retained::into_raw(webview.inner.manager()) as *mut objc2::runtime::AnyObject as *mut std::ffi::c_void, - view_controller: window.ui_view_controller(), - }); + window.ui_view_controller(), + )); } #[cfg(windows)] { - f(Webview { - controller: webview.controller(), - environment: webview.environment(), - }); + f(Webview::new(webview.controller(), webview.environment())); } #[cfg(target_os = "android")] { - f(webview.handle()) + f(Webview::new(webview.handle())) } } #[cfg(any(debug_assertions, feature = "devtools"))] @@ -4037,13 +4181,9 @@ fn handle_user_message( } } Message::CreateWindow(window_id, handler) => match handler(event_loop) { - // wait for borrow_mut to be available - on Windows we might poll for the window to be inserted - Ok(webview) => loop { - if let Ok(mut windows) = windows.0.try_borrow_mut() { - windows.insert(window_id, webview); - break; - } - }, + Ok(webview) => { + windows.0.borrow_mut().insert(window_id, webview); + } Err(e) => { log::error!("{e}"); } @@ -4349,7 +4489,7 @@ fn handle_event_loop( ); } }, - #[cfg(any(target_os = "macos", target_os = "ios"))] + #[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))] Event::Opened { urls } => { callback(RunEvent::Opened { urls }); } @@ -4360,6 +4500,35 @@ fn handle_event_loop( } => callback(RunEvent::Reopen { has_visible_windows, }), + #[cfg(target_os = "ios")] + Event::SceneRequested { scene, options } => { + callback(RunEvent::SceneRequested { scene, options }); + } + #[cfg(mobile)] + e @ Event::Resumed | e @ Event::Suspended => { + let event = match e { + Event::Resumed => WindowEvent::Resumed, + Event::Suspended => WindowEvent::Suspended, + _ => unreachable!(), + }; + + let windows_ref = windows.0.borrow(); + windows_ref.values().for_each(|window| { + let label = window.label.clone(); + let window_event_listeners = window.window_event_listeners.clone(); + let listeners = window_event_listeners.lock().unwrap(); + for handler in listeners.values() { + handler(&event); + } + + callback(RunEvent::WindowEvent { + label, + event: event.clone(), + }); + }); + + drop(windows_ref); + } _ => (), } } @@ -4463,18 +4632,7 @@ fn create_window( #[cfg(desktop)] if window_builder.prevent_overflow.is_some() || window_builder.center { let monitor = if let Some(window_position) = &window_builder.inner.window.position { - event_loop.available_monitors().find(|m| { - let monitor_pos = m.position(); - let monitor_size = m.size(); - - // type annotations required for 32bit targets. - let window_position = window_position.to_physical::(m.scale_factor()); - - monitor_pos.x <= window_position.x - && window_position.x < monitor_pos.x + monitor_size.width as i32 - && monitor_pos.y <= window_position.y - && window_position.y < monitor_pos.y + monitor_size.height as i32 - }) + find_monitor_for_position(event_loop.available_monitors(), *window_position) } else { event_loop.primary_monitor() }; @@ -4541,11 +4699,35 @@ fn create_window( } }; + #[cfg(any(target_os = "macos", target_os = "linux"))] + let (initial_position, is_fullscreen) = ( + window_builder.inner.window.position, + window_builder.inner.window.fullscreen.is_some(), + ); + + // If fullscreen is requested with an explicit position, resolve the target + // monitor up front so the window is created fullscreen on that display. + #[cfg(any(target_os = "macos", target_os = "linux"))] + if let (true, Some(position)) = (is_fullscreen, initial_position) + && let Some(target_monitor) = + find_monitor_for_position(event_loop.available_monitors(), position) + { + window_builder.inner.window.fullscreen = Some(Fullscreen::Borderless(Some(target_monitor))); + } + let window = window_builder .inner .build(event_loop) + .inspect_err(|e| log::error!("Error creating window: {e:?}")) .map_err(|_| Error::CreateWindow)?; + // On macOS, `with_position` uses the content origin; the title bar is added + // above it. `set_outer_position` is needed for precise window placement. + #[cfg(target_os = "macos")] + if !is_fullscreen && let Some(position) = initial_position { + window.set_outer_position(position); + } + #[cfg(feature = "tracing")] { drop(window_create_span); @@ -4707,6 +4889,16 @@ You may have it installed on another user account, but it is not available for t let is_first_context = web_context.is_empty(); // the context must be stored on the HashMap because it must outlive the WebView on macOS let automation_enabled = std::env::var("TAURI_WEBVIEW_AUTOMATION").as_deref() == Ok("true"); + // Make sure the data directory exists before handing it to the web context, + // or WebView2 / WebKitGTK can panic while initializing the user data folder. + if let Some(user_data_dir) = webview_attributes + .data_directory + .as_ref() + .filter(|dir| !dir.exists()) + { + std::fs::create_dir_all(user_data_dir).map_err(|e| Error::CreateWebview(Box::new(e)))?; + } + let web_context_key = webview_attributes.data_directory; let entry = web_context.entry(web_context_key.clone()); let web_context = match entry { @@ -4737,7 +4929,8 @@ You may have it installed on another user account, but it is not available for t .with_accept_first_mouse(webview_attributes.accept_first_mouse) .with_incognito(webview_attributes.incognito) .with_clipboard(webview_attributes.clipboard) - .with_hotkeys_zoom(webview_attributes.zoom_hotkeys_enabled); + .with_hotkeys_zoom(webview_attributes.zoom_hotkeys_enabled) + .with_general_autofill_enabled(webview_attributes.general_autofill_enabled); if url != "about:blank" { webview_builder = webview_builder.with_url(&url); @@ -4841,63 +5034,56 @@ You may have it installed on another user account, but it is not available for t #[cfg(desktop)] let context = context.clone(); webview_builder = webview_builder.with_new_window_req_handler(move |url, features| { - url - .parse() - .map(|url| { - let response = new_window_handler( - url, - tauri_runtime::webview::NewWindowFeatures::new( - features.size, - features.position, - NewWindowOpener { - #[cfg(desktop)] - webview: features.opener.webview, - #[cfg(windows)] - environment: features.opener.environment, - #[cfg(target_os = "macos")] - target_configuration: features.opener.target_configuration, - }, - ), - ); - match response { - tauri_runtime::webview::NewWindowResponse::Allow => wry::NewWindowResponse::Allow, + let Ok(url) = url.parse() else { + return wry::NewWindowResponse::Deny; + }; + let response = new_window_handler( + url, + tauri_runtime::webview::NewWindowFeatures::new( + features.size, + features.position, + NewWindowOpener { #[cfg(desktop)] - tauri_runtime::webview::NewWindowResponse::Create { window_id } => { - let windows = &context.main_thread.windows.0; - let webview = loop { - if let Some(webview) = windows.try_borrow().ok().and_then(|windows| { - windows - .get(&window_id) - .map(|window| window.webviews.first().unwrap().clone()) - }) { - break webview; - } else { - // on Windows the window is created async so we should wait for it to be available - std::thread::sleep(std::time::Duration::from_millis(50)); - continue; - }; - }; - - #[cfg(desktop)] - wry::NewWindowResponse::Create { - #[cfg(target_os = "macos")] - webview: wry::WebViewExtMacOS::webview(&*webview).as_super().into(), - #[cfg(any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd", - ))] - webview: webview.webview(), - #[cfg(windows)] - webview: webview.webview(), - } - } - tauri_runtime::webview::NewWindowResponse::Deny => wry::NewWindowResponse::Deny, + webview: features.opener.webview, + #[cfg(windows)] + environment: features.opener.environment, + #[cfg(target_os = "macos")] + target_configuration: features.opener.target_configuration, + }, + ), + ); + match response { + tauri_runtime::webview::NewWindowResponse::Allow => wry::NewWindowResponse::Allow, + #[cfg(desktop)] + tauri_runtime::webview::NewWindowResponse::Create { window_id } => { + let windows = &context.main_thread.windows.0; + let webview = windows + .borrow() + .get(&window_id) + .unwrap() + .webviews + .first() + .unwrap() + .clone(); + + #[cfg(desktop)] + wry::NewWindowResponse::Create { + #[cfg(target_os = "macos")] + webview: wry::WebViewExtMacOS::webview(&*webview).as_super().into(), + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd", + ))] + webview: webview.webview(), + #[cfg(windows)] + webview: webview.webview(), } - }) - .unwrap_or(wry::NewWindowResponse::Deny) + } + tauri_runtime::webview::NewWindowResponse::Deny => wry::NewWindowResponse::Deny, + } }); } @@ -5070,6 +5256,35 @@ You may have it installed on another user account, but it is not available for t webview_builder = webview_builder.with_allow_link_preview(webview_attributes.allow_link_preview); + + if let Some(on_web_content_process_terminate_handler) = + pending.on_web_content_process_terminate_handler + { + webview_builder = webview_builder + .with_on_web_content_process_terminate_handler(on_web_content_process_terminate_handler); + } else { + log::debug!("web content process terminated"); + let context_ = context.clone(); + let window_id_ = window_id.clone(); + webview_builder = webview_builder.with_on_web_content_process_terminate_handler(move || { + if let Ok(windows) = &context_.main_thread.windows.0.try_borrow() { + if let Some(window) = windows.get(&*window_id_.lock().unwrap()) { + if let Some(webview) = window.webviews.iter().find(|w| w.id == id) { + match webview.reload() { + Ok(_) => log::debug!("webview reloaded"), + Err(e) => log::error!("failed to reload webview: {}", e), + } + } else { + log::error!("failed to find webview") + } + } else { + log::error!("failed to get window") + } + } else { + log::error!("failed to borrow windows") + } + }); + } } #[cfg(target_os = "ios")] diff --git a/crates/tauri-runtime-wry/src/monitor/macos.rs b/crates/tauri-runtime-wry/src/monitor/macos.rs index 6ed9f3c1dd56..aca8e334d0a5 100644 --- a/crates/tauri-runtime-wry/src/monitor/macos.rs +++ b/crates/tauri-runtime-wry/src/monitor/macos.rs @@ -19,6 +19,8 @@ impl super::MonitorExt for tao::monitor::MonitorHandle { position.x += visible_frame.origin.x - screen_frame.origin.x; + position.y += (screen_frame.origin.y + screen_frame.size.height) + - (visible_frame.origin.y + visible_frame.size.height); PhysicalRect { size: LogicalSize::new(visible_frame.size.width, visible_frame.size.height) .to_physical(scale_factor), diff --git a/crates/tauri-runtime-wry/src/webview.rs b/crates/tauri-runtime-wry/src/webview.rs index 67b607392b88..4a78c0fb22d3 100644 --- a/crates/tauri-runtime-wry/src/webview.rs +++ b/crates/tauri-runtime-wry/src/webview.rs @@ -10,20 +10,83 @@ target_os = "openbsd" ))] mod imp { - pub type Webview = webkit2gtk::WebView; + /// The platform webview handle backed by the wry runtime. + pub struct Webview { + webview: webkit2gtk::WebView, + } + + impl Webview { + pub(crate) fn new(webview: webkit2gtk::WebView) -> Self { + Self { webview } + } + + /// Returns the [`webkit2gtk::WebView`] handle. + pub fn inner(&self) -> webkit2gtk::WebView { + self.webview.clone() + } + } } #[cfg(target_vendor = "apple")] mod imp { use std::ffi::c_void; + /// The platform webview handle backed by the wry runtime. pub struct Webview { - pub webview: *mut c_void, - pub manager: *mut c_void, + webview: *mut c_void, + manager: *mut c_void, + #[cfg(target_os = "macos")] + ns_window: *mut c_void, + #[cfg(target_os = "ios")] + view_controller: *mut c_void, + } + + impl Webview { + pub(crate) fn new( + webview: *mut c_void, + manager: *mut c_void, + #[cfg(target_os = "macos")] ns_window: *mut c_void, + #[cfg(target_os = "ios")] view_controller: *mut c_void, + ) -> Self { + Self { + webview, + manager, + #[cfg(target_os = "macos")] + ns_window, + #[cfg(target_os = "ios")] + view_controller, + } + } + + /// Returns the [WKWebView] handle. + /// + /// [WKWebView]: https://developer.apple.com/documentation/webkit/wkwebview + pub fn inner(&self) -> *mut c_void { + self.webview + } + + /// Returns WKWebView [controller] handle. + /// + /// [controller]: https://developer.apple.com/documentation/webkit/wkusercontentcontroller + pub fn controller(&self) -> *mut c_void { + self.manager + } + + /// Returns [NSWindow] associated with the WKWebView webview. + /// + /// [NSWindow]: https://developer.apple.com/documentation/appkit/nswindow #[cfg(target_os = "macos")] - pub ns_window: *mut c_void, + pub fn ns_window(&self) -> *mut c_void { + self.ns_window + } + + /// Returns [UIViewController] used by the WKWebView webview NSWindow. + /// + /// [UIViewController]: https://developer.apple.com/documentation/uikit/uiviewcontroller #[cfg(target_os = "ios")] - pub view_controller: *mut c_void, + pub fn view_controller(&self) -> *mut c_void { + self.view_controller + } } } @@ -32,16 +95,55 @@ mod imp { use webview2_com::Microsoft::Web::WebView2::Win32::{ ICoreWebView2Controller, ICoreWebView2Environment, }; + + /// The platform webview handle backed by the wry runtime. pub struct Webview { - pub controller: ICoreWebView2Controller, - pub environment: ICoreWebView2Environment, + controller: ICoreWebView2Controller, + environment: ICoreWebView2Environment, + } + + impl Webview { + pub(crate) fn new( + controller: ICoreWebView2Controller, + environment: ICoreWebView2Environment, + ) -> Self { + Self { + controller, + environment, + } + } + + /// Returns the WebView2 controller. + pub fn controller(&self) -> ICoreWebView2Controller { + self.controller.clone() + } + + /// Returns the WebView2 environment. + pub fn environment(&self) -> ICoreWebView2Environment { + self.environment.clone() + } } } #[cfg(target_os = "android")] mod imp { use wry::JniHandle; - pub type Webview = JniHandle; + + /// The platform webview handle backed by the wry runtime. + pub struct Webview { + handle: JniHandle, + } + + impl Webview { + pub(crate) fn new(handle: JniHandle) -> Self { + Self { handle } + } + + /// Returns the handle for JNI execution. + pub fn jni_handle(&self) -> JniHandle { + self.handle + } + } } pub use imp::*; diff --git a/crates/tauri-runtime/CHANGELOG.md b/crates/tauri-runtime/CHANGELOG.md index 47263c499fe9..fab7ce5bb382 100644 --- a/crates/tauri-runtime/CHANGELOG.md +++ b/crates/tauri-runtime/CHANGELOG.md @@ -1,5 +1,32 @@ # Changelog +## \[2.11.2] + +### Dependencies + +- Upgraded to `tauri-utils@2.9.2` + +## \[2.11.1] + +### Dependencies + +- Upgraded to `tauri-utils@2.9.1` + +## \[2.11.0] + +### New Features + +- [`001c8fe3d`](https://www.github.com/tauri-apps/tauri/commit/001c8fe3d288802de9a8c29cfd2f46f9220d97c5) ([#14722](https://www.github.com/tauri-apps/tauri/pull/14722)) Add a WebView option to control browser-level general autofill behavior. This option does not disable password or credit card autofill. On Windows (WebView2), setting it to true disables the general autofill "Suggestions" UI, which may appear even when `autocomplete="off"` is specified on input elements. On Linux, macOS, iOS, and Android, this option is currently unsupported and performs no operation. +- [`b27be063f`](https://www.github.com/tauri-apps/tauri/commit/b27be063ff3052cb1071ac3ec719cfa104460fa4) ([#14925](https://www.github.com/tauri-apps/tauri/pull/14925)) Add `eval_with_callback` to the Tauri webview APIs and runtime dispatch layers. +- [`cc5c97602`](https://www.github.com/tauri-apps/tauri/commit/cc5c976027b0ab2431c13ec5b2e201d4414a8a6e) ([#14486](https://www.github.com/tauri-apps/tauri/pull/14486)) Trigger `RunEvent::Opened` on Android. +- [`eb0312ea9`](https://www.github.com/tauri-apps/tauri/commit/eb0312ea9e493954298ac0b3fdaae7eafb52750e) ([#15199](https://www.github.com/tauri-apps/tauri/pull/15199)) Propagates the `Event::Suspended` and `Event::Resumed` events from `tao` when they are emitted on mobile targets. +- [`093e2b47c`](https://www.github.com/tauri-apps/tauri/commit/093e2b47c01361c18783e9ff18750388e41650c5) ([#14484](https://www.github.com/tauri-apps/tauri/pull/14484)) Support creating multiple windows on Android (activity embedding) and iOS (scenes). +- [`1063c48c5`](https://www.github.com/tauri-apps/tauri/commit/1063c48c5e7d099ad74d28a937edf42e3f5c9f03) ([#14523](https://www.github.com/tauri-apps/tauri/pull/14523)) Add handler for web content process termination on macOS and iOS. + +### Dependencies + +- Upgraded to `tauri-utils@2.9.0` + ## \[2.10.1] ### Dependencies diff --git a/crates/tauri-runtime/Cargo.toml b/crates/tauri-runtime/Cargo.toml index 1615c9768f74..c66c9d630f0e 100644 --- a/crates/tauri-runtime/Cargo.toml +++ b/crates/tauri-runtime/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-runtime" -version = "2.10.1" +version = "2.11.2" description = "Runtime for Tauri applications" exclude = ["CHANGELOG.md", "/target"] readme = "README.md" @@ -27,7 +27,7 @@ targets = [ serde = { version = "1", features = ["derive"] } serde_json = "1" thiserror = "2" -tauri-utils = { version = "2.8.3", path = "../tauri-utils" } +tauri-utils = { version = "2.9.2", path = "../tauri-utils" } http = "1" raw-window-handle = "0.6" url = { version = "2" } @@ -50,6 +50,8 @@ objc2 = "0.6" objc2-ui-kit = { version = "0.3.0", default-features = false, features = [ "UIView", "UIResponder", + "UIScene", + "UISceneOptions", ] } [target."cfg(target_os = \"macos\")".dependencies] diff --git a/crates/tauri-runtime/src/dpi.rs b/crates/tauri-runtime/src/dpi.rs index e30d4890c017..3e08eaf2f0c2 100644 --- a/crates/tauri-runtime/src/dpi.rs +++ b/crates/tauri-runtime/src/dpi.rs @@ -14,6 +14,22 @@ pub struct Rect { pub size: dpi::Size, } +impl Rect { + pub fn to_physical(self, scale: f64) -> PhysicalRect { + PhysicalRect { + position: self.position.to_physical(scale), + size: self.size.to_physical(scale), + } + } + + pub fn to_logical(self, scale: f64) -> LogicalRect { + LogicalRect { + position: self.position.to_logical(scale), + size: self.size.to_logical(scale), + } + } +} + impl Default for Rect { fn default() -> Self { Self { @@ -41,6 +57,15 @@ impl Default for PhysicalRect { } } +impl PhysicalRect { + pub fn to_logical(self, scale: f64) -> LogicalRect { + LogicalRect { + position: self.position.to_logical(scale), + size: self.size.to_logical(scale), + } + } +} + /// A rectangular region in logical pixels. #[derive(Clone, Copy, Debug, Serialize)] pub struct LogicalRect { @@ -58,3 +83,12 @@ impl Default for LogicalRect { } } } + +impl LogicalRect { + pub fn to_physical(self, scale: f64) -> PhysicalRect { + PhysicalRect { + position: self.position.to_physical(scale), + size: self.size.to_physical(scale), + } + } +} diff --git a/crates/tauri-runtime/src/lib.rs b/crates/tauri-runtime/src/lib.rs index 92feed0594fd..e9eb8d276973 100644 --- a/crates/tauri-runtime/src/lib.rs +++ b/crates/tauri-runtime/src/lib.rs @@ -242,6 +242,20 @@ pub enum RunEvent { }, /// A custom event defined by the user. UserEvent(T), + /// Emitted when a scene is requested by the system. + /// + /// This event is emitted when a scene is requested by the system. + /// Scenes created by [`Window::new`] are not emitted with this event. + /// It is also not emitted for the main scene. + #[cfg(target_os = "ios")] + SceneRequested { + /// Scene that was requested by the system. + scene: objc2::rc::Retained, + /// Options that were used to request the scene. + /// + /// This lets you determine why the scene was requested. + options: objc2::rc::Retained, + }, } /// Action to take when the event loop is about to exit @@ -424,6 +438,11 @@ pub trait Runtime: Debug + Sized + 'static { type EventLoopProxy: EventLoopProxy; /// The platform specific webview attributes. type PlatformSpecificWebviewAttribute: Send + Sync + 'static; + /// The platform webview handle exposed through [`WebviewDispatch::with_webview`]. + /// + /// This is the runtime-specific type the user interacts with to reach the + /// underlying platform webview APIs. + type Webview: 'static; /// The platform specific runtime init arguments. Must implement [`InitAttribute`]. type PlatformSpecificInitAttribute: InitAttribute + Send + Sync + 'static; /// Data about the window that requested the new window for [`PendingWebview::new_window_handler`]. @@ -550,7 +569,10 @@ pub trait WebviewDispatch: Debug + Clone + Send + Sync + Sized + ' fn on_webview_event(&self, f: F) -> WebviewEventId; /// Runs a closure with the platform webview object as argument. - fn with_webview) + Send + 'static>(&self, f: F) -> Result<()>; + fn with_webview>::Webview) + Send + 'static>( + &self, + f: F, + ) -> Result<()>; /// Open the web inspector which is usually called devtools. #[cfg(any(debug_assertions, feature = "devtools"))] @@ -621,6 +643,16 @@ pub trait WebviewDispatch: Debug + Clone + Send + Sync + Sized + ' /// Executes javascript on the window this [`WindowDispatch`] represents. fn eval_script>(&self, script: S) -> Result<()>; + /// Evaluate JavaScript with callback function on the webview this [`WebviewDispatch`] represents. + /// The evaluation result will be serialized into a JSON string and passed to the callback function. + /// + /// Exception is ignored because of the limitation on Windows. You can catch it yourself and return as string as a workaround. + fn eval_script_with_callback>( + &self, + script: S, + callback: impl Fn(String) + Send + 'static, + ) -> Result<()>; + /// Moves the webview to the given window. fn reparent(&self, window_id: WindowId) -> Result<()>; @@ -792,6 +824,14 @@ pub trait WindowDispatch: Debug + Clone + Send + Sync + Sized + 's ))] fn default_vbox(&self) -> Result; + /// Returns the name of the Android activity associated with this window. + #[cfg(target_os = "android")] + fn activity_name(&self) -> Result; + + /// Returns the identifier of the UIScene tied to this UIWindow. + #[cfg(target_os = "ios")] + fn scene_identifier(&self) -> Result; + /// Raw window handle. fn window_handle( &self, diff --git a/crates/tauri-runtime/src/webview.rs b/crates/tauri-runtime/src/webview.rs index e8ccf44b1b2a..c648bb22c7b7 100644 --- a/crates/tauri-runtime/src/webview.rs +++ b/crates/tauri-runtime/src/webview.rs @@ -44,6 +44,9 @@ pub type AddressChangedHandler = dyn Fn(&Url) + Send + Sync + 'static; pub type DownloadHandler = dyn Fn(DownloadEvent) -> bool + Send + Sync; +#[cfg(any(target_os = "macos", target_os = "ios"))] +type OnWebContentProcessTerminateHandler = dyn Fn() + Send; + #[cfg(target_os = "ios")] type InputAccessoryViewBuilderFn = dyn Fn(&objc2_ui_kit::UIView) -> Option> + Send @@ -161,7 +164,7 @@ pub enum ScrollBarStyle { /// Fluent UI style overlay scrollbars. **Windows Only** /// /// Requires WebView2 Runtime version 125.0.2535.41 or higher, does nothing on older versions, - /// see https://learn.microsoft.com/en-us/microsoft-edge/webview2/release-notes/?tabs=dotnetcsharp#10253541 + /// see FluentOverlay, } @@ -200,13 +203,16 @@ pub struct PendingWebview> { #[cfg(target_os = "android")] #[allow(clippy::type_complexity)] pub on_webview_created: - Option) -> Result<(), jni::errors::Error> + Send>>, + Option) -> Result<(), jni::errors::Error> + Send + Sync>>, pub web_resource_request_handler: Option>, pub on_page_load_handler: Option>, pub download_handler: Option>, + + #[cfg(any(target_os = "macos", target_os = "ios"))] + pub on_web_content_process_terminate_handler: Option>, } impl> PendingWebview { @@ -237,6 +243,8 @@ impl> PendingWebview { web_resource_request_handler: None, on_page_load_handler: None, download_handler: None, + #[cfg(any(target_os = "macos", target_os = "ios"))] + on_web_content_process_terminate_handler: None, }) } } @@ -260,7 +268,7 @@ impl> PendingWebview { #[cfg(target_os = "android")] pub fn on_webview_created< - F: Fn(CreationContext<'_, '_>) -> Result<(), jni::errors::Error> + Send + 'static, + F: Fn(CreationContext<'_, '_>) -> Result<(), jni::errors::Error> + Send + Sync + 'static, >( mut self, f: F, @@ -349,6 +357,24 @@ pub struct WebviewAttributes { /// see https://docs.rs/objc2-web-kit/latest/objc2_web_kit/struct.WKWebView.html#method.allowsLinkPreview pub allow_link_preview: bool, pub scroll_bar_style: ScrollBarStyle, + /// Controls the WebView's browser-level general autofill behavior. + /// + /// **This option does not disable password or credit card autofill.** + /// + /// When set to `false`, the WebView will not automatically populate + /// general form fields using previously stored data such as addresses + /// or contact information. + /// + /// If not specified, this is `true` by default. + /// + /// ## Platform-specific + /// + /// - **Windows**: Supported. WebView2's autofill feature (called + /// "Suggestions") may not honor `autocomplete="off"` on input + /// elements in some cases. + /// - **Linux / Android / iOS / macOS**: Unsupported and performs no + /// operation. + pub general_autofill_enabled: bool, /// Allows overriding the keyboard accessory view on iOS. /// Returning `None` effectively removes the view. /// @@ -400,7 +426,8 @@ impl From<&WindowConfig> for WebviewAttributes { #[cfg(windows)] ConfigScrollBarStyle::FluentOverlay => ScrollBarStyle::FluentOverlay, _ => ScrollBarStyle::Default, - }); + }) + .general_autofill_enabled(config.general_autofill_enabled); #[cfg(any(not(target_os = "macos"), feature = "macos-private-api"))] { @@ -475,6 +502,7 @@ impl WebviewAttributes { javascript_disabled: false, allow_link_preview: true, scroll_bar_style: ScrollBarStyle::Default, + general_autofill_enabled: true, #[cfg(target_os = "ios")] input_accessory_view_builder: None, } @@ -729,7 +757,7 @@ impl WebviewAttributes { /// - **iOS**: Supported since version 17.0+. /// - **macOS**: Supported since version 14.0+. /// - /// see https://github.com/tauri-apps/tauri/issues/5250#issuecomment-2569380578 + /// see #[must_use] pub fn background_throttling(mut self, policy: Option) -> Self { self.background_throttling = policy; @@ -754,6 +782,29 @@ impl WebviewAttributes { self.scroll_bar_style = style; self } + + /// Controls the WebView's browser-level general autofill behavior. + /// + /// **This option does not disable password or credit card autofill.** + /// + /// When set to `false`, the WebView will not automatically populate + /// general form fields using previously stored data such as addresses + /// or contact information. + /// + /// By default, this is `true`. + /// + /// ## Platform-specific + /// + /// - **Windows**: Supported. WebView2's autofill feature (called + /// "Suggestions") may not honor `autocomplete="off"` on input + /// elements in some cases. + /// - **Linux / Android / iOS / macOS**: Unsupported and performs no + /// operation. + #[must_use] + pub fn general_autofill_enabled(mut self, enabled: bool) -> Self { + self.general_autofill_enabled = enabled; + self + } } /// IPC handler. diff --git a/crates/tauri-runtime/src/window.rs b/crates/tauri-runtime/src/window.rs index 790910a3cc58..de911f255b1a 100644 --- a/crates/tauri-runtime/src/window.rs +++ b/crates/tauri-runtime/src/window.rs @@ -62,6 +62,26 @@ pub enum WindowEvent { /// /// Applications might wish to react to this to change the theme of the content of the window when the system changes the window theme. ThemeChanged(Theme), + + /// Emitted when the application has been suspended. + /// + /// ## Platform-specific + /// + /// - **Android**: This is triggered by `onPause` method of the Activity. + /// - **iOS**: This is triggered by `applicationWillResignActive` method of the UIApplicationDelegate. + /// - **Linux / macOS / Windows**: Unsupported. + #[cfg(mobile)] + Suspended, + + /// Emitted when the application has been resumed. + /// + /// ## Platform-specific + /// + /// - **Android**: This is triggered by `onResume` method of the Activity. The first onResume() is ignored to match the iOS implementation, since that is called on activity creation. + /// - **iOS**: This is triggered by `applicationWillEnterForeground` method of the UIApplicationDelegate. + /// - **Linux / macOS / Windows**: Unsupported. + #[cfg(mobile)] + Resumed, } /// An event from a window. @@ -477,6 +497,23 @@ pub trait WindowBuilder: WindowBuilderBase { /// Sets custom name for Windows' window class. **Windows only**. #[must_use] fn window_classname>(self, window_classname: S) -> Self; + + /// The name of the activity to create for this webview window. + #[cfg(target_os = "android")] + fn activity_name>(self, class_name: S) -> Self; + + /// Sets the name of the activity that is creating this webview window. + /// + /// This is important to determine which stack the activity will belong to. + #[cfg(target_os = "android")] + fn created_by_activity_name>(self, class_name: S) -> Self; + + /// Sets the identifier of the UIScene that is requesting the creation of this new scene, + /// establishing a relationship between the two scenes. + /// + /// By default the system uses the foreground scene. + #[cfg(target_os = "ios")] + fn requested_by_scene_identifier>(self, identifier: S) -> Self; } /// A window that has yet to be built. diff --git a/crates/tauri-schema-generator/schemas/config.schema.json b/crates/tauri-schema-generator/schemas/config.schema.json index cbde416db413..e3426ac0cce5 100644 --- a/crates/tauri-schema-generator/schemas/config.schema.json +++ b/crates/tauri-schema-generator/schemas/config.schema.json @@ -1,5 +1,5 @@ { - "$id": "https://schema.tauri.app/config/2.10.3", + "$id": "https://schema.tauri.app/config/2.11.2", "$schema": "http://json-schema.org/draft-07/schema#", "title": "Config", "description": "The Tauri configuration object.\nIt is read from a file where you can define your frontend assets,\nconfigure the bundler and define a tray icon.\n\nThe configuration file is generated by the\n[`tauri init`](https://v2.tauri.app/reference/cli/#init) command that lives in\nyour Tauri application source directory (src-tauri).\n\nOnce generated, you may modify it at will to customize your Tauri application.\n\n## File Formats\n\nBy default, the configuration is defined as a JSON file named `tauri.conf.json`.\n\nTauri also supports JSON5 and TOML files via the `config-json5` and `config-toml` Cargo features, respectively.\nThe JSON5 file name must be either `tauri.conf.json` or `tauri.conf.json5`.\nThe TOML file name is `Tauri.toml`.\n\n## Platform-Specific Configuration\n\nIn addition to the default configuration file, Tauri can\nread a platform-specific configuration from `tauri.linux.conf.json`,\n`tauri.windows.conf.json`, `tauri.macos.conf.json`, `tauri.android.conf.json` and `tauri.ios.conf.json`\n(or `Tauri.linux.toml`, `Tauri.windows.toml`, `Tauri.macos.toml`, `Tauri.android.toml` and `Tauri.ios.toml` if the `Tauri.toml` format is used),\nwhich gets merged with the main configuration object.\n\n## Configuration Structure\n\nThe configuration is composed of the following objects:\n\n- [`app`](#appconfig): The Tauri configuration\n- [`build`](#buildconfig): The build configuration\n- [`bundle`](#bundleconfig): The bundle configurations\n- [`plugins`](#pluginconfig): The plugins configuration\n\nExample tauri.config.json file:\n\n```json\n{\n \"productName\": \"tauri-app\",\n \"version\": \"0.1.0\",\n \"build\": {\n \"beforeBuildCommand\": \"\",\n \"beforeDevCommand\": \"\",\n \"devUrl\": \"http://localhost:3000\",\n \"frontendDist\": \"../dist\"\n },\n \"app\": {\n \"security\": {\n \"csp\": null\n },\n \"windows\": [\n {\n \"fullscreen\": false,\n \"height\": 600,\n \"resizable\": true,\n \"title\": \"Tauri App\",\n \"width\": 800\n }\n ]\n },\n \"bundle\": {},\n \"plugins\": {}\n}\n```", @@ -68,7 +68,10 @@ "description": "The build configuration.", "default": { "removeUnusedCommands": false, - "additionalWatchFolders": [] + "additionalWatchFolders": [], + "windows": { + "staticVCRuntime": true + } }, "allOf": [ { @@ -94,9 +97,11 @@ "silent": true }, "allowDowngrades": true, + "minimumWebview2Version": null, "wix": null, "nsis": null, - "signCommand": null + "signCommand": null, + "bundleVCRuntime": false }, "linux": { "appimage": { @@ -549,7 +554,7 @@ ] }, "backgroundThrottling": { - "description": "Change the default background throttling behaviour.\n\nBy default, browsers use a suspend policy that will throttle timers and even unload\nthe whole tab (view) to free resources after roughly 5 minutes when a view became\nminimized or hidden. This will pause all tasks until the documents visibility state\nchanges back from hidden to visible by bringing the view back to the foreground.\n\n## Platform-specific\n\n- **Linux / Windows / Android**: Unsupported. Workarounds like a pending WebLock transaction might suffice.\n- **iOS**: Supported since version 17.0+.\n- **macOS**: Supported since version 14.0+.\n\nsee https://github.com/tauri-apps/tauri/issues/5250#issuecomment-2569380578", + "description": "Change the default background throttling behaviour.\n\nBy default, browsers use a suspend policy that will throttle timers and even unload\nthe whole tab (view) to free resources after roughly 5 minutes when a view became\nminimized or hidden. This will pause all tasks until the documents visibility state\nchanges back from hidden to visible by bringing the view back to the foreground.\n\n## Platform-specific\n\n- **Linux / Windows / Android**: Unsupported. Workarounds like a pending WebLock transaction might suffice.\n- **iOS**: Supported since version 17.0+.\n- **macOS**: Supported since version 14.0+.\n\nsee ", "anyOf": [ { "$ref": "#/definitions/BackgroundThrottlingPolicy" @@ -604,6 +609,32 @@ "$ref": "#/definitions/ScrollBarStyle" } ] + }, + "activityName": { + "description": "The name of the Android activity to create for this window.", + "type": [ + "string", + "null" + ] + }, + "createdByActivityName": { + "description": "The name of the Android activity that is creating this webview window.\n\nThis is important to determine which stack the activity will belong to.", + "type": [ + "string", + "null" + ] + }, + "requestedBySceneIdentifier": { + "description": "Sets the identifier of the scene that is requesting the new scene,\nestablishing a relationship between the two scenes.\n\nBy default the system uses the foreground scene.", + "type": [ + "string", + "null" + ] + }, + "generalAutofillEnabled": { + "description": "Controls the WebView's browser-level general autofill behavior.\n\n**This option does not disable password or credit card autofill.**\n\nWhen set to `false`, the WebView will not automatically populate\ngeneral form fields using previously stored data such as addresses\nor contact information.\n\nIf not specified, this is `true` by default.\n\n## Platform-specific\n\n- **Windows**: Supported. WebView2's autofill feature (called\n \"Suggestions\") may not honor `autocomplete=\"off\"` on input\n elements in some cases.\n- **Linux / Android / iOS / macOS**: Unsupported and performs no\n operation.", + "type": "boolean", + "default": true } }, "additionalProperties": false @@ -1068,7 +1099,7 @@ "const": "default" }, { - "description": "Fluent UI style overlay scrollbars. **Windows Only**\n\nRequires WebView2 Runtime version 125.0.2535.41 or higher, does nothing on older versions,\nsee https://learn.microsoft.com/en-us/microsoft-edge/webview2/release-notes/?tabs=dotnetcsharp#10253541", + "description": "Fluent UI style overlay scrollbars. **Windows Only**\n\nRequires WebView2 Runtime version 125.0.2535.41 or higher, does nothing on older versions,\nsee ", "type": "string", "const": "fluentOverlay" } @@ -1863,6 +1894,17 @@ "type": "string" }, "default": [] + }, + "windows": { + "description": "Windows-specific build configuration.", + "default": { + "staticVCRuntime": true + }, + "allOf": [ + { + "$ref": "#/definitions/WindowsBuildConfig" + } + ] } }, "additionalProperties": false @@ -1990,6 +2032,18 @@ } ] }, + "WindowsBuildConfig": { + "description": "Windows-specific build configuration.", + "type": "object", + "properties": { + "staticVCRuntime": { + "description": "Whether to statically link the Visual C++ runtime into the application binary on Windows MSVC targets.", + "type": "boolean", + "default": true + } + }, + "additionalProperties": false + }, "BundleConfig": { "description": "Configuration for tauri-bundler.\n\nSee more: ", "type": "object", @@ -2129,9 +2183,11 @@ "silent": true }, "allowDowngrades": true, + "minimumWebview2Version": null, "wix": null, "nsis": null, - "signCommand": null + "signCommand": null, + "bundleVCRuntime": false }, "allOf": [ { @@ -2370,7 +2426,7 @@ ] }, "mimeType": { - "description": "The mime-type e.g. 'image/png' or 'text/plain'. Linux-only.", + "description": "The mime-type of the association, e.g. `'image/png'` or `'text/plain'`.\n\n- **Linux**: written as `MimeType=` in the `.desktop` file.\n- **macOS / iOS**: added as `public.mime-type` in the `UTTypeTagSpecification` dictionary of\n the `UTExportedTypeDeclarations` entry in `Info.plist`.\n- **Android**: used as `android:mimeType` in the `` element of an ``\n in `AndroidManifest.xml`.", "type": [ "string", "null" @@ -2395,6 +2451,16 @@ "type": "null" } ] + }, + "androidIntentActionFilters": { + "description": "Intent action filters for this file association.\n\nBy default all filters are used.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/AndroidIntentAction" + } } }, "additionalProperties": false, @@ -2485,6 +2551,26 @@ "identifier" ] }, + "AndroidIntentAction": { + "description": "Android intent action.", + "oneOf": [ + { + "description": "ACTION_SEND.\n\n", + "type": "string", + "const": "send" + }, + { + "description": "ACTION_SEND_MULTIPLE.\n\n", + "type": "string", + "const": "sendMultiple" + }, + { + "description": "ACTION_VIEW.\n\n", + "type": "string", + "const": "view" + } + ] + }, "WindowsConfig": { "description": "Windows bundler configuration.\n\nSee more: ", "type": "object", @@ -2532,6 +2618,13 @@ "type": "boolean", "default": true }, + "minimumWebview2Version": { + "description": "Try to ensure that the WebView2 version is equal to or newer than this version,\nif the user's WebView2 is older than this version,\nthe installer will try to trigger a WebView2 update.", + "type": [ + "string", + "null" + ] + }, "wix": { "description": "Configuration for the MSI generated with WiX.", "anyOf": [ @@ -2564,6 +2657,11 @@ "type": "null" } ] + }, + "bundleVCRuntime": { + "description": "Whether to bundle the Visual C++ runtime DLLs alongside the application.\n\nThis can be particularly useful when your application includes sidecars or DLLs that do\nnot statically link the Visual C++ runtime and require the runtime DLLs at runtime, and\nyou do not want to require users to install the Visual C++ Redistributable. This can also\nbe useful when `build > windows > staticVCRuntime` is set to `false`.", + "type": "boolean", + "default": false } }, "additionalProperties": false @@ -2842,6 +2940,20 @@ "null" ] }, + "uninstallerIcon": { + "description": "The path to an icon file used as the uninstaller icon.", + "type": [ + "string", + "null" + ] + }, + "uninstallerHeaderImage": { + "description": "The path to a bitmap file to display on the header of uninstallers pages.\nDefaults to [`Self::header_image`]. If this is set but [`Self::header_image`] is not, a default image from NSIS will be applied to `header_image`\n\nThe recommended dimensions are 150px x 57px.", + "type": [ + "string", + "null" + ] + }, "installMode": { "description": "Whether the installation will be for all users or just the current user.", "default": "currentUser", @@ -2852,7 +2964,7 @@ ] }, "languages": { - "description": "A list of installer languages.\nBy default the OS language is used. If the OS language is not in the list of languages, the first language will be used.\nTo allow the user to select the language, set `display_language_selector` to `true`.\n\nSee for the complete list of languages.", + "description": "A list of installer languages. Default to `[\"English\"]` if not set.\n\nBy default the OS language is used. If the OS language is not in the list of languages, the first language will be used.\nTo allow the user to select the language, set `display_language_selector` to `true`.\n\nSee for the complete list of languages.", "type": [ "array", "null" @@ -2862,7 +2974,7 @@ } }, "customLanguageFiles": { - "description": "A key-value pair where the key is the language and the\nvalue is the path to a custom `.nsh` file that holds the translated text for tauri's custom messages.\n\nSee for an example `.nsh` file.\n\n**Note**: the key must be a valid NSIS language and it must be added to [`NsisConfig`] languages array,", + "description": "A key-value pair where the key is the language and the\nvalue is the path to a custom `.nsh` file that holds the translated text for tauri's custom messages.\n\nSee for an example `.nsh` file.\n\n**Note**: the key must be a valid NSIS language and it must be added to the [`Self::languages`] array,", "type": [ "object", "null" @@ -2900,11 +3012,12 @@ ] }, "minimumWebview2Version": { - "description": "Try to ensure that the WebView2 version is equal to or newer than this version,\nif the user's WebView2 is older than this version,\nthe installer will try to trigger a WebView2 update.", + "description": "Deprecated: use [`WindowsConfig::minimum_webview2_version`] (`bundle > windows > minimumWebview2Version`) instead.\n\nTry to ensure that the WebView2 version is equal to or newer than this version,\nif the user's WebView2 is older than this version,\nthe installer will try to trigger a WebView2 update.", "type": [ "string", "null" - ] + ], + "deprecated": true } }, "additionalProperties": false @@ -3685,6 +3798,13 @@ "description": "Whether to automatically increment the `versionCode` on each build.\n\n- If `true`, the generator will try to read the last `versionCode` from\n `tauri.properties` and increment it by 1 for every build.\n- If `false` or not set, it falls back to `version_code` or semver-derived logic.\n\nNote that to use this feature, you should remove `/tauri.properties` from `src-tauri/gen/android/app/.gitignore` so the current versionCode is committed to the repository.", "type": "boolean", "default": false + }, + "debugApplicationIdSuffix": { + "description": "Application ID suffix to append for debug builds.\nThis allows installing debug and release versions side-by-side on the same device.\nExample: \".debug\" will make debug builds use \"com.example.app.debug\" as the application ID.", + "type": [ + "string", + "null" + ] } }, "additionalProperties": false diff --git a/crates/tauri-schema-worker/Cargo.toml b/crates/tauri-schema-worker/Cargo.toml index 47e9ce1e124e..7c407c2fb6bc 100644 --- a/crates/tauri-schema-worker/Cargo.toml +++ b/crates/tauri-schema-worker/Cargo.toml @@ -8,8 +8,8 @@ publish = false crate-type = ["cdylib"] [dependencies] -worker = { version = "0.7", features = ['http', 'axum'] } -worker-macros = { version = "0.7", features = ['http'] } +worker = { version = "0.8", features = ['http', 'axum'] } +worker-macros = { version = "0.8", features = ['http'] } console_error_panic_hook = { version = "0.1" } axum = { version = "0.8", default-features = false } tower-service = "0.3" diff --git a/crates/tauri-schema-worker/package.json b/crates/tauri-schema-worker/package.json index 7e7326a0dfb3..c5cba019328e 100644 --- a/crates/tauri-schema-worker/package.json +++ b/crates/tauri-schema-worker/package.json @@ -8,6 +8,6 @@ "dev": "wrangler dev" }, "devDependencies": { - "wrangler": "^4.70.0" + "wrangler": "^4.75.0" } } diff --git a/crates/tauri-schema-worker/src/config.rs b/crates/tauri-schema-worker/src/config.rs index 3145cd65e3a0..5812743ea080 100644 --- a/crates/tauri-schema-worker/src/config.rs +++ b/crates/tauri-schema-worker/src/config.rs @@ -6,8 +6,8 @@ use anyhow::Context; use axum::{ Router, extract::Path, - http::{StatusCode, header}, - response::Result, + http::{HeaderValue, StatusCode, header}, + response::{IntoResponse, Result}, routing::get, }; use semver::{Version, VersionReq}; @@ -48,23 +48,26 @@ pub fn router() -> Router { .route("/config/{version}", get(schema_for_version)) } -async fn schema_for_version(Path(version): Path) -> Result { +async fn schema_for_version(Path(version): Path) -> Result { try_schema_for_version(version) .await + .map(JsonResponse) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string())) .map_err(Into::into) } -async fn stable_schema() -> Result { +async fn stable_schema() -> Result { try_stable_schema() .await + .map(JsonResponse) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string())) .map_err(Into::into) } -async fn next_schema() -> Result { +async fn next_schema() -> Result { try_next_schema() .await + .map(JsonResponse) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string())) .map_err(Into::into) } @@ -172,3 +175,18 @@ fn fetch_req(url: &str) -> anyhow::Result { ) .map_err(Into::into) } + +struct JsonResponse(String); + +impl IntoResponse for JsonResponse { + fn into_response(self) -> axum::response::Response { + ( + [( + header::CONTENT_TYPE, + HeaderValue::from_static("application/json"), + )], + self.0, + ) + .into_response() + } +} diff --git a/crates/tauri-schema-worker/wrangler.toml b/crates/tauri-schema-worker/wrangler.toml index 661b6059ab28..e7e56a635766 100644 --- a/crates/tauri-schema-worker/wrangler.toml +++ b/crates/tauri-schema-worker/wrangler.toml @@ -9,7 +9,7 @@ send_metrics = false # The minor version of worker-build must match worker/worker-macros in Cargo.toml! [build] -command = "cargo install -q worker-build@^0.7 && worker-build --release" +command = "cargo install -q worker-build@^0.8 && worker-build --release" [observability] enabled = true diff --git a/crates/tauri-utils/CHANGELOG.md b/crates/tauri-utils/CHANGELOG.md index 4ed8a4248dcb..435dc48706c7 100644 --- a/crates/tauri-utils/CHANGELOG.md +++ b/crates/tauri-utils/CHANGELOG.md @@ -1,5 +1,62 @@ # Changelog +## \[2.9.2] + +### Bug Fixes + +- [`b5b72ce51`](https://www.github.com/tauri-apps/tauri/commit/b5b72ce51811e9f95b1f7e9a05ea19c8f12ce694) ([#15383](https://www.github.com/tauri-apps/tauri/pull/15383) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Fix a regression in tauri-utils 2.8.3 that made empty path an invalid resource target, e.g. + + ```json + { + "bundle": { + "resources": { + "README.md": "", + } + } + } + ``` + + (this means `README.md` -> `$RESOURCE/README.md`, note this is a confusing behavior, and will be changed in v3) +- [`3fd8ba2c0`](https://www.github.com/tauri-apps/tauri/commit/3fd8ba2c022717068ff6a154ce12942c3a672232) ([#15388](https://www.github.com/tauri-apps/tauri/pull/15388) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Fix a regression in tauri-utils 2.8.3 that made an empty directory makes it skip all the following entries, e.g. + + ```json + { + "bundle": { + "resources": [ + "empty-directory", + "README.md" + ] + } + } + ``` + + if `empty-directory` is empty, the `README.md` will not be copied to the resource directory (skipped) + +## \[2.9.1] + +### Dependencies + +- [`4f548e739`](https://www.github.com/tauri-apps/tauri/commit/4f548e73947b3b06bf2073c822564aed3dd5f948) ([#15308](https://www.github.com/tauri-apps/tauri/pull/15308)) Updated `phf` to 0.13 + +## \[2.9.0] + +### New Features + +- [`001c8fe3d`](https://www.github.com/tauri-apps/tauri/commit/001c8fe3d288802de9a8c29cfd2f46f9220d97c5) ([#14722](https://www.github.com/tauri-apps/tauri/pull/14722)) Add a WebView option to control browser-level general autofill behavior. This option does not disable password or credit card autofill. On Windows (WebView2), setting it to true disables the general autofill "Suggestions" UI, which may appear even when `autocomplete="off"` is specified on input elements. On Linux, macOS, iOS, and Android, this option is currently unsupported and performs no operation. +- [`926a57bb0`](https://www.github.com/tauri-apps/tauri/commit/926a57bb0851e45d47ad1ee68fc96a9c25754c7c) ([#15201](https://www.github.com/tauri-apps/tauri/pull/15201)) Added uninstaller icon and uninstaller header image support for NSIS installer. + + Notes: + + - For `tauri-bundler` lib users, the `NsisSettings` now has 2 new fields `uninstaller_icon` and `uninstaller_header_image` which can be a breaking change + - When bundling with NSIS, users can add `uninstallerIcon` and `uninstallerHeaderImage` under `bundle > windows > nsis` to configure them. +- [`093e2b47c`](https://www.github.com/tauri-apps/tauri/commit/093e2b47c01361c18783e9ff18750388e41650c5) ([#14484](https://www.github.com/tauri-apps/tauri/pull/14484)) Support creating multiple windows on Android (activity embedding) and iOS (scenes). + +### Dependencies + +- [`e032c3b34`](https://www.github.com/tauri-apps/tauri/commit/e032c3b3421f53bca7b869ffee2be105c5c06ad9) ([#14959](https://www.github.com/tauri-apps/tauri/pull/14959)) Add new `html-manipulation-2` and `build-2` feature flags that use `dom_query` instead of `kuchikiki` for HTML parsing / manipulation. + This allows downstream users to remove `kuchikiki` and its dependencies from their dependency tree. +- [`1ef6a119b`](https://www.github.com/tauri-apps/tauri/commit/1ef6a119b1571d1da0acc08bdb7fd5521a4c6d52) ([#15115](https://www.github.com/tauri-apps/tauri/pull/15115)) Changed `toml` crate version from `0.9` to `">=0.9, <=1"` + ## \[2.8.3] ### Bug Fixes diff --git a/crates/tauri-utils/Cargo.toml b/crates/tauri-utils/Cargo.toml index ed6bc6c18d5e..7db190a0d625 100644 --- a/crates/tauri-utils/Cargo.toml +++ b/crates/tauri-utils/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-utils" -version = "2.8.3" +version = "2.9.2" description = "Utilities for Tauri" exclude = ["CHANGELOG.md", "/target"] readme = "README.md" @@ -17,13 +17,14 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" anyhow = "1" thiserror = "2" -phf = { version = "0.11", features = ["macros"] } +phf = { version = "0.13", features = ["macros"] } brotli = { version = "8", optional = true, default-features = false, features = [ "std", ] } url = { version = "2", features = ["serde"] } html5ever = { version = "0.29", optional = true } kuchiki = { package = "kuchikiki", version = "0.8.8-speedreader", optional = true } +dom_query = { version = "0.27", optional = true, default-features = false } proc-macro2 = { version = "1", optional = true } quote = { version = "1", optional = true } schemars = { version = "1", features = ["url2", "uuid1"], optional = true } @@ -31,9 +32,13 @@ serde_with = "3" aes-gcm = { version = "0.10", optional = true } getrandom = { version = "0.3", optional = true, features = ["std"] } serialize-to-javascript = { version = "0.1.2", optional = true } -ctor = "0.2" +ctor = { version = "0.8", default-features = false, features = [ + "std", + "proc_macro", +] } json5 = { version = "0.4", optional = true } -toml = { version = "0.9", features = ["parse"] } +# Part of public api in error type +toml = { version = ">=0.9, <=1", features = ["parse"] } json-patch = "3.0" # Our code requires at least 0.3.1 glob = "0.3.1" @@ -49,6 +54,7 @@ cargo_metadata = { version = "0.19", optional = true } serde-untagged = "0.1" uuid = { version = "1", features = ["serde"] } http = "1" +plist = "1" [target."cfg(target_os = \"macos\")".dependencies] swift-rs = { version = "1", optional = true, features = ["build"] } @@ -57,6 +63,7 @@ swift-rs = { version = "1", optional = true, features = ["build"] } getrandom = { version = "0.3", features = ["std"] } serial_test = "3" tauri = { path = "../tauri" } +tempfile = "3.15.0" [features] build = [ @@ -67,6 +74,15 @@ build = [ "swift-rs", "html-manipulation", ] +# Same as `build` but uses `html-manipulation-2` to avoid the `kuchikiki` dependency. +build-2 = [ + "proc-macro2", + "quote", + "cargo_metadata", + "schema", + "swift-rs", + "html-manipulation-2", +] compression = ["brotli"] schema = ["schemars"] isolation = ["aes-gcm", "getrandom", "serialize-to-javascript"] @@ -75,3 +91,4 @@ config-json5 = ["json5"] config-toml = [] resources = ["walkdir"] html-manipulation = ["dep:html5ever", "dep:kuchiki"] +html-manipulation-2 = ["dep:dom_query"] diff --git a/crates/tauri-utils/src/acl/build.rs b/crates/tauri-utils/src/acl/build.rs index 6667e9fa2c98..4555ddf62306 100644 --- a/crates/tauri-utils/src/acl/build.rs +++ b/crates/tauri-utils/src/acl/build.rs @@ -26,6 +26,9 @@ use super::{ /// Known name of the folder containing autogenerated permissions. pub const AUTOGENERATED_FOLDER_NAME: &str = "autogenerated"; +/// Known name of the file listing the commands that get permissions autogenerated on demand. +pub const AUTOGENERATED_COMMANDS_FILE_NAME: &str = "commands.toml"; + /// Cargo cfg key for permissions file paths pub const PERMISSION_FILES_PATH_KEY: &str = "PERMISSION_FILES_PATH"; @@ -251,7 +254,14 @@ pub struct AutogeneratedPermissions { pub denied: Vec, } -/// Autogenerate permission files for a list of commands. +/// Autogenerate a permission file listing the given commands. +/// +/// Instead of writing two explicit permissions (`allow-$command` and `deny-$command`) per command, +/// a single [`AUTOGENERATED_COMMANDS_FILE_NAME`] file is written listing the command names. The +/// `allow-`/`deny-` permissions are then [materialized on demand](super::manifest::Manifest::command_permission) +/// by the ACL resolver, which drastically reduces the size of the manifest for plugins with many commands. +/// +/// Returns the `allow-`/`deny-` permission identifiers for the commands (e.g. for a default permission set). pub fn autogenerate_command_permissions( path: &Path, commands: &[&str], @@ -262,6 +272,19 @@ pub fn autogenerate_command_permissions( fs::create_dir_all(path).expect("unable to create autogenerated commands dir"); } + // remove stale per-command permission files written by older Tauri versions + // (one file with two permissions per command); the commands are now listed in a single file + // and resolved on demand. + for entry in fs::read_dir(path).into_iter().flatten().flatten() { + let entry_path = entry.path(); + let is_command_file = + entry_path.file_name() == Some(std::ffi::OsStr::new(AUTOGENERATED_COMMANDS_FILE_NAME)); + let is_toml = entry_path.extension().and_then(|e| e.to_str()) == Some("toml"); + if is_toml && !is_command_file { + let _ = fs::remove_file(entry_path); + } + } + let schema_entry = if schema_ref { let cwd = env::current_dir().unwrap(); let components_len = path.strip_prefix(&cwd).unwrap_or(path).components().count(); @@ -281,40 +304,28 @@ pub fn autogenerate_command_permissions( "".to_string() }; - let mut autogenerated = AutogeneratedPermissions { - allowed: Vec::new(), - denied: Vec::new(), + let autogenerated = AutogeneratedPermissions { + allowed: commands + .iter() + .map(|command| format!("allow-{}", command.replace('_', "-"))) + .collect(), + denied: commands + .iter() + .map(|command| format!("deny-{}", command.replace('_', "-"))) + .collect(), }; - for command in commands { - let slugified_command = command.replace('_', "-"); - - let toml = format!( - r###"{license_header}# Automatically generated - DO NOT EDIT! + let commands_list = serde_json::to_string(commands).expect("failed to serialize command list"); + let toml = format!( + r###"{license_header}# Automatically generated - DO NOT EDIT! {schema_entry} -[[permission]] -identifier = "allow-{slugified_command}" -description = "Enables the {command} command without any pre-configured scope." -commands.allow = ["{command}"] - -[[permission]] -identifier = "deny-{slugified_command}" -description = "Denies the {command} command without any pre-configured scope." -commands.deny = ["{command}"] +commands = {commands_list} "###, - ); - - let out_path = path.join(format!("{command}.toml")); - write_if_changed(&out_path, toml) - .unwrap_or_else(|_| panic!("unable to autogenerate {out_path:?}")); + ); - autogenerated - .allowed - .push(format!("allow-{slugified_command}")); - autogenerated - .denied - .push(format!("deny-{slugified_command}")); - } + let out_path = path.join(AUTOGENERATED_COMMANDS_FILE_NAME); + write_if_changed(&out_path, toml) + .unwrap_or_else(|_| panic!("unable to autogenerate {out_path:?}")); autogenerated } @@ -372,6 +383,30 @@ pub fn generate_docs( )); permission_table.push('\n'); } + + // docs for the `allow-$command`/`deny-$command` permissions generated on demand + // (sorted so the generated reference stays stable regardless of the command declaration order) + let mut commands = permission.commands.clone(); + commands.sort(); + for command in &commands { + let slug = command.replace('_', "-"); + permission_table.push_str(&docs_from( + &format!("allow-{slug}"), + Some(&format!( + "Enables the {command} command without any pre-configured scope." + )), + plugin_identifier, + )); + permission_table.push('\n'); + permission_table.push_str(&docs_from( + &format!("deny-{slug}"), + Some(&format!( + "Denies the {command} command without any pre-configured scope." + )), + plugin_identifier, + )); + permission_table.push('\n'); + } } let docs = format!("{default_permission}{PERMISSION_TABLE_HEADER}\n{permission_table}\n"); @@ -463,8 +498,8 @@ pub fn generate_allowed_commands( let capabilities = crate::acl::get_capabilities(&config, capabilities_from_files, None)?; let permission_entries = capabilities - .into_iter() - .flat_map(|(_, capabilities)| capabilities.permissions); + .into_values() + .flat_map(|capabilities| capabilities.permissions); let mut allowed_commands = AllowedCommands { has_app_acl: has_app_manifest(&acl), ..Default::default() diff --git a/crates/tauri-utils/src/acl/capability.rs b/crates/tauri-utils/src/acl/capability.rs index 5fa6435e19de..e870678fe697 100644 --- a/crates/tauri-utils/src/acl/capability.rs +++ b/crates/tauri-utils/src/acl/capability.rs @@ -317,7 +317,7 @@ impl FromStr for CapabilityFile { } } -#[cfg(feature = "build")] +#[cfg(any(feature = "build", feature = "build-2"))] mod build { use std::convert::identity; diff --git a/crates/tauri-utils/src/acl/identifier.rs b/crates/tauri-utils/src/acl/identifier.rs index 1ceea7166b9b..31e496a3e55b 100644 --- a/crates/tauri-utils/src/acl/identifier.rs +++ b/crates/tauri-utils/src/acl/identifier.rs @@ -17,7 +17,10 @@ const MAX_LEN_PREFIX: usize = 64 - PLUGIN_PREFIX.len(); const MAX_LEN_BASE: usize = 64; const MAX_LEN_IDENTIFIER: usize = MAX_LEN_PREFIX + 1 + MAX_LEN_BASE; -/// Plugin identifier. +/// Permission identifier. +/// +/// Typically used in the [`permissions`](crate::acl::Capability::permissions) field of a capability file. +/// (e.g. `core:default`, `sample:allow-ping-scoped`) #[derive(Debug, Clone, PartialEq, Eq)] pub struct Identifier { inner: String, @@ -93,7 +96,8 @@ impl ValidByte { } fn alpha_numeric_hyphen(byte: u8) -> Option { - (byte.is_ascii_alphanumeric() || byte == b'-').then_some(Self::Byte(byte)) + // `*` is allowed so the implicit `allow-*`/`deny-*` wildcard command permissions can be referenced + (byte.is_ascii_alphanumeric() || byte == b'-' || byte == b'*').then_some(Self::Byte(byte)) } fn next(&self, next: u8) -> Option { @@ -214,7 +218,12 @@ impl<'de> Deserialize<'de> for Identifier { where D: Deserializer<'de>, { - Self::try_from(String::deserialize(deserializer)?).map_err(serde::de::Error::custom) + let raw = String::deserialize(deserializer)?; + Self::try_from(raw.clone()).map_err(|e| { + serde::de::Error::custom(format!( + "invalid plugin or permission identifier '{raw}': {e}" + )) + }) } } @@ -259,6 +268,11 @@ mod tests { assert!(ident("prefix:base--sep").is_ok()); assert!(ident("pre--fix:base").is_ok()); + // wildcard command permissions + assert!(ident("allow-*").is_ok()); + assert!(ident("deny-*").is_ok()); + assert!(ident("prefix:allow-*").is_ok()); + assert!(ident("prefix::base").is_err()); assert!(ident(":base").is_err()); assert!(ident("prefix:").is_err()); @@ -284,7 +298,7 @@ mod tests { } } -#[cfg(feature = "build")] +#[cfg(any(feature = "build", feature = "build-2"))] mod build { use proc_macro2::TokenStream; use quote::{ToTokens, TokenStreamExt, quote}; diff --git a/crates/tauri-utils/src/acl/manifest.rs b/crates/tauri-utils/src/acl/manifest.rs index 64624edafcf2..4192c35901f9 100644 --- a/crates/tauri-utils/src/acl/manifest.rs +++ b/crates/tauri-utils/src/acl/manifest.rs @@ -6,7 +6,7 @@ use std::{collections::BTreeMap, num::NonZeroU64}; -use super::{Permission, PermissionSet}; +use super::{Commands, Permission, PermissionSet}; use serde::{Deserialize, Serialize}; /// The default permission set of the plugin. @@ -41,6 +41,13 @@ pub struct PermissionFile { /// A list of inlined permissions #[serde(default)] pub permission: Vec, + + /// A list of command names that get `allow-$command` and `deny-$command` permissions + /// autogenerated on demand instead of being stored as explicit permissions. + /// + /// See [`Manifest::command_permission`] and the ACL resolver for how these are expanded. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub commands: Vec, } /// Plugin manifest. @@ -52,6 +59,13 @@ pub struct Manifest { pub permissions: BTreeMap, /// Plugin permission sets. pub permission_sets: BTreeMap, + /// Commands that have `allow-$command` and `deny-$command` permissions autogenerated on demand. + /// + /// Storing the command names instead of two explicit permissions per command drastically reduces + /// the size of the manifest (and the resolved ACL embedded in the app) when a plugin exposes a + /// large number of commands. The implicit permissions are materialized by [`Self::command_permission`]. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub commands: Vec, /// The global scope schema. pub global_scope_schema: Option, } @@ -66,6 +80,7 @@ impl Manifest { default_permission: None, permissions: BTreeMap::new(), permission_sets: BTreeMap::new(), + commands: Vec::new(), global_scope_schema, }; @@ -89,10 +104,63 @@ impl Manifest { let key = set.identifier.clone(); manifest.permission_sets.insert(key, set); } + + manifest.commands.extend(permission_file.commands); } + // keep the list deterministic (it ends up embedded in the app) and free of duplicates + manifest.commands.sort(); + manifest.commands.dedup(); + manifest } + + /// Materializes the `allow-$command`/`deny-$command` (or `allow-*`/`deny-*`) [`Permission`] for the + /// manifest's [`commands`](Self::commands), if `identifier` refers to one of them. + /// + /// Permission identifiers are slugified (snake_case `_` becomes `-`) while command names keep their + /// original form, so `allow-do-something` resolves the `do_something` command. + /// + /// The `allow-*`/`deny-*` wildcards allow/deny **all** of the manifest's commands while resolving to a + /// single command (`*`) instead of one per command, which keeps the resolved ACL small even when a + /// manifest exposes many commands. The wildcards are only available for the application manifest, so + /// `allow_wildcard` must be set accordingly by the caller (it knows the manifest's ACL key). + pub fn command_permission(&self, identifier: &str, allow_wildcard: bool) -> Option { + let (deny, command_slug) = if let Some(slug) = identifier.strip_prefix("allow-") { + (false, slug) + } else if let Some(slug) = identifier.strip_prefix("deny-") { + (true, slug) + } else { + return None; + }; + + let command = if command_slug == "*" { + // wildcard: app manifest only, and only when it actually exposes commands + if !allow_wildcard || self.commands.is_empty() { + return None; + } + "*".to_string() + } else { + self + .commands + .iter() + .find(|command| command.replace('_', "-") == command_slug)? + .clone() + }; + + let mut commands = Commands::default(); + if deny { + commands.deny.push(command); + } else { + commands.allow.push(command); + } + + Some(Permission { + identifier: identifier.to_string(), + commands, + ..Default::default() + }) + } } #[cfg(feature = "schema")] @@ -132,7 +200,7 @@ impl Manifest { } } -#[cfg(feature = "build")] +#[cfg(any(feature = "build", feature = "build-2"))] mod build { use proc_macro2::TokenStream; use quote::{ToTokens, TokenStreamExt, quote}; @@ -178,6 +246,8 @@ mod build { identity, ); + let commands = vec_lit(&self.commands, str_lit); + // Only used in build script and macros, so don't include them in runtime // let global_scope_schema = // opt_lit_owned(self.global_scope_schema.as_ref().map(json_value_lit)); @@ -189,6 +259,7 @@ mod build { default_permission, permissions, permission_sets, + commands, global_scope_schema ) } diff --git a/crates/tauri-utils/src/acl/mod.rs b/crates/tauri-utils/src/acl/mod.rs index 3b48bb6a73a9..509211e6cca1 100644 --- a/crates/tauri-utils/src/acl/mod.rs +++ b/crates/tauri-utils/src/acl/mod.rs @@ -58,7 +58,7 @@ pub const ALLOWED_COMMANDS_FILE_NAME: &str = "allowed-commands.json"; /// the value is set to the config's directory pub const REMOVE_UNUSED_COMMANDS_ENV_VAR: &str = "REMOVE_UNUSED_COMMANDS"; -#[cfg(feature = "build")] +#[cfg(any(feature = "build", feature = "build-2"))] pub mod build; pub mod capability; pub mod identifier; @@ -108,7 +108,7 @@ pub enum Error { CreateDir(std::io::Error, PathBuf), /// [`cargo_metadata`] was not able to complete successfully - #[cfg(feature = "build")] + #[cfg(any(feature = "build", feature = "build-2"))] #[error("failed to execute: {0}")] Metadata(#[from] ::cargo_metadata::Error), @@ -464,7 +464,7 @@ mod tests { } } -#[cfg(feature = "build")] +#[cfg(any(feature = "build", feature = "build-2"))] mod build_ { use std::convert::identity; diff --git a/crates/tauri-utils/src/acl/resolved.rs b/crates/tauri-utils/src/acl/resolved.rs index b30fb6677550..c9f343151ad9 100644 --- a/crates/tauri-utils/src/acl/resolved.rs +++ b/crates/tauri-utils/src/acl/resolved.rs @@ -4,7 +4,7 @@ //! Resolved ACL for runtime usage. -use std::{collections::BTreeMap, fmt}; +use std::{borrow::Cow, collections::BTreeMap, fmt}; use crate::platform::Target; @@ -332,8 +332,11 @@ pub struct TraversedPermission<'a> { pub key: String, /// Permission's name pub permission_name: String, - /// Permission details - pub permission: &'a Permission, + /// Permission details. + /// + /// This is borrowed for permissions stored in the [`Manifest`], or owned for the `allow-$command` + /// and `deny-$command` permissions [materialized on demand](Manifest::command_permission). + pub permission: Cow<'a, Permission>, } /// Expand a permissions id based on the ACL to get the associated permissions (e.g. expand some-plugin:default) @@ -361,7 +364,14 @@ pub fn get_permissions<'a>( Ok(vec![TraversedPermission { key: key.to_string(), permission_name: permission_name.to_string(), - permission, + permission: Cow::Borrowed(permission), + }]) + } else if let Some(permission) = manifest.command_permission(permission_name, key == APP_ACL_KEY) + { + Ok(vec![TraversedPermission { + key: key.to_string(), + permission_name: permission_name.to_string(), + permission: Cow::Owned(permission), }]) } else { Err(Error::UnknownPermission { @@ -409,7 +419,7 @@ fn get_permission_set_permissions<'a>( permissions.push(TraversedPermission { key: key.to_string(), permission_name: permission_name.to_string(), - permission, + permission: Cow::Borrowed(permission), }); } else if let Some(permission_set) = manifest.permission_sets.get(permission_name) { permissions.extend(get_permission_set_permissions( @@ -418,6 +428,14 @@ fn get_permission_set_permissions<'a>( manifest, permission_set, )?); + } else if let Some(permission) = + manifest.command_permission(permission_name, key == APP_ACL_KEY) + { + permissions.push(TraversedPermission { + key: key.to_string(), + permission_name: permission_name.to_string(), + permission: Cow::Owned(permission), + }); } else { return Err(Error::SetPermissionNotFound { permission: permission_name.to_string(), @@ -438,7 +456,7 @@ fn display_perm_key(prefix: &str) -> &str { } } -#[cfg(feature = "build")] +#[cfg(any(feature = "build", feature = "build-2"))] mod build { use proc_macro2::TokenStream; use quote::{ToTokens, TokenStreamExt, quote}; @@ -563,7 +581,10 @@ mod build { #[cfg(test)] mod tests { - use super::{Identifier, Manifest, Permission, PermissionSet, get_permissions}; + use super::{ + APP_ACL_KEY, Identifier, Manifest, Permission, PermissionSet, Resolved, get_permissions, + }; + use crate::platform::Target; fn manifest( name: &str, @@ -680,4 +701,102 @@ mod tests { assert_eq!(permissions[5].key, "http"); assert_eq!(permissions[5].permission_name, "fetch-cancel"); } + + fn manifest_with_commands(name: &str, commands: &[&str]) -> (String, Manifest) { + ( + name.to_string(), + Manifest { + commands: commands.iter().map(|c| c.to_string()).collect(), + ..Default::default() + }, + ) + } + + #[test] + fn resolves_implicit_command_permissions() { + let acl = [manifest_with_commands("fs", &["read_file", "write_file"])].into(); + + // `allow-$command` resolves to the command with the original (snake_case) name + let permissions = get_permissions(&id("fs:allow-read-file"), &acl).unwrap(); + assert_eq!(permissions.len(), 1); + assert_eq!(permissions[0].key, "fs"); + assert_eq!(permissions[0].permission.commands.allow, ["read_file"]); + assert!(permissions[0].permission.commands.deny.is_empty()); + + // `deny-$command` resolves to the deny side + let permissions = get_permissions(&id("fs:deny-write-file"), &acl).unwrap(); + assert_eq!(permissions.len(), 1); + assert_eq!(permissions[0].permission.commands.deny, ["write_file"]); + + // unknown command is still an error + assert!(get_permissions(&id("fs:allow-unknown"), &acl).is_err()); + } + + #[test] + fn resolves_wildcard_command_permission() { + let acl = [( + APP_ACL_KEY.to_string(), + manifest_with_commands("__app__", &["read_file", "write_file"]).1, + )] + .into(); + + // the wildcard resolves to a single `*` command instead of one entry per command + let permissions = get_permissions(&id("allow-*"), &acl).unwrap(); + assert_eq!(permissions.len(), 1); + assert_eq!(permissions[0].permission.commands.allow, ["*"]); + + let permissions = get_permissions(&id("deny-*"), &acl).unwrap(); + assert_eq!(permissions.len(), 1); + assert_eq!(permissions[0].permission.commands.deny, ["*"]); + + // the wildcard is only available for the app manifest, not for plugins + let plugin_acl = [manifest_with_commands("fs", &["read_file", "write_file"])].into(); + assert!(get_permissions(&id("fs:allow-*"), &plugin_acl).is_err()); + // but specific command permissions still work for plugins + assert!(get_permissions(&id("fs:allow-read-file"), &plugin_acl).is_ok()); + + // the wildcard is not a valid permission when the app manifest has no commands + let empty = [(APP_ACL_KEY.to_string(), manifest("__app__", [], None, []).1)].into(); + assert!(get_permissions(&id("allow-*"), &empty).is_err()); + } + + #[test] + fn resolve_wildcard_uses_single_entry() { + use crate::acl::{Capability, capability::PermissionEntry}; + + let acl = [( + APP_ACL_KEY.to_string(), + Manifest { + commands: ["echo", "ping", "spam"] + .iter() + .map(|c| c.to_string()) + .collect(), + ..Default::default() + }, + )] + .into(); + + let capability = Capability { + identifier: "main".to_string(), + description: String::new(), + remote: None, + local: true, + windows: vec!["main".to_string()], + webviews: Vec::new(), + permissions: vec![PermissionEntry::PermissionRef(id("allow-*"))], + platforms: None, + }; + + let resolved = Resolved::resolve( + &acl, + [("main".to_string(), capability)].into(), + Target::Linux, + ) + .unwrap(); + + // the whole app is allowed through a single resolved entry keyed by `*` + assert_eq!(resolved.allowed_commands.len(), 1); + assert!(resolved.allowed_commands.contains_key("*")); + assert!(resolved.denied_commands.is_empty()); + } } diff --git a/crates/tauri-utils/src/acl/schema.rs b/crates/tauri-utils/src/acl/schema.rs index 3aeec721e08a..f68619980671 100644 --- a/crates/tauri-utils/src/acl/schema.rs +++ b/crates/tauri-utils/src/acl/schema.rs @@ -51,6 +51,11 @@ pub trait PermissionSchemaGenerator< /// Permissions to generate schema for. fn permissions(&'a self) -> P; + /// Commands that have `allow-$command`/`deny-$command` permissions generated on demand. + fn commands(&self) -> &[String] { + &[] + } + /// A utility function to generate a schema for a permission identifier fn perm_id_schema(name: Option<&str>, id: &str, description: Option<&str>) -> Schema { let command_name = match name { @@ -110,6 +115,40 @@ pub trait PermissionSchemaGenerator< permission_schemas.push(schema); } + // schema for the `allow-$command`/`deny-$command` permissions generated on demand + let commands = self.commands(); + for command in commands { + let slug = command.replace('_', "-"); + permission_schemas.push(Self::perm_id_schema( + name, + &format!("allow-{slug}"), + Some(&format!( + "Enables the {command} command without any pre-configured scope." + )), + )); + permission_schemas.push(Self::perm_id_schema( + name, + &format!("deny-{slug}"), + Some(&format!( + "Denies the {command} command without any pre-configured scope." + )), + )); + } + + // schema for the `allow-*`/`deny-*` wildcard permissions (app manifest only) + if !commands.is_empty() && name == Some(super::APP_ACL_KEY) { + permission_schemas.push(Self::perm_id_schema( + name, + "allow-*", + Some("Enables all commands without any pre-configured scope."), + )); + permission_schemas.push(Self::perm_id_schema( + name, + "deny-*", + Some("Denies all commands without any pre-configured scope."), + )); + } + permission_schemas } } @@ -164,6 +203,10 @@ impl<'a> fn permissions(&'a self) -> Values<'a, std::string::String, Permission> { self.permissions.values() } + + fn commands(&self) -> &[String] { + &self.commands + } } impl<'a> PermissionSchemaGenerator<'a, Iter<'a, PermissionSet>, Iter<'a, Permission>> @@ -188,6 +231,10 @@ impl<'a> PermissionSchemaGenerator<'a, Iter<'a, PermissionSet>, Iter<'a, Permiss fn permissions(&'a self) -> Iter<'a, Permission> { self.permission.iter() } + + fn commands(&self) -> &[String] { + &self.commands + } } /// Collect and include all possible identifiers in `Identifier` definition in the schema diff --git a/crates/tauri-utils/src/acl/value.rs b/crates/tauri-utils/src/acl/value.rs index f0f91a891701..439bbdf9cbff 100644 --- a/crates/tauri-utils/src/acl/value.rs +++ b/crates/tauri-utils/src/acl/value.rs @@ -86,12 +86,12 @@ impl From for Value { match value { serde_json::Value::Null => Value::Null, serde_json::Value::Bool(b) => Value::Bool(b), - serde_json::Value::Number(n) => Value::Number(if let Some(f) = n.as_f64() { + serde_json::Value::Number(n) => Value::Number(if let Some(i) = n.as_i64() { + Number::Int(i) + } else if let Some(u) = n.as_u64() { + Number::Int(u as i64) + } else if let Some(f) = n.as_f64() { Number::Float(f) - } else if let Some(n) = n.as_u64() { - Number::Int(n as i64) - } else if let Some(n) = n.as_i64() { - Number::Int(n) } else { Number::Int(0) }), @@ -145,7 +145,7 @@ impl From for Value { } } -#[cfg(feature = "build")] +#[cfg(any(feature = "build", feature = "build-2"))] mod build { use std::convert::identity; diff --git a/crates/tauri-utils/src/assets.rs b/crates/tauri-utils/src/assets.rs index 3d904cd1f39f..486662d8dce8 100644 --- a/crates/tauri-utils/src/assets.rs +++ b/crates/tauri-utils/src/assets.rs @@ -168,27 +168,18 @@ impl EmbeddedAssets { /// Get an asset by key. #[cfg(feature = "compression")] pub fn get(&self, key: &AssetKey) -> Option> { - self - .assets - .get(key.as_ref()) - .map(|&(mut asdf)| { - // with the exception of extremely small files, output should usually be - // at least as large as the compressed version. - let mut buf = Vec::with_capacity(asdf.len()); - brotli::BrotliDecompress(&mut asdf, &mut buf).map(|()| buf) - }) - .and_then(Result::ok) - .map(Cow::Owned) + let &(mut asdf) = self.assets.get(key.as_ref())?; + // with the exception of extremely small files, output should usually be + // at least as large as the compressed version. + let mut buf = Vec::with_capacity(asdf.len()); + brotli::BrotliDecompress(&mut asdf, &mut buf).ok()?; + Some(Cow::Owned(buf)) } /// Get an asset by key. #[cfg(not(feature = "compression"))] pub fn get(&self, key: &AssetKey) -> Option> { - self - .assets - .get(key.as_ref()) - .copied() - .map(|a| Cow::Owned(a.to_vec())) + Some(Cow::Borrowed(self.assets.get(key.as_ref())?)) } /// Iterate on the assets. diff --git a/crates/tauri-utils/src/build.rs b/crates/tauri-utils/src/build.rs index 57456b45278f..de575ab4a47e 100644 --- a/crates/tauri-utils/src/build.rs +++ b/crates/tauri-utils/src/build.rs @@ -98,3 +98,74 @@ fn link_xcode_library(name: &str, source: impl AsRef) { println!("cargo:rustc-link-search=native={}", lib_out_dir.display()); println!("cargo:rustc-link-lib=static={name}"); } + +/// Updates the Android manifest by inserting XML content into a specified parent tag. +/// +/// The content is wrapped in auto-generated comments and will replace any existing +/// content with the same block identifier. +/// +/// # Arguments +/// +/// * `block_identifier` - A unique identifier for the block (used in comments) +/// * `parent` - The parent XML tag name (e.g., "activity", "application") +/// * `insert` - The XML content to insert +pub fn update_android_manifest( + block_identifier: &str, + parent: &str, + insert: String, +) -> anyhow::Result<()> { + use std::{ + env::var_os, + fs::{read_to_string, write}, + path::PathBuf, + }; + + if let Some(project_path) = var_os("TAURI_ANDROID_PROJECT_PATH").map(PathBuf::from) { + let manifest_path = project_path.join("app/src/main/AndroidManifest.xml"); + if !manifest_path.exists() { + return Ok(()); + } + let manifest = read_to_string(&manifest_path)?; + let rewritten = insert_into_xml(&manifest, block_identifier, parent, &insert); + if rewritten != manifest { + write(&manifest_path, rewritten)?; + } + } + Ok(()) +} + +fn xml_block_comment(id: &str) -> String { + format!("") +} + +fn insert_into_xml(xml: &str, block_identifier: &str, parent_tag: &str, contents: &str) -> String { + let block_comment = xml_block_comment(block_identifier); + + let mut rewritten = Vec::new(); + let mut found_block = false; + let parent_closing_tag = format!(""); + for line in xml.split('\n') { + if line.contains(&block_comment) { + found_block = !found_block; + continue; + } + + // found previous block which should be removed + if found_block { + continue; + } + + if let Some(index) = line.find(&parent_closing_tag) { + let indentation = " ".repeat(index + 4); + rewritten.push(format!("{indentation}{block_comment}")); + for l in contents.split('\n') { + rewritten.push(format!("{indentation}{l}")); + } + rewritten.push(format!("{indentation}{block_comment}")); + } + + rewritten.push(line.to_string()); + } + + rewritten.join("\n") +} diff --git a/crates/tauri-utils/src/config.rs b/crates/tauri-utils/src/config.rs index c5c8f8a53085..39814370ad2e 100644 --- a/crates/tauri-utils/src/config.rs +++ b/crates/tauri-utils/src/config.rs @@ -36,7 +36,7 @@ use serde_with::skip_serializing_none; use url::Url; use std::{ - collections::HashMap, + collections::{HashMap, HashSet}, fmt::{self, Display}, fs::read_to_string, path::PathBuf, @@ -810,13 +810,24 @@ pub struct NsisConfig { /// The recommended dimensions are 164px x 314px. #[serde(alias = "sidebar-image")] pub sidebar_image: Option, + // TODO: Change the alias to installer-icon in v3 /// The path to an icon file used as the installer icon. #[serde(alias = "install-icon")] pub installer_icon: Option, + /// The path to an icon file used as the uninstaller icon. + #[serde(alias = "uninstaller-icon")] + pub uninstaller_icon: Option, + /// The path to a bitmap file to display on the header of uninstallers pages. + /// Defaults to [`Self::header_image`]. If this is set but [`Self::header_image`] is not, a default image from NSIS will be applied to `header_image` + /// + /// The recommended dimensions are 150px x 57px. + #[serde(alias = "uninstaller-header-image")] + pub uninstaller_header_image: Option, /// Whether the installation will be for all users or just the current user. #[serde(default, alias = "install-mode")] pub install_mode: NSISInstallerMode, - /// A list of installer languages. + /// A list of installer languages. Default to `["English"]` if not set. + /// /// By default the OS language is used. If the OS language is not in the list of languages, the first language will be used. /// To allow the user to select the language, set `display_language_selector` to `true`. /// @@ -827,7 +838,7 @@ pub struct NsisConfig { /// /// See for an example `.nsh` file. /// - /// **Note**: the key must be a valid NSIS language and it must be added to [`NsisConfig`] languages array, + /// **Note**: the key must be a valid NSIS language and it must be added to the [`Self::languages`] array, pub custom_language_files: Option>, /// Whether to display a language selector dialog before the installer and uninstaller windows are rendered or not. /// By default the OS language is selected, with a fallback to the first language in the `languages` array. @@ -879,9 +890,15 @@ pub struct NsisConfig { /// ``` #[serde(alias = "installer-hooks")] pub installer_hooks: Option, + /// Deprecated: use [`WindowsConfig::minimum_webview2_version`] (`bundle > windows > minimumWebview2Version`) instead. + /// /// Try to ensure that the WebView2 version is equal to or newer than this version, /// if the user's WebView2 is older than this version, /// the installer will try to trigger a WebView2 update. + #[deprecated( + since = "2.10.0", + note = "Use `WindowsConfig::minimum_webview2_version` instead." + )] #[serde(alias = "minimum-webview2-version")] pub minimum_webview2_version: Option, } @@ -996,6 +1013,11 @@ pub struct WindowsConfig { /// The default value of this flag is `true`. #[serde(default = "default_true", alias = "allow-downgrades")] pub allow_downgrades: bool, + /// Try to ensure that the WebView2 version is equal to or newer than this version, + /// if the user's WebView2 is older than this version, + /// the installer will try to trigger a WebView2 update. + #[serde(alias = "minimum-webview2-version")] + pub minimum_webview2_version: Option, /// Configuration for the MSI generated with WiX. pub wix: Option, /// Configuration for the installer generated with NSIS. @@ -1009,6 +1031,19 @@ pub struct WindowsConfig { /// need to use another tool like `osslsigncode`. #[serde(alias = "sign-command")] pub sign_command: Option, + /// Whether to bundle the Visual C++ runtime DLLs alongside the application. + /// + /// This can be particularly useful when your application includes sidecars or DLLs that do + /// not statically link the Visual C++ runtime and require the runtime DLLs at runtime, and + /// you do not want to require users to install the Visual C++ Redistributable. This can also + /// be useful when `build > windows > staticVCRuntime` is set to `false`. + #[serde( + default, + rename = "bundleVCRuntime", + alias = "bundle-vc-runtime", + alias = "bundleVcRuntime" + )] + pub bundle_vc_runtime: bool, } impl Default for WindowsConfig { @@ -1020,9 +1055,11 @@ impl Default for WindowsConfig { tsp: false, webview_install_mode: Default::default(), allow_downgrades: true, + minimum_webview2_version: None, wix: None, nsis: None, sign_command: None, + bundle_vc_runtime: false, } } } @@ -1128,7 +1165,13 @@ pub struct FileAssociation { /// The app's role with respect to the type. Maps to `CFBundleTypeRole` on macOS. #[serde(default)] pub role: BundleTypeRole, - /// The mime-type e.g. 'image/png' or 'text/plain'. Linux-only. + /// The mime-type of the association, e.g. `'image/png'` or `'text/plain'`. + /// + /// - **Linux**: written as `MimeType=` in the `.desktop` file. + /// - **macOS / iOS**: added as `public.mime-type` in the `UTTypeTagSpecification` dictionary of + /// the `UTExportedTypeDeclarations` entry in `Info.plist`. + /// - **Android**: used as `android:mimeType` in the `` element of an `` + /// in `AndroidManifest.xml`. #[serde(alias = "mime-type")] pub mime_type: Option, /// The ranking of this app among apps that declare themselves as editors or viewers of the given file type. Maps to `LSHandlerRank` on macOS. @@ -1138,6 +1181,31 @@ pub struct FileAssociation { /// /// You should define this if the associated file is a custom file type defined by your application. pub exported_type: Option, + /// Intent action filters for this file association. + /// + /// By default all filters are used. + #[serde(alias = "android-intent-action-filters")] + pub android_intent_action_filters: Option>, +} + +/// Android intent action. +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize, Hash)] +#[cfg_attr(feature = "schema", derive(JsonSchema))] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub enum AndroidIntentAction { + /// ACTION_SEND. + /// + /// + Send, + /// ACTION_SEND_MULTIPLE. + /// + /// + SendMultiple, + /// ACTION_VIEW. + /// + /// + View, } /// The exported type definition. Maps to a `UTExportedTypeDeclarations` entry on macOS. @@ -1154,6 +1222,227 @@ pub struct ExportedFileAssociation { pub conforms_to: Option>, } +impl FileAssociation { + /// Infers UTIs (Uniform Type Identifiers) from file extensions and mime types. + /// This is useful for macOS and iOS to automatically populate `LSItemContentTypes` + /// in the Info.plist for share sheet and file association support. + /// + /// Returns a vector of UTIs that should be included in `LSItemContentTypes`. + /// Explicitly provided content types are included first, followed by inferred types. + pub fn infer_content_types(&self) -> HashSet { + let mut content_types = HashSet::new(); + + // when we have an exported type, we only reference it + if let Some(exported_type) = &self.exported_type { + content_types.insert(exported_type.identifier.clone()); + return content_types; + } + + // Start with explicitly provided content types + if let Some(explicit_types) = &self.content_types { + content_types.extend(explicit_types.iter().cloned()); + } + + // Infer from extensions and add to content_types (avoiding duplicates) + for ext in &self.ext { + if let Some(uti) = extension_to_uti(&ext.0) { + content_types.insert(uti.to_string()); + } + } + + // Also infer from mime type if available (avoiding duplicates) + if let Some(mime_type) = &self.mime_type + && let Some(uti) = mime_type_to_uti(mime_type) + { + content_types.insert(uti.to_string()); + } + + content_types + } +} + +/// Generates plist dictionary entries for file associations. +/// This is used by both macOS and iOS bundlers to populate Info.plist. +/// +/// Returns a plist dictionary containing `UTExportedTypeDeclarations` and `CFBundleDocumentTypes` +/// if there are any file associations configured. +pub fn file_associations_plist(associations: &[FileAssociation]) -> Option { + use plist::{Dictionary, Value}; + + if associations.is_empty() { + return None; + } + + let exported_associations = associations + .iter() + .filter_map(|association| { + association.exported_type.as_ref().map(|exported_type| { + let mut dict = Dictionary::new(); + + dict.insert( + "UTTypeIdentifier".into(), + exported_type.identifier.clone().into(), + ); + if let Some(description) = &association.description { + dict.insert("UTTypeDescription".into(), description.clone().into()); + } + if let Some(conforms_to) = &exported_type.conforms_to { + dict.insert( + "UTTypeConformsTo".into(), + Value::Array(conforms_to.iter().map(|s| s.clone().into()).collect()), + ); + } + + let mut specification = Dictionary::new(); + specification.insert( + "public.filename-extension".into(), + Value::Array( + association + .ext + .iter() + .map(|s| s.to_string().into()) + .collect(), + ), + ); + if let Some(mime_type) = &association.mime_type { + specification.insert("public.mime-type".into(), mime_type.clone().into()); + } + + dict.insert("UTTypeTagSpecification".into(), specification.into()); + + Value::Dictionary(dict) + }) + }) + .collect::>(); + + let document_types = associations + .iter() + .map(|association| { + let mut dict = Dictionary::new(); + + if !association.ext.is_empty() { + dict.insert( + "CFBundleTypeExtensions".into(), + Value::Array( + association + .ext + .iter() + .map(|ext| ext.to_string().into()) + .collect(), + ), + ); + } + + // For macOS/iOS share sheet, we need LSItemContentTypes with standard UTIs + let content_types = association.infer_content_types(); + + // Add LSItemContentTypes if we have any content types + if !content_types.is_empty() { + dict.insert( + "LSItemContentTypes".into(), + Value::Array(content_types.iter().map(|s| s.clone().into()).collect()), + ); + } + + let type_name = association + .name + .clone() + .or_else(|| association.ext.first().map(|ext| ext.0.clone())) + .unwrap_or_default(); + dict.insert("CFBundleTypeName".into(), type_name.into()); + dict.insert( + "CFBundleTypeRole".into(), + association.role.to_string().into(), + ); + dict.insert("LSHandlerRank".into(), association.rank.to_string().into()); + + Value::Dictionary(dict) + }) + .collect::>(); + + if exported_associations.is_empty() && document_types.is_empty() { + return None; + } + + let mut plist = Dictionary::new(); + if !exported_associations.is_empty() { + plist.insert( + "UTExportedTypeDeclarations".into(), + Value::Array(exported_associations), + ); + } + if !document_types.is_empty() { + plist.insert("CFBundleDocumentTypes".into(), Value::Array(document_types)); + } + + Some(Value::Dictionary(plist)) +} + +/// Maps file extensions to their standard UTIs for macOS/iOS share sheet support +fn extension_to_uti(ext: &str) -> Option<&'static str> { + match ext.to_lowercase().as_str() { + // Images + "png" => Some("public.png"), + "jpg" | "jpeg" => Some("public.jpeg"), + "gif" => Some("com.compuserve.gif"), + "bmp" => Some("com.microsoft.bmp"), + "tiff" | "tif" => Some("public.tiff"), + "ico" => Some("com.microsoft.ico"), + "heic" | "heif" => Some("public.heif-standard-image"), + "webp" => Some("org.webmproject.webp"), + "svg" => Some("public.svg-image"), + // Videos + "mp4" => Some("public.mpeg-4"), + "mov" => Some("com.apple.quicktime-movie"), + "avi" => Some("public.avi"), + "mkv" => Some("public.mpeg-4"), + // Audio + "mp3" => Some("public.mp3"), + "wav" => Some("com.microsoft.waveform-audio"), + "aac" => Some("public.aac-audio"), + "m4a" => Some("public.mpeg-4-audio"), + // Documents + "pdf" => Some("com.adobe.pdf"), + "txt" => Some("public.plain-text"), + "rtf" => Some("public.rtf"), + "html" | "htm" => Some("public.html"), + "json" => Some("public.json"), + "xml" => Some("public.xml"), + _ => None, + } +} + +/// Infers UTIs from mime type +fn mime_type_to_uti(mime_type: &str) -> Option<&'static str> { + match mime_type { + "image/png" => Some("public.png"), + "image/jpeg" | "image/jpg" => Some("public.jpeg"), + "image/gif" => Some("com.compuserve.gif"), + "image/bmp" => Some("com.microsoft.bmp"), + "image/tiff" => Some("public.tiff"), + "image/heic" | "image/heif" => Some("public.heif-standard-image"), + "image/webp" => Some("org.webmproject.webp"), + "image/svg+xml" => Some("public.svg-image"), + mime if mime.starts_with("image/") => Some("public.image"), + "video/mp4" => Some("public.mpeg-4"), + "video/quicktime" => Some("com.apple.quicktime-movie"), + "video/x-msvideo" => Some("public.avi"), + mime if mime.starts_with("video/") => Some("public.movie"), + "audio/mpeg" | "audio/mp3" => Some("public.mp3"), + "audio/wav" | "audio/wave" => Some("com.microsoft.waveform-audio"), + "audio/aac" => Some("public.aac-audio"), + "audio/mp4" => Some("public.mpeg-4-audio"), + mime if mime.starts_with("audio/") => Some("public.audio"), + "application/pdf" => Some("com.adobe.pdf"), + "text/plain" => Some("public.plain-text"), + "text/rtf" => Some("public.rtf"), + "text/html" => Some("public.html"), + "application/json" => Some("public.json"), + "application/xml" | "text/xml" => Some("public.xml"), + _ => None, + } +} + /// Deep link protocol configuration. #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[cfg_attr(feature = "schema", derive(JsonSchema))] @@ -1567,7 +1856,7 @@ pub enum ScrollBarStyle { /// Fluent UI style overlay scrollbars. **Windows Only** /// /// Requires WebView2 Runtime version 125.0.2535.41 or higher, does nothing on older versions, - /// see https://learn.microsoft.com/en-us/microsoft-edge/webview2/release-notes/?tabs=dotnetcsharp#10253541 + /// see FluentOverlay, } @@ -1859,7 +2148,7 @@ pub struct WindowConfig { /// - **iOS**: Supported since version 17.0+. /// - **macOS**: Supported since version 14.0+. /// - /// see https://github.com/tauri-apps/tauri/issues/5250#issuecomment-2569380578 + /// see #[serde(default, alias = "background-throttling")] pub background_throttling: Option, /// Whether we should disable JavaScript code execution on the webview or not. @@ -1919,6 +2208,40 @@ pub struct WindowConfig { /// - **Linux / Android / iOS / macOS**: Unsupported. Only supports `Default` and performs no operation. #[serde(default, alias = "scroll-bar-style")] pub scroll_bar_style: ScrollBarStyle, + /// The name of the Android activity to create for this window. + #[serde(default, alias = "activity-name")] + pub activity_name: Option, + /// The name of the Android activity that is creating this webview window. + /// + /// This is important to determine which stack the activity will belong to. + #[serde(default, alias = "created-by-activity-name")] + pub created_by_activity_name: Option, + + /// Sets the identifier of the scene that is requesting the new scene, + /// establishing a relationship between the two scenes. + /// + /// By default the system uses the foreground scene. + #[serde(default, alias = "requested-by-scene-identifier")] + pub requested_by_scene_identifier: Option, + /// Controls the WebView's browser-level general autofill behavior. + /// + /// **This option does not disable password or credit card autofill.** + /// + /// When set to `false`, the WebView will not automatically populate + /// general form fields using previously stored data such as addresses + /// or contact information. + /// + /// If not specified, this is `true` by default. + /// + /// ## Platform-specific + /// + /// - **Windows**: Supported. WebView2's autofill feature (called + /// "Suggestions") may not honor `autocomplete="off"` on input + /// elements in some cases. + /// - **Linux / Android / iOS / macOS**: Unsupported and performs no + /// operation. + #[serde(default = "default_true", alias = "general-autofill-enabled")] + pub general_autofill_enabled: bool, } impl Default for WindowConfig { @@ -1981,6 +2304,10 @@ impl Default for WindowConfig { data_directory: None, data_store_identifier: None, scroll_bar_style: ScrollBarStyle::Default, + activity_name: None, + created_by_activity_name: None, + requested_by_scene_identifier: None, + general_autofill_enabled: true, } } } @@ -1990,11 +2317,11 @@ fn default_window_label() -> String { } fn default_width() -> f64 { - 800f64 + 800. } fn default_height() -> f64 { - 600f64 + 600. } fn default_title() -> String { @@ -2858,6 +3185,12 @@ pub struct AndroidConfig { /// Note that to use this feature, you should remove `/tauri.properties` from `src-tauri/gen/android/app/.gitignore` so the current versionCode is committed to the repository. #[serde(alias = "auto-increment-version-code", default)] pub auto_increment_version_code: bool, + + /// Application ID suffix to append for debug builds. + /// This allows installing debug and release versions side-by-side on the same device. + /// Example: ".debug" will make debug builds use "com.example.app.debug" as the application ID. + #[serde(alias = "debug-application-id-suffix")] + pub debug_application_id_suffix: Option, } impl Default for AndroidConfig { @@ -2866,6 +3199,7 @@ impl Default for AndroidConfig { min_sdk_version: default_min_sdk_version(), version_code: None, auto_increment_version_code: false, + debug_application_id_suffix: None, } } } @@ -3067,6 +3401,32 @@ pub struct BuildConfig { /// Additional paths to watch for changes when running `tauri dev`. #[serde(alias = "additional-watch-directories", default)] pub additional_watch_folders: Vec, + /// Windows-specific build configuration. + #[serde(default)] + pub windows: WindowsBuildConfig, +} + +/// Windows-specific build configuration. +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[cfg_attr(feature = "schema", derive(JsonSchema))] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WindowsBuildConfig { + /// Whether to statically link the Visual C++ runtime into the application binary on Windows MSVC targets. + #[serde( + default = "default_true", + rename = "staticVCRuntime", + alias = "static-vc-runtime", + alias = "staticVcRuntime" + )] + pub static_vc_runtime: bool, +} + +impl Default for WindowsBuildConfig { + fn default() -> Self { + Self { + static_vc_runtime: true, + } + } } #[derive(Debug, PartialEq, Eq)] @@ -3275,7 +3635,7 @@ pub struct PluginConfig(pub HashMap); /// This allows for a build script to output the values in a `Config` to a `TokenStream`, which can /// then be consumed by another crate. Useful for passing a config to both the build script and the /// application using tauri while only parsing it once (in the build script). -#[cfg(feature = "build")] +#[cfg(any(feature = "build", feature = "build-2"))] mod build { use super::*; use crate::{literal_struct, tokens::*}; @@ -3515,6 +3875,10 @@ mod build { let data_directory = opt_lit(self.data_directory.as_ref().map(path_buf_lit).as_ref()); let data_store_identifier = opt_vec_lit(self.data_store_identifier, identity); let scroll_bar_style = &self.scroll_bar_style; + let activity_name = opt_lit(self.activity_name.as_ref()); + let created_by_activity_name = opt_lit(self.created_by_activity_name.as_ref()); + let requested_by_scene_identifier = opt_lit(self.requested_by_scene_identifier.as_ref()); + let general_autofill_enabled = self.general_autofill_enabled; literal_struct!( tokens, @@ -3575,7 +3939,11 @@ mod build { disable_input_accessory_view, data_directory, data_store_identifier, - scroll_bar_style + scroll_bar_style, + activity_name, + created_by_activity_name, + requested_by_scene_identifier, + general_autofill_enabled ); } } @@ -3737,6 +4105,7 @@ mod build { let features = quote!(None); let remove_unused_commands = quote!(false); let additional_watch_folders = quote!(Vec::new()); + let windows = &self.windows; literal_struct!( tokens, @@ -3749,7 +4118,20 @@ mod build { before_bundle_command, features, remove_unused_commands, - additional_watch_folders + additional_watch_folders, + windows + ); + } + } + + impl ToTokens for WindowsBuildConfig { + fn to_tokens(&self, tokens: &mut TokenStream) { + let static_vc_runtime = self.static_vc_runtime; + + literal_struct!( + tokens, + ::tauri::utils::config::WindowsBuildConfig, + static_vc_runtime ); } } @@ -4093,6 +4475,7 @@ mod test { features: None, remove_unused_commands: false, additional_watch_folders: Vec::new(), + windows: Default::default(), }; // create a bundle config diff --git a/crates/tauri-utils/src/html.rs b/crates/tauri-utils/src/html.rs index 4f8499798c53..cc5344d87077 100644 --- a/crates/tauri-utils/src/html.rs +++ b/crates/tauri-utils/src/html.rs @@ -281,6 +281,7 @@ pub fn inline_isolation(document: &NodeRef, dir: &Path) { } } +// TODO: Verify this, this is not found in the HTML spec, see https://github.com/tauri-apps/tauri/pull/14265#discussion_r2415396842 /// Normalize line endings in script content to match what the browser uses for CSP hashing. /// /// According to the HTML spec, browsers normalize: @@ -315,6 +316,13 @@ pub fn normalize_script_for_csp(input: &[u8]) -> Vec { #[cfg(test)] mod tests { + use std::io::Write; + + use super::*; + use crate::{ + assets::{SCRIPT_NONCE_TOKEN, STYLE_NONCE_TOKEN}, + config, + }; #[test] fn csp() { @@ -322,12 +330,14 @@ mod tests { "".to_string(), "".to_string(), ]; + for html in htmls { - let document = super::parse(html); + let document = parse(html); let csp = "csp-string"; - super::inject_csp(&document, csp); + inject_csp(&document, csp); + assert_eq!( - document.to_string(), + String::from_utf8(serialize_node(&document)).unwrap(), format!( r#""#, ) @@ -336,12 +346,97 @@ mod tests { } #[test] - fn normalize_script_for_csp() { + fn normalize_script_for_csp_test() { let js = "// Copyright 2019-2024 Tauri Programme within The Commons Conservancy\r// SPDX-License-Identifier: Apache-2.0\n// SPDX-License-Identifier: MIT\r\n\r\nwindow.__TAURI_ISOLATION_HOOK__ = (payload, options) => {\r\n return payload\r\n}\r\n"; let expected = "// Copyright 2019-2024 Tauri Programme within The Commons Conservancy\n// SPDX-License-Identifier: Apache-2.0\n// SPDX-License-Identifier: MIT\n\nwindow.__TAURI_ISOLATION_HOOK__ = (payload, options) => {\n return payload\n}\n"; + + assert_eq!(normalize_script_for_csp(js.as_bytes()), expected.as_bytes()) + } + + #[test] + fn parse_and_serialize_roundtrips() { + let htmls = [ + "Test

Hello

", + "", + ]; + + for html in htmls { + let parsed = parse(html.to_string()); + let serialized = serialize_node(&parsed); + let result = String::from_utf8(serialized).unwrap(); + + assert_eq!(result, html); + } + } + + #[test] + fn inject_nonce_to_scripts() { + let html = r#""#; + + let document = parse(html.to_string()); + inject_nonce_token(&document, &config::DisabledCspModificationKind::Flag(false)); + + assert_eq!( + String::from_utf8(serialize_node(&document)).unwrap(), + format!( + r#""# + ) + ); + } + + #[test] + fn inject_nonce_to_styles() { + let html = r#""#; + + let document = parse(html.to_string()); + inject_nonce_token(&document, &config::DisabledCspModificationKind::Flag(false)); + + assert_eq!( + String::from_utf8(serialize_node(&document)).unwrap(), + format!( + r#""# + ) + ); + } + + #[test] + fn inject_nonce_skips_existing() { + let html = r#""#; + + let document = parse(html.to_string()); + inject_nonce_token(&document, &config::DisabledCspModificationKind::Flag(false)); + + assert_eq!(String::from_utf8(serialize_node(&document)).unwrap(), html); + } + + #[test] + fn inject_nonce_respects_disabled_modification() { + let html = r#""#; + + let document = parse(html.to_string()); + inject_nonce_token(&document, &config::DisabledCspModificationKind::Flag(true)); + assert_eq!( - super::normalize_script_for_csp(js.as_bytes()), - expected.as_bytes() - ) + String::from_utf8(serialize_node(&document)).unwrap(), + r#""# + ); + } + + #[test] + fn inline_isolation_replaces_src_with_content() { + let temp_dir = tempfile::tempdir().unwrap(); + let mut file = tempfile::NamedTempFile::with_suffix_in(".js", &temp_dir).unwrap(); + file.write_all(b"console.log('test');").unwrap(); + let file_name = file.path().file_name().unwrap().to_str().unwrap(); + + let html = + format!(r#""#); + let document = parse(html); + inline_isolation(&document, temp_dir.path()); + + assert_eq!( + String::from_utf8(serialize_node(&document)).unwrap(), + r#""# + ); } } diff --git a/crates/tauri-utils/src/html2.rs b/crates/tauri-utils/src/html2.rs new file mode 100644 index 000000000000..7f591dc2788d --- /dev/null +++ b/crates/tauri-utils/src/html2.rs @@ -0,0 +1,335 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! The module to process HTML in Tauri. +//! +//! # Stability +//! +//! This is utility used in Tauri internally and not considered part of the stable API. +//! If you use it, note that it may include breaking changes in the future. + +use dom_query::NodeRef; + +use crate::{ + assets::{SCRIPT_NONCE_TOKEN, STYLE_NONCE_TOKEN}, + config::DisabledCspModificationKind, +}; + +/// # Stability +/// +/// This dependency might receive updates in minor releases. +pub use dom_query::Document; + +/// Serializes the document to HTML. +/// +/// # Stability +/// +/// This dependency [`dom_query`] for [`Document`] might receive updates in minor releases. +pub fn serialize_doc(document: &Document) -> Vec { + document.html().as_bytes().to_vec() +} + +/// Parses the given HTML string. +/// +/// # Stability +/// +/// This dependency [`dom_query`] for [`Document`] might receive updates in minor releases. +pub fn parse_doc(html: String) -> Document { + Document::from(html) +} + +fn ensure_head(document: &Document) -> NodeRef<'_> { + document.head().unwrap_or_else(|| { + let html = document.html_root(); + let head = document.tree.new_element("head"); + html.prepend_child(&head); + head + }) +} + +fn inject_nonce(document: &Document, selector: &str, token: &str) { + let elements = document.select(selector); + for elem in elements.nodes() { + // if the node already has the `nonce` attribute, skip it + if elem.attr("nonce").is_some() { + continue; + } + elem.set_attr("nonce", token); + } +} + +/// Inject nonce tokens to all scripts and styles. +/// +/// # Stability +/// +/// This dependency [`dom_query`] for [`Document`] might receive updates in minor releases. +pub fn inject_nonce_token( + document: &Document, + dangerous_disable_asset_csp_modification: &DisabledCspModificationKind, +) { + if dangerous_disable_asset_csp_modification.can_modify("script-src") { + inject_nonce(document, "script[src^='http']", SCRIPT_NONCE_TOKEN); + } + if dangerous_disable_asset_csp_modification.can_modify("style-src") { + inject_nonce(document, "style", STYLE_NONCE_TOKEN); + } +} + +/// Injects a content security policy to the HTML. +/// +/// # Stability +/// +/// This dependency [`dom_query`] for [`Document`] might receive updates in minor releases. +pub fn inject_csp(document: &Document, csp: &str) { + let head = ensure_head(document); + let meta_tag = document.tree.new_element("meta"); + meta_tag.set_attr("http-equiv", "Content-Security-Policy"); + meta_tag.set_attr("content", csp); + head.append_child(&meta_tag); +} + +/// Injects a content security policy to the HTML. +/// +/// # Stability +/// +/// This dependency [`dom_query`] for [`Document`] might receive updates in minor releases. +pub fn append_script_to_head(document: &Document, script: &str) { + let head = ensure_head(document); + let script_tag = document.tree.new_element("script"); + script_tag.set_text(script); + head.prepend_child(&script_tag); +} + +/// Injects the Isolation JavaScript to a codegen time document. +/// +/// Note: This function is not considered part of the stable API. +/// +/// # Stability +/// +/// This dependency [`dom_query`] for [`Document`] might receive updates in minor releases. +#[cfg(feature = "isolation")] +pub fn inject_codegen_isolation_script(document: &Document) { + use crate::pattern::isolation::IsolationJavascriptCodegen; + use serialize_to_javascript::DefaultTemplate; + + let head = ensure_head(document); + + let script_content = IsolationJavascriptCodegen {} + .render_default(&Default::default()) + .expect("unable to render codegen isolation script template") + .into_string(); + + let script_tag = document.tree.new_element("script"); + script_tag.set_attr("nonce", SCRIPT_NONCE_TOKEN); + script_tag.set_text(script_content); + + head.prepend_child(&script_tag); +} + +/// Temporary workaround for Windows not allowing requests +/// +/// Note: this does not prevent path traversal due to the isolation application expectation that it +/// is secure. +/// +/// # Stability +/// +/// This dependency [`dom_query`] for [`Document`] might receive updates in minor releases. +#[cfg(feature = "isolation")] +pub fn inline_isolation(document: &Document, dir: &std::path::Path) { + let scripts = document.select("script[src]"); + + for script in scripts.nodes() { + let src = match script.attr("src") { + Some(s) => s.to_string(), + None => continue, + }; + + let mut path = std::path::PathBuf::from(src); + if path.has_root() { + path = path + .strip_prefix("/") + .expect("Tauri \"Isolation\" Pattern only supports relative or absolute (`/`) paths.") + .into(); + } + + let file = std::fs::read_to_string(dir.join(path)).expect("unable to find isolation file"); + + script.set_text(file); + script.remove_attr("src"); + } +} + +// TODO: Verify this, this is not found in the HTML spec, see https://github.com/tauri-apps/tauri/pull/14265#discussion_r2415396842 +/// Normalize line endings in script content to match what the browser uses for CSP hashing. +/// +/// According to the HTML spec, browsers normalize: +/// - `\r\n` → `\n` +/// - `\r` → `\n` +pub fn normalize_script_for_csp(input: &[u8]) -> Vec { + let mut output = Vec::with_capacity(input.len()); + + let mut i = 0; + while i < input.len() { + match input[i] { + b'\r' => { + if i + 1 < input.len() && input[i + 1] == b'\n' { + // CRLF → LF + output.push(b'\n'); + i += 2; + } else { + // Lone CR → LF + output.push(b'\n'); + i += 1; + } + } + _ => { + output.push(input[i]); + i += 1; + } + } + } + + output +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + assets::{SCRIPT_NONCE_TOKEN, STYLE_NONCE_TOKEN}, + config, + }; + + #[test] + fn csp() { + let htmls = vec![ + "".to_string(), + "".to_string(), + ]; + + for html in htmls { + let document = parse_doc(html); + let csp = "csp-string"; + inject_csp(&document, csp); + + assert_eq!( + String::from_utf8(serialize_doc(&document)).unwrap(), + format!( + r#""# + ) + ); + } + } + + #[test] + fn normalize_script_for_csp_test() { + let js = "// Copyright 2019-2024 Tauri Programme within The Commons Conservancy\r// SPDX-License-Identifier: Apache-2.0\n// SPDX-License-Identifier: MIT\r\n\r\nwindow.__TAURI_ISOLATION_HOOK__ = (payload, options) => {\r\n return payload\r\n}\r\n"; + let expected = "// Copyright 2019-2024 Tauri Programme within The Commons Conservancy\n// SPDX-License-Identifier: Apache-2.0\n// SPDX-License-Identifier: MIT\n\nwindow.__TAURI_ISOLATION_HOOK__ = (payload, options) => {\n return payload\n}\n"; + + assert_eq!(normalize_script_for_csp(js.as_bytes()), expected.as_bytes()) + } + + #[test] + fn parse_and_serialize_roundtrips() { + let htmls = [ + "Test

Hello

", + "", + ]; + + for html in htmls { + let parsed = parse_doc(html.to_string()); + let serialized = serialize_doc(&parsed); + let result = String::from_utf8(serialized).unwrap(); + + assert_eq!(result, html); + } + } + + #[test] + fn inject_nonce_to_scripts() { + let html = r#""#; + + let document = parse_doc(html.to_string()); + inject_nonce_token(&document, &config::DisabledCspModificationKind::Flag(false)); + + assert_eq!( + String::from_utf8(serialize_doc(&document)).unwrap(), + format!( + r#""# + ) + ); + } + + #[test] + fn inject_nonce_to_styles() { + let html = r#""#; + + let document = parse_doc(html.to_string()); + inject_nonce_token(&document, &config::DisabledCspModificationKind::Flag(false)); + + assert_eq!( + String::from_utf8(serialize_doc(&document)).unwrap(), + format!( + r#""# + ) + ); + } + + #[test] + fn append_script_to_head_test() { + let html = r#""#; + + let document = parse_doc(html.to_string()); + append_script_to_head(&document, r#"console.log('Test')"#); + + assert_eq!( + String::from_utf8(serialize_doc(&document)).unwrap(), + format!(r#""#) + ); + } + + #[test] + fn inject_nonce_skips_existing() { + let html = r#""#; + + let document = parse_doc(html.to_string()); + inject_nonce_token(&document, &config::DisabledCspModificationKind::Flag(false)); + + assert_eq!(String::from_utf8(serialize_doc(&document)).unwrap(), html); + } + + #[test] + fn inject_nonce_respects_disabled_modification() { + let html = r#""#; + + let document = parse_doc(html.to_string()); + inject_nonce_token(&document, &config::DisabledCspModificationKind::Flag(true)); + + assert_eq!( + String::from_utf8(serialize_doc(&document)).unwrap(), + r#""# + ); + } + + #[test] + #[cfg(feature = "isolation")] + fn inline_isolation_replaces_src_with_content() { + use std::io::Write; + + let temp_dir = tempfile::tempdir().unwrap(); + let mut file = tempfile::NamedTempFile::with_suffix_in(".js", &temp_dir).unwrap(); + file.write_all(b"console.log('test');").unwrap(); + let file_name = file.path().file_name().unwrap().to_str().unwrap(); + + let html = + format!(r#""#); + let document = parse_doc(html); + inline_isolation(&document, temp_dir.path()); + + assert_eq!( + String::from_utf8(serialize_doc(&document)).unwrap(), + r#""# + ); + } +} diff --git a/crates/tauri-utils/src/lib.rs b/crates/tauri-utils/src/lib.rs index 5070ff7f35d0..fb85d7dcfd5e 100644 --- a/crates/tauri-utils/src/lib.rs +++ b/crates/tauri-utils/src/lib.rs @@ -26,6 +26,8 @@ pub mod config; pub mod config_v1; #[cfg(feature = "html-manipulation")] pub mod html; +#[cfg(feature = "html-manipulation-2")] +pub mod html2; pub mod io; pub mod mime_type; pub mod platform; @@ -33,10 +35,10 @@ pub mod plugin; /// Prepare application resources and sidecars. #[cfg(feature = "resources")] pub mod resources; -#[cfg(feature = "build")] +#[cfg(any(feature = "build", feature = "build-2"))] pub mod tokens; -#[cfg(feature = "build")] +#[cfg(any(feature = "build", feature = "build-2"))] pub mod build; /// Application pattern. diff --git a/crates/tauri-utils/src/platform.rs b/crates/tauri-utils/src/platform.rs index ebb3858824ba..708f9684f085 100644 --- a/crates/tauri-utils/src/platform.rs +++ b/crates/tauri-utils/src/platform.rs @@ -369,7 +369,7 @@ pub fn bundle_type() -> Option { } } -#[cfg(feature = "build")] +#[cfg(any(feature = "build", feature = "build-2"))] mod build { use proc_macro2::TokenStream; use quote::{ToTokens, TokenStreamExt, quote}; diff --git a/crates/tauri-utils/src/platform/starting_binary.rs b/crates/tauri-utils/src/platform/starting_binary.rs index 05e28c7fb354..4e6d29c3cc2f 100644 --- a/crates/tauri-utils/src/platform/starting_binary.rs +++ b/crates/tauri-utils/src/platform/starting_binary.rs @@ -11,7 +11,7 @@ use std::{ /// A cached version of the current binary using [`ctor`] to cache it before even `main` runs. #[ctor] #[used] -pub(super) static STARTING_BINARY: StartingBinary = StartingBinary::new(); +pub(super) static STARTING_BINARY: StartingBinary = unsafe { StartingBinary::new() }; /// Represents a binary path that was cached when the program was loaded. pub(super) struct StartingBinary(std::io::Result); diff --git a/crates/tauri-utils/src/plugin.rs b/crates/tauri-utils/src/plugin.rs index 8f178ca4163a..9fe35be2dfc4 100644 --- a/crates/tauri-utils/src/plugin.rs +++ b/crates/tauri-utils/src/plugin.rs @@ -3,10 +3,10 @@ // SPDX-License-Identifier: MIT //! Compile-time and runtime types for Tauri plugins. -#[cfg(feature = "build")] +#[cfg(any(feature = "build", feature = "build-2"))] pub use build::*; -#[cfg(feature = "build")] +#[cfg(any(feature = "build", feature = "build-2"))] mod build { use std::{ env::vars_os, diff --git a/crates/tauri-utils/src/resources.rs b/crates/tauri-utils/src/resources.rs index 210374a2a048..3639b9ae302c 100644 --- a/crates/tauri-utils/src/resources.rs +++ b/crates/tauri-utils/src/resources.rs @@ -209,7 +209,21 @@ impl ResourcePathsIter<'_> { // preserving the file name as it is ResourcePathsInnerIter::Glob { .. } => dest.join(path.file_name().unwrap()), }, - None => dest.clone(), + None => { + if dest.components().count() == 0 { + // if current_dest is empty while processing a file pattern + // we preserve the file name as it is + // + // e.g. `{ "README.md": "" }` is `README.md` -> `$RESOURCE/README.md` + // + // TODO: This behavior is a confusing special case, + // remove this in v3 or make other cases like this work + // > `{ "README.md": "./folder/" }` is `README.md` -> `$RESOURCE/folder/README.md` (this gives `$RESOURCE/folder` today) + PathBuf::from(path.file_name().unwrap()) + } else { + dest.clone() + } + } } } else { // If [`ResourcePathsIter::pattern_iter`] is a [`PatternIter::Slice`] @@ -221,6 +235,7 @@ impl ResourcePathsIter<'_> { fn next_pattern(&mut self) -> Option> { self.current_dest = None; + self.current_iter = None; let pattern = match &mut self.pattern_iter { PatternIter::Slice(iter) => iter.next()?, @@ -237,10 +252,10 @@ impl ResourcePathsIter<'_> { Err(error) => return Some(Err(error.into())), }; match self.next_current_iter() { - Some(r) => return Some(r), + Some(r) => Some(r), None => { self.current_iter = None; - return Some(Err(crate::Error::GlobPathNotFound(pattern.clone()))); + Some(Err(crate::Error::GlobPathNotFound(pattern.clone()))) } } } else { @@ -257,12 +272,12 @@ impl ResourcePathsIter<'_> { None }, }); + // If the directory is empty, skip and continue to the next pattern + self.next_current_iter().or_else(|| self.next_pattern()) } else { - return Some(self.resource_from_path(path)); + Some(self.resource_from_path(path)) } } - - self.next_current_iter() } } @@ -358,6 +373,7 @@ mod tests { fs::create_dir_all(path.parent().unwrap()).unwrap(); fs::write(path, "").unwrap(); } + fs::create_dir_all("empty-directory").unwrap(); } fn resources_map(literal: &[(&str, &str)]) -> HashMap { @@ -377,6 +393,8 @@ mod tests { let resources = ResourcePaths::new( &[ + // `empty-directory` should not affect anything + "../empty-directory".into(), "../src/script.js".into(), "../src/assets".into(), "../src/index.html".into(), diff --git a/crates/tauri/CHANGELOG.md b/crates/tauri/CHANGELOG.md index 3572afd7ab04..b709e89f0c0b 100644 --- a/crates/tauri/CHANGELOG.md +++ b/crates/tauri/CHANGELOG.md @@ -1,5 +1,86 @@ # Changelog +## \[2.11.2] + +### Bug Fixes + +- [`47e1b7549`](https://www.github.com/tauri-apps/tauri/commit/47e1b754951bffeedbcd6400928d60755fb954de) ([#15386](https://www.github.com/tauri-apps/tauri/pull/15386) by [@DominikPeters](https://www.github.com/tauri-apps/tauri/../../DominikPeters)) Fixed `Submenu.setAsWindowsMenuForNSApp()` calling the Help menu setter instead of the Window menu setter. + +### Dependencies + +- Upgraded to `tauri-utils@2.9.2` +- Upgraded to `tauri-runtime@2.11.2` +- Upgraded to `tauri-runtime-wry@2.11.2` +- Upgraded to `tauri-macros@2.6.2` +- Upgraded to `tauri-build@2.6.2` + +## \[2.11.1] + +### Enhancements + +- [`5e3126ff7`](https://www.github.com/tauri-apps/tauri/commit/5e3126ff7045aec54811b227cb4d33d78b3957b5) ([#15338](https://www.github.com/tauri-apps/tauri/pull/15338)) Expose the monitor (display) APIs on mobile. + +### Bug Fixes + +- [`5f479c0c3`](https://www.github.com/tauri-apps/tauri/commit/5f479c0c364d7f5d89a83eaff66fbb7ef5045ce9) ([#15336](https://www.github.com/tauri-apps/tauri/pull/15336)) Fix crash when using the requestPermission API on Android. + +### Security fixes + +- [`1b26769f9`](https://www.github.com/tauri-apps/tauri/commit/1b26769f92b54b158777a35a7f548f870f4e7901) ([#15266](https://www.github.com/tauri-apps/tauri/pull/15266)) Enforce ACL checks for IPC requests from remote origins even when no `AppManifest` is configured. Previously, custom (non-plugin) commands bypassed ACL entirely without an `AppManifest`, allowing any origin to invoke them. Now, remote origins are always subject to ACL resolution, and can only reach custom commands if an explicit `remote` capability has been granted. +- [`ba025588f`](https://www.github.com/tauri-apps/tauri/commit/ba025588f3559858f43547e8c04424c47a3c445b) Correctly handle .localhost suffix in local origins on Windows and Android to fix a security issue that made tauri think remote websites that started with a registered scheme were local websites. + For example, when registering an `app` custom protocol, Tauri would think `http://app.evil.com/` would be a local URL on Windows/Android. + +### Dependencies + +- Upgraded to `tauri-utils@2.9.1` +- Upgraded to `tauri-runtime@2.11.1` +- Upgraded to `tauri-runtime-wry@2.11.1` +- Upgraded to `tauri-macros@2.6.1` +- Upgraded to `tauri-build@2.6.1` + +## \[2.11.0] + +### New Features + +- [`074299c08`](https://www.github.com/tauri-apps/tauri/commit/074299c08dd99d2e1c57796f55ab24bc1d3976cc) ([#14307](https://www.github.com/tauri-apps/tauri/pull/14307)) Add Bring All to Front predefined menu item type +- [`c00a3dbff`](https://www.github.com/tauri-apps/tauri/commit/c00a3dbffccd6e051d3b7332f706b6c63759865d) ([#14473](https://www.github.com/tauri-apps/tauri/pull/14473)) Add support for the `rename` attribute in the `tauri::command` macro to allow renaming the command to something other than the function name. +- [`a12142a48`](https://www.github.com/tauri-apps/tauri/commit/a12142a481f7a19b69e88ee36a438b1db71b36f5) ([#14357](https://www.github.com/tauri-apps/tauri/pull/14357)) Add macos support for setting the icon and icon template state in the same step of the main thread, to prevent flickering. +- [`2dd9b15a2`](https://www.github.com/tauri-apps/tauri/commit/2dd9b15a2bcab8e52c87b03a919b4a75567ad3ce) ([#15062](https://www.github.com/tauri-apps/tauri/pull/15062)) Add `data-tauri-drag-region="deep"` so clicks on non-clickable children will drag as well. Can still opt out of drag on some regions using `data-tauri-drag-region="false"` +- [`001c8fe3d`](https://www.github.com/tauri-apps/tauri/commit/001c8fe3d288802de9a8c29cfd2f46f9220d97c5) ([#14722](https://www.github.com/tauri-apps/tauri/pull/14722)) Add a WebView option to control browser-level general autofill behavior. This option does not disable password or credit card autofill. On Windows (WebView2), setting it to true disables the general autofill "Suggestions" UI, which may appear even when `autocomplete="off"` is specified on input elements. On Linux, macOS, iOS, and Android, this option is currently unsupported and performs no operation. +- [`b27be063f`](https://www.github.com/tauri-apps/tauri/commit/b27be063ff3052cb1071ac3ec719cfa104460fa4) ([#14925](https://www.github.com/tauri-apps/tauri/pull/14925)) Add `eval_with_callback` to the Tauri webview APIs and runtime dispatch layers. +- [`d83d2d92b`](https://www.github.com/tauri-apps/tauri/commit/d83d2d92b4327da3dbac60f83cada36c8ec194dc) ([#14905](https://www.github.com/tauri-apps/tauri/pull/14905)) Enable track_caller attribute for async_runtime to provide better location information in logs and panics. +- [`cc5c97602`](https://www.github.com/tauri-apps/tauri/commit/cc5c976027b0ab2431c13ec5b2e201d4414a8a6e) ([#14486](https://www.github.com/tauri-apps/tauri/pull/14486)) Implement file association for Android and iOS. +- [`cc5c97602`](https://www.github.com/tauri-apps/tauri/commit/cc5c976027b0ab2431c13ec5b2e201d4414a8a6e) ([#14486](https://www.github.com/tauri-apps/tauri/pull/14486)) Trigger `RunEvent::Opened` on Android. +- [`eb0312ea9`](https://www.github.com/tauri-apps/tauri/commit/eb0312ea9e493954298ac0b3fdaae7eafb52750e) ([#15199](https://www.github.com/tauri-apps/tauri/pull/15199)) Propagates the `Event::Suspended` and `Event::Resumed` events from `tao` when they are emitted on mobile targets. +- [`093e2b47c`](https://www.github.com/tauri-apps/tauri/commit/093e2b47c01361c18783e9ff18750388e41650c5) ([#14484](https://www.github.com/tauri-apps/tauri/pull/14484)) Support creating multiple windows on Android (activity embedding) and iOS (scenes). +- [`093e2b47c`](https://www.github.com/tauri-apps/tauri/commit/093e2b47c01361c18783e9ff18750388e41650c5) ([#14484](https://www.github.com/tauri-apps/tauri/pull/14484)) Added `dbus` feature flag (enabled by default) which is required for theme detection on Linux. +- [`1063c48c5`](https://www.github.com/tauri-apps/tauri/commit/1063c48c5e7d099ad74d28a937edf42e3f5c9f03) ([#14523](https://www.github.com/tauri-apps/tauri/pull/14523)) Add handler for web content process termination on macOS and iOS. + +### Enhancements + +- [`d730770bb`](https://www.github.com/tauri-apps/tauri/commit/d730770bb93d77358cfc6f1286f10187cef37362) ([#15117](https://www.github.com/tauri-apps/tauri/pull/15117)) Simplify async-sync code boundaries, no externally visible changes +- [`c69d5ca4b`](https://www.github.com/tauri-apps/tauri/commit/c69d5ca4b1a646843c3f250a0d1b13414c5e8223) ([#15262](https://www.github.com/tauri-apps/tauri/pull/15262)) Remove a clone, no user-facing changes. +- [`4017a7ed7`](https://www.github.com/tauri-apps/tauri/commit/4017a7ed7313cebf912ef3af1e3b280855b6f100) ([#14908](https://www.github.com/tauri-apps/tauri/pull/14908)) Implement retrieving inner PathBuf from SafePathBuf to ease using APIs that require an owned PathBuf + +### Bug Fixes + +- [`110336c88`](https://www.github.com/tauri-apps/tauri/commit/110336c88a8c0a04476619db0a5c8f7694d969a5) ([#15250](https://www.github.com/tauri-apps/tauri/pull/15250)) Fix initial window position when positioning it to another monitor. +- [`9808236eb`](https://www.github.com/tauri-apps/tauri/commit/9808236ebf7755d498d674b614f3fc75eeac1ec4) ([#14655](https://www.github.com/tauri-apps/tauri/pull/14655)) Fix monitor work area Y position on macOS. + +### What's Changed + +- [`d34497ef1`](https://www.github.com/tauri-apps/tauri/commit/d34497ef154eddcc36327a30dda06dc4748f6b20) ([#14862](https://www.github.com/tauri-apps/tauri/pull/14862)) The new window handler passed to `on_new_window` no longer requires `Sync`, and runs on main thread on Windows, aligning with other platforms + +### Dependencies + +- Upgraded to `tauri-macros@2.6.0` +- Upgraded to `tauri-build@2.6.0` +- Upgraded to `tauri-runtime@2.11.0` +- Upgraded to `tauri-runtime-wry@2.11.0` +- Upgraded to `tauri-utils@2.9.0` +- [`373b7e677`](https://www.github.com/tauri-apps/tauri/commit/373b7e677ec498899759de9fcd35941fe792b58b) ([#15177](https://www.github.com/tauri-apps/tauri/pull/15177)) Update Specta in lockfile and upgrade dependencies using the removed `doc_auto_cfg` attribute to fix errors building documentation +- [`a219ede00`](https://www.github.com/tauri-apps/tauri/commit/a219ede0003bb8073d8002be42bcf343538c42f8) ([#15203](https://www.github.com/tauri-apps/tauri/pull/15203)) Updated `tray-icon` to v0.22 + ## \[2.10.3] ### Dependencies diff --git a/crates/tauri/Cargo.toml b/crates/tauri/Cargo.toml index 3faf5c3be2cd..2b1f22456367 100644 --- a/crates/tauri/Cargo.toml +++ b/crates/tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri" -version = "2.10.3" +version = "2.11.2" description = "Make tiny, secure apps for all desktop platforms with Tauri" exclude = ["/test", "/.scripts", "CHANGELOG.md", "/target"] readme = "README.md" @@ -26,6 +26,7 @@ features = [ "test", "specta", "dynamic-acl", + "isolation", ] default-target = "x86_64-unknown-linux-gnu" targets = [ @@ -55,12 +56,12 @@ uuid = { version = "1", features = ["v4"], optional = true } url = "2" anyhow = "1" thiserror = "2" -tauri-runtime = { version = "2.10.1", path = "../tauri-runtime" } -tauri-macros = { version = "2.5.5", path = "../tauri-macros" } -tauri-utils = { version = "2.8.3", features = [ +tauri-runtime = { version = "2.11.2", path = "../tauri-runtime" } +tauri-macros = { version = "2.6.2", path = "../tauri-macros" } +tauri-utils = { version = "2.9.2", features = [ "resources", ], path = "../tauri-utils" } -tauri-runtime-wry = { version = "2.10.1", path = "../tauri-runtime-wry", default-features = false, optional = true } +tauri-runtime-wry = { version = "2.11.2", path = "../tauri-runtime-wry", default-features = false, optional = true } tauri-runtime-cef = { version = "0.1.0", path = "../tauri-runtime-cef", optional = true } getrandom = "0.3" serde_repr = "0.1" @@ -97,12 +98,13 @@ rustls = { version = "0.23", default-features = false, features = [ # desktop [target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd", target_os = "windows", target_os = "macos"))'.dependencies] -muda = { version = "0.17", default-features = false, features = [ +muda = { version = "0.19", default-features = false, features = [ "serde", "gtk", ] } -tray-icon = { version = "0.21", default-features = false, features = [ +tray-icon = { version = "0.24", default-features = false, features = [ "serde", + "gtk", ], optional = true } # linux @@ -159,15 +161,17 @@ jni = "0.21" libc = "0.2" swift-rs = "1" objc2-ui-kit = { version = "0.3.0", default-features = false, features = [ + "UIApplication", + "UIResponder", "UIView", ] } [build-dependencies] glob = "0.3" heck = "0.5" -tauri-build = { path = "../tauri-build/", default-features = false, version = "2.5.6" } -tauri-utils = { path = "../tauri-utils/", version = "2.8.3", features = [ - "build", +tauri-build = { path = "../tauri-build/", default-features = false, version = "2.6.2" } +tauri-utils = { path = "../tauri-utils/", version = "2.9.2", features = [ + "build-2", ] } [dev-dependencies] @@ -182,9 +186,17 @@ cargo_toml = "0.22" http-range = "0.1.5" [features] -default = ["wry", "compression", "common-controls-v6", "dynamic-acl", "x11"] +default = [ + "wry", + "compression", + "common-controls-v6", + "dynamic-acl", + "x11", + "dbus", +] unstable = ["tauri-runtime-wry?/unstable"] x11 = ["tauri-runtime-wry?/x11"] +dbus = ["tauri-runtime-wry?/dbus"] common-controls-v6 = [ "tray-icon?/common-controls-v6", "muda/common-controls-v6", @@ -218,7 +230,7 @@ macos-private-api = [ "tauri-runtime-wry?/macos-private-api", "tauri-runtime-cef?/macos-private-api", ] -webview-data-url = ["data-url", "tauri-utils/html-manipulation"] +webview-data-url = ["data-url", "tauri-utils/html-manipulation-2"] protocol-asset = ["http-range"] config-json5 = ["tauri-macros/config-json5"] config-toml = ["tauri-macros/config-toml"] diff --git a/crates/tauri/build.rs b/crates/tauri/build.rs index 0296aebe06c2..a1b9d078f6e9 100644 --- a/crates/tauri/build.rs +++ b/crates/tauri/build.rs @@ -69,6 +69,8 @@ const PLUGINS: &[(&str, &[(&str, bool)])] = &[ ("cursor_position", true), ("theme", true), ("is_always_on_top", true), + ("activity_name", true), + ("scene_identifier", true), // setters ("center", false), ("request_user_attention", false), @@ -166,6 +168,7 @@ const PLUGINS: &[(&str, &[(&str, bool)])] = &[ ("bundle_type", true), ("register_listener", true), ("remove_listener", true), + ("supports_multiple_windows", true), ], ), ( @@ -219,6 +222,7 @@ const PLUGINS: &[(&str, &[(&str, bool)])] = &[ ("set_visible", true), ("set_temp_dir_path", true), ("set_icon_as_template", true), + ("set_icon_with_as_template", true), ("set_show_menu_on_left_click", true), ], ), diff --git a/crates/tauri/mobile/android-codegen/TauriActivity.kt b/crates/tauri/mobile/android-codegen/TauriActivity.kt index 1c96394b89a7..251c41e3598e 100644 --- a/crates/tauri/mobile/android-codegen/TauriActivity.kt +++ b/crates/tauri/mobile/android-codegen/TauriActivity.kt @@ -8,44 +8,58 @@ package {{package}} import android.content.Intent import android.content.res.Configuration +import android.os.Bundle import app.tauri.plugin.PluginManager +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ProcessLifecycleOwner + +object TauriLifecycleObserver : DefaultLifecycleObserver { + override fun onResume(owner: LifecycleOwner) { + super.onResume(owner) + PluginManager.onResume() + } + + override fun onPause(owner: LifecycleOwner) { + super.onPause(owner) + PluginManager.onPause() + } + + override fun onStop(owner: LifecycleOwner) { + super.onStop(owner) + PluginManager.onStop() + } +} abstract class TauriActivity : WryActivity() { - var pluginManager: PluginManager = PluginManager(this) override val handleBackNavigation: Boolean = false - override fun onNewIntent(intent: Intent) { - super.onNewIntent(intent) - pluginManager.onNewIntent(intent) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + PluginManager.onActivityCreate(this) } - override fun onResume() { - super.onResume() - pluginManager.onResume() + fun getPluginManager(): PluginManager { + return PluginManager } - override fun onPause() { - super.onPause() - pluginManager.onPause() + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + PluginManager.onNewIntent(intent) } override fun onRestart() { super.onRestart() - pluginManager.onRestart() - } - - override fun onStop() { - super.onStop() - pluginManager.onStop() + PluginManager.onRestart(this) } override fun onDestroy() { super.onDestroy() - pluginManager.onDestroy() + PluginManager.onDestroy(this) } override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) - pluginManager.onConfigurationChanged(newConfig) + PluginManager.onConfigurationChanged(newConfig) } } diff --git a/crates/tauri/mobile/android/src/main/java/app/tauri/plugin/Plugin.kt b/crates/tauri/mobile/android/src/main/java/app/tauri/plugin/Plugin.kt index d33fa8a4d15a..fda20dd03f07 100644 --- a/crates/tauri/mobile/android/src/main/java/app/tauri/plugin/Plugin.kt +++ b/crates/tauri/mobile/android/src/main/java/app/tauri/plugin/Plugin.kt @@ -11,6 +11,7 @@ import android.content.pm.PackageManager import android.net.Uri import android.webkit.WebView import androidx.activity.result.IntentSenderRequest +import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat import app.tauri.FsUtils import app.tauri.Logger @@ -71,10 +72,18 @@ abstract class Plugin(private val activity: Activity) { */ open fun onResume() {} + + /** + * This event is called after onStop() when the current activity is being re-displayed to the user (the user has navigated back to it). + * It will be followed by onStart() and then onResume(). + */ + open fun onRestart(activity: AppCompatActivity) {} + /** * This event is called after onStop() when the current activity is being re-displayed to the user (the user has navigated back to it). * It will be followed by onStart() and then onResume(). */ + @Deprecated("use onRestart(activity: AppCompatActivity) instead") open fun onRestart() {} /** @@ -86,8 +95,23 @@ abstract class Plugin(private val activity: Activity) { /** * This event is called before the activity is destroyed. */ + open fun onDestroy(activity: AppCompatActivity) {} + /** + * This event is called before an activity is destroyed. + */ + @Deprecated("use onDestroy(activity: AppCompatActivity) instead") open fun onDestroy() {} + internal fun triggerOnDestroy(activity: AppCompatActivity) { + onDestroy(activity) + onDestroy() + } + + internal fun triggerOnRestart(activity: AppCompatActivity) { + onRestart(activity) + onRestart() + } + /** * This event is called when a configuration change occurs but the app does not recreate the activity. */ diff --git a/crates/tauri/mobile/android/src/main/java/app/tauri/plugin/PluginManager.kt b/crates/tauri/mobile/android/src/main/java/app/tauri/plugin/PluginManager.kt index 362896b706d0..0c64bb5f3411 100644 --- a/crates/tauri/mobile/android/src/main/java/app/tauri/plugin/PluginManager.kt +++ b/crates/tauri/mobile/android/src/main/java/app/tauri/plugin/PluginManager.kt @@ -4,7 +4,6 @@ package app.tauri.plugin -import android.app.PendingIntent import android.content.res.Configuration import android.content.Context import android.content.Intent @@ -26,7 +25,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.module.SimpleModule import java.lang.reflect.InvocationTargetException -class PluginManager(val activity: AppCompatActivity) { +object PluginManager { fun interface RequestPermissionsCallback { fun onResult(permissions: Map) } @@ -35,16 +34,33 @@ class PluginManager(val activity: AppCompatActivity) { fun onResult(result: ActivityResult) } + lateinit var activity: AppCompatActivity private val plugins: HashMap = HashMap() - private val startActivityForResultLauncher: ActivityResultLauncher - private val startIntentSenderForResultLauncher: ActivityResultLauncher - private val requestPermissionsLauncher: ActivityResultLauncher> + private lateinit var startActivityForResultLauncher: ActivityResultLauncher + private lateinit var startIntentSenderForResultLauncher: ActivityResultLauncher + private lateinit var requestPermissionsLauncher: ActivityResultLauncher> private var requestPermissionsCallback: RequestPermissionsCallback? = null private var startActivityForResultCallback: ActivityResultCallback? = null private var startIntentSenderForResultCallback: ActivityResultCallback? = null - private var jsonMapper: ObjectMapper + private var jsonMapper: ObjectMapper = ObjectMapper() + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .enable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES) + .setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY) init { + val channelDeserializer = ChannelDeserializer({ channelId, payload -> + sendChannelData(channelId, payload) + }, jsonMapper) + jsonMapper + .registerModule(SimpleModule().addDeserializer(Channel::class.java, channelDeserializer)) + } + + fun onActivityCreate(activity: AppCompatActivity) { + // TODO: on destroy, we should change to a different activity + if (::activity.isInitialized) { + return + } + this.activity = activity startActivityForResultLauncher = activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult() ) { result -> @@ -68,17 +84,6 @@ class PluginManager(val activity: AppCompatActivity) { requestPermissionsCallback!!.onResult(result) } } - - jsonMapper = ObjectMapper() - .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) - .enable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES) - .setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY) - - val channelDeserializer = ChannelDeserializer({ channelId, payload -> - sendChannelData(channelId, payload) - }, jsonMapper) - jsonMapper - .registerModule(SimpleModule().addDeserializer(Channel::class.java, channelDeserializer)) } fun onNewIntent(intent: Intent) { @@ -99,9 +104,9 @@ class PluginManager(val activity: AppCompatActivity) { } } - fun onRestart() { + fun onRestart(activity: AppCompatActivity) { for (plugin in plugins.values) { - plugin.instance.onRestart() + plugin.instance.triggerOnRestart(activity) } } @@ -111,9 +116,9 @@ class PluginManager(val activity: AppCompatActivity) { } } - fun onDestroy() { + fun onDestroy(activity: AppCompatActivity) { for (plugin in plugins.values) { - plugin.instance.onDestroy() + plugin.instance.triggerOnDestroy(activity) } } @@ -201,14 +206,12 @@ class PluginManager(val activity: AppCompatActivity) { } } - companion object { - fun loadConfig(context: Context, plugin: String, cls: Class): T { - val tauriConfigJson = FsUtils.readAsset(context.assets, "tauri.conf.json") - val mapper = ObjectMapper() - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - val config = mapper.readValue(tauriConfigJson, Config::class.java) - return mapper.readValue(config.plugins[plugin].toString(), cls) - } + fun loadConfig(context: Context, plugin: String, cls: Class): T { + val tauriConfigJson = FsUtils.readAsset(context.assets, "tauri.conf.json") + val mapper = ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + val config = mapper.readValue(tauriConfigJson, Config::class.java) + return mapper.readValue(config.plugins[plugin].toString(), cls) } private external fun handlePluginResponse(id: Int, success: String?, error: String?) diff --git a/crates/tauri/permissions/app/autogenerated/reference.md b/crates/tauri/permissions/app/autogenerated/reference.md index 178a632d93ab..581bd8da85e8 100644 --- a/crates/tauri/permissions/app/autogenerated/reference.md +++ b/crates/tauri/permissions/app/autogenerated/reference.md @@ -11,6 +11,7 @@ Default permissions for the plugin. - `allow-bundle-type` - `allow-register-listener` - `allow-remove-listener` +- `allow-supports-multiple-windows` ## Permission Table @@ -336,6 +337,32 @@ Denies the set_dock_visibility command without any pre-configured scope. +`core:app:allow-supports-multiple-windows` + + + + +Enables the supports_multiple_windows command without any pre-configured scope. + + + + + + + +`core:app:deny-supports-multiple-windows` + + + + +Denies the supports_multiple_windows command without any pre-configured scope. + + + + + + + `core:app:allow-tauri-version` diff --git a/crates/tauri/permissions/tray/autogenerated/reference.md b/crates/tauri/permissions/tray/autogenerated/reference.md index 6ea38e1c9af4..341858892145 100644 --- a/crates/tauri/permissions/tray/autogenerated/reference.md +++ b/crates/tauri/permissions/tray/autogenerated/reference.md @@ -14,6 +14,7 @@ Default permissions for the plugin, which enables all commands. - `allow-set-visible` - `allow-set-temp-dir-path` - `allow-set-icon-as-template` +- `allow-set-icon-with-as-template` - `allow-set-show-menu-on-left-click` ## Permission Table @@ -158,6 +159,32 @@ Denies the set_icon_as_template command without any pre-configured scope. +`core:tray:allow-set-icon-with-as-template` + + + + +Enables the set_icon_with_as_template command without any pre-configured scope. + + + + + + + +`core:tray:deny-set-icon-with-as-template` + + + + +Denies the set_icon_with_as_template command without any pre-configured scope. + + + + + + + `core:tray:allow-set-menu` diff --git a/crates/tauri/permissions/window/autogenerated/reference.md b/crates/tauri/permissions/window/autogenerated/reference.md index 00158661d0ed..9d591ba38e3d 100644 --- a/crates/tauri/permissions/window/autogenerated/reference.md +++ b/crates/tauri/permissions/window/autogenerated/reference.md @@ -29,6 +29,8 @@ Default permissions for the plugin. - `allow-cursor-position` - `allow-theme` - `allow-is-always-on-top` +- `allow-activity-name` +- `allow-scene-identifier` - `allow-internal-toggle-maximize` ## Permission Table @@ -40,6 +42,32 @@ Default permissions for the plugin. + + + +`core:window:allow-activity-name` + + + + +Enables the activity_name command without any pre-configured scope. + + + + + + + +`core:window:deny-activity-name` + + + + +Denies the activity_name command without any pre-configured scope. + + + + @@ -875,6 +903,32 @@ Denies the scale_factor command without any pre-configured scope. +`core:window:allow-scene-identifier` + + + + +Enables the scene_identifier command without any pre-configured scope. + + + + + + + +`core:window:deny-scene-identifier` + + + + +Denies the scene_identifier command without any pre-configured scope. + + + + + + + `core:window:allow-set-always-on-bottom` diff --git a/crates/tauri/scripts/bundle.global.js b/crates/tauri/scripts/bundle.global.js index 7f8eff34b263..0441f23e6912 100644 --- a/crates/tauri/scripts/bundle.global.js +++ b/crates/tauri/scripts/bundle.global.js @@ -1 +1 @@ -var __TAURI_IIFE__=function(e){"use strict";function n(e,n,t,i){if("a"===t&&!i)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof n?e!==n||!i:!n.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===t?i:"a"===t?i.call(e):i?i.value:n.get(e)}function t(e,n,t,i,r){if("m"===i)throw new TypeError("Private method is not writable");if("a"===i&&!r)throw new TypeError("Private accessor was defined without a setter");if("function"==typeof n?e!==n||!r:!n.has(e))throw new TypeError("Cannot write private member to an object whose class did not declare it");return"a"===i?r.call(e,t):r?r.value=t:n.set(e,t),t}var i,r,s,a,l;"function"==typeof SuppressedError&&SuppressedError;const o="__TAURI_TO_IPC_KEY__";function u(e,n=!1){return window.__TAURI_INTERNALS__.transformCallback(e,n)}class c{constructor(e){i.set(this,void 0),r.set(this,0),s.set(this,[]),a.set(this,void 0),t(this,i,e||(()=>{}),"f"),this.id=u(e=>{const l=e.index;if("end"in e)return void(l==n(this,r,"f")?this.cleanupCallback():t(this,a,l,"f"));const o=e.message;if(l==n(this,r,"f")){for(n(this,i,"f").call(this,o),t(this,r,n(this,r,"f")+1,"f");n(this,r,"f")in n(this,s,"f");){const e=n(this,s,"f")[n(this,r,"f")];n(this,i,"f").call(this,e),delete n(this,s,"f")[n(this,r,"f")],t(this,r,n(this,r,"f")+1,"f")}n(this,r,"f")===n(this,a,"f")&&this.cleanupCallback()}else n(this,s,"f")[l]=o})}cleanupCallback(){window.__TAURI_INTERNALS__.unregisterCallback(this.id)}set onmessage(e){t(this,i,e,"f")}get onmessage(){return n(this,i,"f")}[(i=new WeakMap,r=new WeakMap,s=new WeakMap,a=new WeakMap,o)](){return`__CHANNEL__:${this.id}`}toJSON(){return this[o]()}}class d{constructor(e,n,t){this.plugin=e,this.event=n,this.channelId=t}async unregister(){return h(`plugin:${this.plugin}|remove_listener`,{event:this.event,channelId:this.channelId})}}async function p(e,n,t){const i=new c(t);try{return await h(`plugin:${e}|register_listener`,{event:n,handler:i}),new d(e,n,i.id)}catch{return await h(`plugin:${e}|registerListener`,{event:n,handler:i}),new d(e,n,i.id)}}async function h(e,n={},t){return window.__TAURI_INTERNALS__.invoke(e,n,t)}class w{get rid(){return n(this,l,"f")}constructor(e){l.set(this,void 0),t(this,l,e,"f")}async close(){return h("plugin:resources|close",{rid:this.rid})}}l=new WeakMap;var _=Object.freeze({__proto__:null,Channel:c,PluginListener:d,Resource:w,SERIALIZE_TO_IPC_FN:o,addPluginListener:p,checkPermissions:async function(e){return h(`plugin:${e}|check_permissions`)},convertFileSrc:function(e,n="asset"){return window.__TAURI_INTERNALS__.convertFileSrc(e,n)},invoke:h,isTauri:function(){return!!(globalThis||window).isTauri},requestPermissions:async function(e){return h(`plugin:${e}|request_permissions`)},transformCallback:u});class y extends w{constructor(e){super(e)}static async new(e,n,t){return h("plugin:image|new",{rgba:g(e),width:n,height:t}).then(e=>new y(e))}static async fromBytes(e){return h("plugin:image|from_bytes",{bytes:g(e)}).then(e=>new y(e))}static async fromPath(e){return h("plugin:image|from_path",{path:e}).then(e=>new y(e))}async rgba(){return h("plugin:image|rgba",{rid:this.rid}).then(e=>new Uint8Array(e))}async size(){return h("plugin:image|size",{rid:this.rid})}}function g(e){return null==e?null:"string"==typeof e?e:e instanceof y?e.rid:e}var b,m=Object.freeze({__proto__:null,Image:y,transformImage:g});!function(e){e.Nsis="nsis",e.Msi="msi",e.Deb="deb",e.Rpm="rpm",e.AppImage="appimage",e.App="app"}(b||(b={}));var f=Object.freeze({__proto__:null,get BundleType(){return b},defaultWindowIcon:async function(){return h("plugin:app|default_window_icon").then(e=>e?new y(e):null)},fetchDataStoreIdentifiers:async function(){return h("plugin:app|fetch_data_store_identifiers")},getBundleType:async function(){return h("plugin:app|bundle_type")},getIdentifier:async function(){return h("plugin:app|identifier")},getName:async function(){return h("plugin:app|name")},getTauriVersion:async function(){return h("plugin:app|tauri_version")},getVersion:async function(){return h("plugin:app|version")},hide:async function(){return h("plugin:app|app_hide")},onBackButtonPress:async function(e){return p("app","back-button",e)},removeDataStore:async function(e){return h("plugin:app|remove_data_store",{uuid:e})},setDockVisibility:async function(e){return h("plugin:app|set_dock_visibility",{visible:e})},setTheme:async function(e){return h("plugin:app|set_app_theme",{theme:e})},show:async function(){return h("plugin:app|app_show")}});class v{constructor(...e){this.type="Logical",1===e.length?"Logical"in e[0]?(this.width=e[0].Logical.width,this.height=e[0].Logical.height):(this.width=e[0].width,this.height=e[0].height):(this.width=e[0],this.height=e[1])}toPhysical(e){return new k(this.width*e,this.height*e)}[o](){return{width:this.width,height:this.height}}toJSON(){return this[o]()}}class k{constructor(...e){this.type="Physical",1===e.length?"Physical"in e[0]?(this.width=e[0].Physical.width,this.height=e[0].Physical.height):(this.width=e[0].width,this.height=e[0].height):(this.width=e[0],this.height=e[1])}toLogical(e){return new v(this.width/e,this.height/e)}[o](){return{width:this.width,height:this.height}}toJSON(){return this[o]()}}class A{constructor(e){this.size=e}toLogical(e){return this.size instanceof v?this.size:this.size.toLogical(e)}toPhysical(e){return this.size instanceof k?this.size:this.size.toPhysical(e)}[o](){return{[`${this.size.type}`]:{width:this.size.width,height:this.size.height}}}toJSON(){return this[o]()}}class T{constructor(...e){this.type="Logical",1===e.length?"Logical"in e[0]?(this.x=e[0].Logical.x,this.y=e[0].Logical.y):(this.x=e[0].x,this.y=e[0].y):(this.x=e[0],this.y=e[1])}toPhysical(e){return new I(this.x*e,this.y*e)}[o](){return{x:this.x,y:this.y}}toJSON(){return this[o]()}}class I{constructor(...e){this.type="Physical",1===e.length?"Physical"in e[0]?(this.x=e[0].Physical.x,this.y=e[0].Physical.y):(this.x=e[0].x,this.y=e[0].y):(this.x=e[0],this.y=e[1])}toLogical(e){return new T(this.x/e,this.y/e)}[o](){return{x:this.x,y:this.y}}toJSON(){return this[o]()}}class E{constructor(e){this.position=e}toLogical(e){return this.position instanceof T?this.position:this.position.toLogical(e)}toPhysical(e){return this.position instanceof I?this.position:this.position.toPhysical(e)}[o](){return{[`${this.position.type}`]:{x:this.position.x,y:this.position.y}}}toJSON(){return this[o]()}}var R,D=Object.freeze({__proto__:null,LogicalPosition:T,LogicalSize:v,PhysicalPosition:I,PhysicalSize:k,Position:E,Size:A});async function S(e,n){window.__TAURI_EVENT_PLUGIN_INTERNALS__.unregisterListener(e,n),await h("plugin:event|unlisten",{event:e,eventId:n})}async function N(e,n,t){var i;const r="string"==typeof(null==t?void 0:t.target)?{kind:"AnyLabel",label:t.target}:null!==(i=null==t?void 0:t.target)&&void 0!==i?i:{kind:"Any"};return h("plugin:event|listen",{event:e,target:r,handler:u(n)}).then(n=>async()=>S(e,n))}async function L(e,n,t){return N(e,t=>{S(e,t.id),n(t)},t)}async function C(e,n){await h("plugin:event|emit",{event:e,payload:n})}async function x(e,n,t){const i="string"==typeof e?{kind:"AnyLabel",label:e}:e;await h("plugin:event|emit_to",{target:i,event:n,payload:t})}!function(e){e.WINDOW_RESIZED="tauri://resize",e.WINDOW_MOVED="tauri://move",e.WINDOW_CLOSE_REQUESTED="tauri://close-requested",e.WINDOW_DESTROYED="tauri://destroyed",e.WINDOW_FOCUS="tauri://focus",e.WINDOW_BLUR="tauri://blur",e.WINDOW_SCALE_FACTOR_CHANGED="tauri://scale-change",e.WINDOW_THEME_CHANGED="tauri://theme-changed",e.WINDOW_CREATED="tauri://window-created",e.WEBVIEW_CREATED="tauri://webview-created",e.DRAG_ENTER="tauri://drag-enter",e.DRAG_OVER="tauri://drag-over",e.DRAG_DROP="tauri://drag-drop",e.DRAG_LEAVE="tauri://drag-leave"}(R||(R={}));var P,z,W,O=Object.freeze({__proto__:null,get TauriEvent(){return R},emit:C,emitTo:x,listen:N,once:L});function U(e){var n;if("items"in e)e.items=null===(n=e.items)||void 0===n?void 0:n.map(e=>"rid"in e?e:U(e));else if("action"in e&&e.action){const n=new c;return n.onmessage=e.action,delete e.action,{...e,handler:n}}return e}async function F(e,n){const t=new c;if(n&&"object"==typeof n&&("action"in n&&n.action&&(t.onmessage=n.action,delete n.action),"item"in n&&n.item&&"object"==typeof n.item&&"About"in n.item&&n.item.About&&"object"==typeof n.item.About&&"icon"in n.item.About&&n.item.About.icon&&(n.item.About.icon=g(n.item.About.icon)),"icon"in n&&n.icon&&(n.icon=g(n.icon)),"items"in n&&n.items)){function i(e){var n;return"rid"in e?[e.rid,e.kind]:("item"in e&&"object"==typeof e.item&&(null===(n=e.item.About)||void 0===n?void 0:n.icon)&&(e.item.About.icon=g(e.item.About.icon)),"icon"in e&&e.icon&&(e.icon=g(e.icon)),"items"in e&&e.items&&(e.items=e.items.map(i)),U(e))}n.items=n.items.map(i)}return h("plugin:menu|new",{kind:e,options:n,handler:t})}class M extends w{get id(){return n(this,P,"f")}get kind(){return n(this,z,"f")}constructor(e,n,i){super(e),P.set(this,void 0),z.set(this,void 0),t(this,P,n,"f"),t(this,z,i,"f")}}P=new WeakMap,z=new WeakMap;class B extends M{constructor(e,n){super(e,n,"MenuItem")}static async new(e){return F("MenuItem",e).then(([e,n])=>new B(e,n))}async text(){return h("plugin:menu|text",{rid:this.rid,kind:this.kind})}async setText(e){return h("plugin:menu|set_text",{rid:this.rid,kind:this.kind,text:e})}async isEnabled(){return h("plugin:menu|is_enabled",{rid:this.rid,kind:this.kind})}async setEnabled(e){return h("plugin:menu|set_enabled",{rid:this.rid,kind:this.kind,enabled:e})}async setAccelerator(e){return h("plugin:menu|set_accelerator",{rid:this.rid,kind:this.kind,accelerator:e})}}class V extends M{constructor(e,n){super(e,n,"Check")}static async new(e){return F("Check",e).then(([e,n])=>new V(e,n))}async text(){return h("plugin:menu|text",{rid:this.rid,kind:this.kind})}async setText(e){return h("plugin:menu|set_text",{rid:this.rid,kind:this.kind,text:e})}async isEnabled(){return h("plugin:menu|is_enabled",{rid:this.rid,kind:this.kind})}async setEnabled(e){return h("plugin:menu|set_enabled",{rid:this.rid,kind:this.kind,enabled:e})}async setAccelerator(e){return h("plugin:menu|set_accelerator",{rid:this.rid,kind:this.kind,accelerator:e})}async isChecked(){return h("plugin:menu|is_checked",{rid:this.rid})}async setChecked(e){return h("plugin:menu|set_checked",{rid:this.rid,checked:e})}}!function(e){e.Add="Add",e.Advanced="Advanced",e.Bluetooth="Bluetooth",e.Bookmarks="Bookmarks",e.Caution="Caution",e.ColorPanel="ColorPanel",e.ColumnView="ColumnView",e.Computer="Computer",e.EnterFullScreen="EnterFullScreen",e.Everyone="Everyone",e.ExitFullScreen="ExitFullScreen",e.FlowView="FlowView",e.Folder="Folder",e.FolderBurnable="FolderBurnable",e.FolderSmart="FolderSmart",e.FollowLinkFreestanding="FollowLinkFreestanding",e.FontPanel="FontPanel",e.GoLeft="GoLeft",e.GoRight="GoRight",e.Home="Home",e.IChatTheater="IChatTheater",e.IconView="IconView",e.Info="Info",e.InvalidDataFreestanding="InvalidDataFreestanding",e.LeftFacingTriangle="LeftFacingTriangle",e.ListView="ListView",e.LockLocked="LockLocked",e.LockUnlocked="LockUnlocked",e.MenuMixedState="MenuMixedState",e.MenuOnState="MenuOnState",e.MobileMe="MobileMe",e.MultipleDocuments="MultipleDocuments",e.Network="Network",e.Path="Path",e.PreferencesGeneral="PreferencesGeneral",e.QuickLook="QuickLook",e.RefreshFreestanding="RefreshFreestanding",e.Refresh="Refresh",e.Remove="Remove",e.RevealFreestanding="RevealFreestanding",e.RightFacingTriangle="RightFacingTriangle",e.Share="Share",e.Slideshow="Slideshow",e.SmartBadge="SmartBadge",e.StatusAvailable="StatusAvailable",e.StatusNone="StatusNone",e.StatusPartiallyAvailable="StatusPartiallyAvailable",e.StatusUnavailable="StatusUnavailable",e.StopProgressFreestanding="StopProgressFreestanding",e.StopProgress="StopProgress",e.TrashEmpty="TrashEmpty",e.TrashFull="TrashFull",e.User="User",e.UserAccounts="UserAccounts",e.UserGroup="UserGroup",e.UserGuest="UserGuest"}(W||(W={}));class G extends M{constructor(e,n){super(e,n,"Icon")}static async new(e){return F("Icon",e).then(([e,n])=>new G(e,n))}async text(){return h("plugin:menu|text",{rid:this.rid,kind:this.kind})}async setText(e){return h("plugin:menu|set_text",{rid:this.rid,kind:this.kind,text:e})}async isEnabled(){return h("plugin:menu|is_enabled",{rid:this.rid,kind:this.kind})}async setEnabled(e){return h("plugin:menu|set_enabled",{rid:this.rid,kind:this.kind,enabled:e})}async setAccelerator(e){return h("plugin:menu|set_accelerator",{rid:this.rid,kind:this.kind,accelerator:e})}async setIcon(e){return h("plugin:menu|set_icon",{rid:this.rid,kind:this.kind,icon:g(e)})}}class j extends M{constructor(e,n){super(e,n,"Predefined")}static async new(e){return F("Predefined",e).then(([e,n])=>new j(e,n))}async text(){return h("plugin:menu|text",{rid:this.rid,kind:this.kind})}async setText(e){return h("plugin:menu|set_text",{rid:this.rid,kind:this.kind,text:e})}}function H([e,n,t]){switch(t){case"Submenu":return new $(e,n);case"Predefined":return new j(e,n);case"Check":return new V(e,n);case"Icon":return new G(e,n);default:return new B(e,n)}}class $ extends M{constructor(e,n){super(e,n,"Submenu")}static async new(e){return F("Submenu",e).then(([e,n])=>new $(e,n))}async text(){return h("plugin:menu|text",{rid:this.rid,kind:this.kind})}async setText(e){return h("plugin:menu|set_text",{rid:this.rid,kind:this.kind,text:e})}async isEnabled(){return h("plugin:menu|is_enabled",{rid:this.rid,kind:this.kind})}async setEnabled(e){return h("plugin:menu|set_enabled",{rid:this.rid,kind:this.kind,enabled:e})}async append(e){return h("plugin:menu|append",{rid:this.rid,kind:this.kind,items:(Array.isArray(e)?e:[e]).map(e=>"rid"in e?[e.rid,e.kind]:e)})}async prepend(e){return h("plugin:menu|prepend",{rid:this.rid,kind:this.kind,items:(Array.isArray(e)?e:[e]).map(e=>"rid"in e?[e.rid,e.kind]:e)})}async insert(e,n){return h("plugin:menu|insert",{rid:this.rid,kind:this.kind,items:(Array.isArray(e)?e:[e]).map(e=>"rid"in e?[e.rid,e.kind]:e),position:n})}async remove(e){return h("plugin:menu|remove",{rid:this.rid,kind:this.kind,item:[e.rid,e.kind]})}async removeAt(e){return h("plugin:menu|remove_at",{rid:this.rid,kind:this.kind,position:e}).then(H)}async items(){return h("plugin:menu|items",{rid:this.rid,kind:this.kind}).then(e=>e.map(H))}async get(e){return h("plugin:menu|get",{rid:this.rid,kind:this.kind,id:e}).then(e=>e?H(e):null)}async popup(e,n){var t;return h("plugin:menu|popup",{rid:this.rid,kind:this.kind,window:null!==(t=null==n?void 0:n.label)&&void 0!==t?t:null,at:e instanceof E?e:e?new E(e):null})}async setAsWindowsMenuForNSApp(){return h("plugin:menu|set_as_windows_menu_for_nsapp",{rid:this.rid})}async setAsHelpMenuForNSApp(){return h("plugin:menu|set_as_help_menu_for_nsapp",{rid:this.rid})}async setIcon(e){return h("plugin:menu|set_icon",{rid:this.rid,kind:this.kind,icon:g(e)})}}class q extends M{constructor(e,n){super(e,n,"Menu")}static async new(e){return F("Menu",e).then(([e,n])=>new q(e,n))}static async default(){return h("plugin:menu|create_default").then(([e,n])=>new q(e,n))}async append(e){return h("plugin:menu|append",{rid:this.rid,kind:this.kind,items:(Array.isArray(e)?e:[e]).map(e=>"rid"in e?[e.rid,e.kind]:e)})}async prepend(e){return h("plugin:menu|prepend",{rid:this.rid,kind:this.kind,items:(Array.isArray(e)?e:[e]).map(e=>"rid"in e?[e.rid,e.kind]:e)})}async insert(e,n){return h("plugin:menu|insert",{rid:this.rid,kind:this.kind,items:(Array.isArray(e)?e:[e]).map(e=>"rid"in e?[e.rid,e.kind]:e),position:n})}async remove(e){return h("plugin:menu|remove",{rid:this.rid,kind:this.kind,item:[e.rid,e.kind]})}async removeAt(e){return h("plugin:menu|remove_at",{rid:this.rid,kind:this.kind,position:e}).then(H)}async items(){return h("plugin:menu|items",{rid:this.rid,kind:this.kind}).then(e=>e.map(H))}async get(e){return h("plugin:menu|get",{rid:this.rid,kind:this.kind,id:e}).then(e=>e?H(e):null)}async popup(e,n){var t;return h("plugin:menu|popup",{rid:this.rid,kind:this.kind,window:null!==(t=null==n?void 0:n.label)&&void 0!==t?t:null,at:e instanceof E?e:e?new E(e):null})}async setAsAppMenu(){return h("plugin:menu|set_as_app_menu",{rid:this.rid}).then(e=>e?new q(e[0],e[1]):null)}async setAsWindowMenu(e){var n;return h("plugin:menu|set_as_window_menu",{rid:this.rid,window:null!==(n=null==e?void 0:e.label)&&void 0!==n?n:null}).then(e=>e?new q(e[0],e[1]):null)}}var J=Object.freeze({__proto__:null,CheckMenuItem:V,IconMenuItem:G,Menu:q,MenuItem:B,get NativeIcon(){return W},PredefinedMenuItem:j,Submenu:$,itemFromKind:H});function Q(){var e,n;window.__TAURI_INTERNALS__=null!==(e=window.__TAURI_INTERNALS__)&&void 0!==e?e:{},window.__TAURI_EVENT_PLUGIN_INTERNALS__=null!==(n=window.__TAURI_EVENT_PLUGIN_INTERNALS__)&&void 0!==n?n:{}}var Z,K=Object.freeze({__proto__:null,clearMocks:function(){"object"==typeof window.__TAURI_INTERNALS__&&(delete window.__TAURI_INTERNALS__.invoke,delete window.__TAURI_INTERNALS__.transformCallback,delete window.__TAURI_INTERNALS__.unregisterCallback,delete window.__TAURI_INTERNALS__.runCallback,delete window.__TAURI_INTERNALS__.callbacks,delete window.__TAURI_INTERNALS__.convertFileSrc,delete window.__TAURI_INTERNALS__.metadata,"object"==typeof window.__TAURI_EVENT_PLUGIN_INTERNALS__&&delete window.__TAURI_EVENT_PLUGIN_INTERNALS__.unregisterListener)},mockConvertFileSrc:function(e){Q(),window.__TAURI_INTERNALS__.convertFileSrc=function(n,t="asset"){const i=encodeURIComponent(n);return"windows"===e?`http://${t}.localhost/${i}`:`${t}://localhost/${i}`}},mockIPC:function(e,n){function t(e,n){switch(e){case"plugin:event|listen":return function(e){i.has(e.event)||i.set(e.event,[]);return i.get(e.event).push(e.handler),e.handler}(n);case"plugin:event|emit":return function(e){const n=i.get(e.event)||[];for(const t of n)a(t,e);return null}(n);case"plugin:event|unlisten":return function(e){const n=i.get(e.event);if(n){const t=n.indexOf(e.id);-1!==t&&n.splice(t,1)}}(n)}}Q();const i=new Map,r=new Map;function s(e){r.delete(e)}function a(e,n){const t=r.get(e);t?t(n):console.warn(`[TAURI] Couldn't find callback id ${e}. This might happen when the app is reloaded while Rust is running an asynchronous operation.`)}window.__TAURI_INTERNALS__.invoke=async function(i,r,s){return(null==n?void 0:n.shouldMockEvents)&&function(e){return e.startsWith("plugin:event|")}(i)?t(i,r):e(i,r)},window.__TAURI_INTERNALS__.transformCallback=function(e,n=!1){const t=window.crypto.getRandomValues(new Uint32Array(1))[0];return r.set(t,i=>(n&&s(t),e&&e(i))),t},window.__TAURI_INTERNALS__.unregisterCallback=s,window.__TAURI_INTERNALS__.runCallback=a,window.__TAURI_INTERNALS__.callbacks=r,window.__TAURI_EVENT_PLUGIN_INTERNALS__.unregisterListener=function(e,n){s(n)}},mockWindows:function(e,...n){Q(),window.__TAURI_INTERNALS__.metadata={currentWindow:{label:e},currentWebview:{windowLabel:e,label:e}}}});!function(e){e[e.Audio=1]="Audio",e[e.Cache=2]="Cache",e[e.Config=3]="Config",e[e.Data=4]="Data",e[e.LocalData=5]="LocalData",e[e.Document=6]="Document",e[e.Download=7]="Download",e[e.Picture=8]="Picture",e[e.Public=9]="Public",e[e.Video=10]="Video",e[e.Resource=11]="Resource",e[e.Temp=12]="Temp",e[e.AppConfig=13]="AppConfig",e[e.AppData=14]="AppData",e[e.AppLocalData=15]="AppLocalData",e[e.AppCache=16]="AppCache",e[e.AppLog=17]="AppLog",e[e.Desktop=18]="Desktop",e[e.Executable=19]="Executable",e[e.Font=20]="Font",e[e.Home=21]="Home",e[e.Runtime=22]="Runtime",e[e.Template=23]="Template"}(Z||(Z={}));var Y=Object.freeze({__proto__:null,get BaseDirectory(){return Z},appCacheDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.AppCache})},appConfigDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.AppConfig})},appDataDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.AppData})},appLocalDataDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.AppLocalData})},appLogDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.AppLog})},audioDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Audio})},basename:async function(e,n){return h("plugin:path|basename",{path:e,ext:n})},cacheDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Cache})},configDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Config})},dataDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Data})},delimiter:function(){return window.__TAURI_INTERNALS__.plugins.path.delimiter},desktopDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Desktop})},dirname:async function(e){return h("plugin:path|dirname",{path:e})},documentDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Document})},downloadDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Download})},executableDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Executable})},extname:async function(e){return h("plugin:path|extname",{path:e})},fontDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Font})},homeDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Home})},isAbsolute:async function(e){return h("plugin:path|is_absolute",{path:e})},join:async function(...e){return h("plugin:path|join",{paths:e})},localDataDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.LocalData})},normalize:async function(e){return h("plugin:path|normalize",{path:e})},pictureDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Picture})},publicDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Public})},resolve:async function(...e){return h("plugin:path|resolve",{paths:e})},resolveResource:async function(e){return h("plugin:path|resolve_directory",{directory:Z.Resource,path:e})},resourceDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Resource})},runtimeDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Runtime})},sep:function(){return window.__TAURI_INTERNALS__.plugins.path.sep},tempDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Temp})},templateDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Template})},videoDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Video})}});class X extends w{constructor(e,n){super(e),this.id=n}static async getById(e){return h("plugin:tray|get_by_id",{id:e}).then(n=>n?new X(n,e):null)}static async removeById(e){return h("plugin:tray|remove_by_id",{id:e})}static async new(e){(null==e?void 0:e.menu)&&(e.menu=[e.menu.rid,e.menu.kind]),(null==e?void 0:e.icon)&&(e.icon=g(e.icon));const n=new c;if(null==e?void 0:e.action){const t=e.action;n.onmessage=e=>t(function(e){const n=e;return n.position=new I(e.position),n.rect.position=new I(e.rect.position),n.rect.size=new k(e.rect.size),n}(e)),delete e.action}return h("plugin:tray|new",{options:null!=e?e:{},handler:n}).then(([e,n])=>new X(e,n))}async setIcon(e){let n=null;return e&&(n=g(e)),h("plugin:tray|set_icon",{rid:this.rid,icon:n})}async setMenu(e){return e&&(e=[e.rid,e.kind]),h("plugin:tray|set_menu",{rid:this.rid,menu:e})}async setTooltip(e){return h("plugin:tray|set_tooltip",{rid:this.rid,tooltip:e})}async setTitle(e){return h("plugin:tray|set_title",{rid:this.rid,title:e})}async setVisible(e){return h("plugin:tray|set_visible",{rid:this.rid,visible:e})}async setTempDirPath(e){return h("plugin:tray|set_temp_dir_path",{rid:this.rid,path:e})}async setIconAsTemplate(e){return h("plugin:tray|set_icon_as_template",{rid:this.rid,asTemplate:e})}async setMenuOnLeftClick(e){return h("plugin:tray|set_show_menu_on_left_click",{rid:this.rid,onLeft:e})}async setShowMenuOnLeftClick(e){return h("plugin:tray|set_show_menu_on_left_click",{rid:this.rid,onLeft:e})}}var ee,ne,te=Object.freeze({__proto__:null,TrayIcon:X});!function(e){e[e.Critical=1]="Critical",e[e.Informational=2]="Informational"}(ee||(ee={}));class ie{constructor(e){this._preventDefault=!1,this.event=e.event,this.id=e.id}preventDefault(){this._preventDefault=!0}isPreventDefault(){return this._preventDefault}}function re(){return new le(window.__TAURI_INTERNALS__.metadata.currentWindow.label,{skip:!0})}async function se(){return h("plugin:window|get_all_windows").then(e=>e.map(e=>new le(e,{skip:!0})))}!function(e){e.None="none",e.Normal="normal",e.Indeterminate="indeterminate",e.Paused="paused",e.Error="error"}(ne||(ne={}));const ae=["tauri://created","tauri://error"];class le{constructor(e,n={}){var t;this.label=e,this.listeners=Object.create(null),(null==n?void 0:n.skip)||h("plugin:window|create",{options:{...n,parent:"string"==typeof n.parent?n.parent:null===(t=n.parent)||void 0===t?void 0:t.label,label:e}}).then(async()=>this.emit("tauri://created")).catch(async e=>this.emit("tauri://error",e))}static async getByLabel(e){var n;return null!==(n=(await se()).find(n=>n.label===e))&&void 0!==n?n:null}static getCurrent(){return re()}static async getAll(){return se()}static async getFocusedWindow(){for(const e of await se())if(await e.isFocused())return e;return null}async listen(e,n){return this._handleTauriEvent(e,n)?()=>{const t=this.listeners[e];t.splice(t.indexOf(n),1)}:N(e,n,{target:{kind:"Window",label:this.label}})}async once(e,n){return this._handleTauriEvent(e,n)?()=>{const t=this.listeners[e];t.splice(t.indexOf(n),1)}:L(e,n,{target:{kind:"Window",label:this.label}})}async emit(e,n){if(!ae.includes(e))return C(e,n);for(const t of this.listeners[e]||[])t({event:e,id:-1,payload:n})}async emitTo(e,n,t){if(!ae.includes(n))return x(e,n,t);for(const e of this.listeners[n]||[])e({event:n,id:-1,payload:t})}_handleTauriEvent(e,n){return!!ae.includes(e)&&(e in this.listeners?this.listeners[e].push(n):this.listeners[e]=[n],!0)}async scaleFactor(){return h("plugin:window|scale_factor",{label:this.label})}async innerPosition(){return h("plugin:window|inner_position",{label:this.label}).then(e=>new I(e))}async outerPosition(){return h("plugin:window|outer_position",{label:this.label}).then(e=>new I(e))}async innerSize(){return h("plugin:window|inner_size",{label:this.label}).then(e=>new k(e))}async outerSize(){return h("plugin:window|outer_size",{label:this.label}).then(e=>new k(e))}async isFullscreen(){return h("plugin:window|is_fullscreen",{label:this.label})}async isMinimized(){return h("plugin:window|is_minimized",{label:this.label})}async isMaximized(){return h("plugin:window|is_maximized",{label:this.label})}async isFocused(){return h("plugin:window|is_focused",{label:this.label})}async isDecorated(){return h("plugin:window|is_decorated",{label:this.label})}async isResizable(){return h("plugin:window|is_resizable",{label:this.label})}async isMaximizable(){return h("plugin:window|is_maximizable",{label:this.label})}async isMinimizable(){return h("plugin:window|is_minimizable",{label:this.label})}async isClosable(){return h("plugin:window|is_closable",{label:this.label})}async isVisible(){return h("plugin:window|is_visible",{label:this.label})}async title(){return h("plugin:window|title",{label:this.label})}async theme(){return h("plugin:window|theme",{label:this.label})}async isAlwaysOnTop(){return h("plugin:window|is_always_on_top",{label:this.label})}async center(){return h("plugin:window|center",{label:this.label})}async requestUserAttention(e){let n=null;return e&&(n=e===ee.Critical?{type:"Critical"}:{type:"Informational"}),h("plugin:window|request_user_attention",{label:this.label,value:n})}async setResizable(e){return h("plugin:window|set_resizable",{label:this.label,value:e})}async setEnabled(e){return h("plugin:window|set_enabled",{label:this.label,value:e})}async isEnabled(){return h("plugin:window|is_enabled",{label:this.label})}async setMaximizable(e){return h("plugin:window|set_maximizable",{label:this.label,value:e})}async setMinimizable(e){return h("plugin:window|set_minimizable",{label:this.label,value:e})}async setClosable(e){return h("plugin:window|set_closable",{label:this.label,value:e})}async setTitle(e){return h("plugin:window|set_title",{label:this.label,value:e})}async maximize(){return h("plugin:window|maximize",{label:this.label})}async unmaximize(){return h("plugin:window|unmaximize",{label:this.label})}async toggleMaximize(){return h("plugin:window|toggle_maximize",{label:this.label})}async minimize(){return h("plugin:window|minimize",{label:this.label})}async unminimize(){return h("plugin:window|unminimize",{label:this.label})}async show(){return h("plugin:window|show",{label:this.label})}async hide(){return h("plugin:window|hide",{label:this.label})}async close(){return h("plugin:window|close",{label:this.label})}async destroy(){return h("plugin:window|destroy",{label:this.label})}async setDecorations(e){return h("plugin:window|set_decorations",{label:this.label,value:e})}async setShadow(e){return h("plugin:window|set_shadow",{label:this.label,value:e})}async setEffects(e){return h("plugin:window|set_effects",{label:this.label,value:e})}async clearEffects(){return h("plugin:window|set_effects",{label:this.label,value:null})}async setAlwaysOnTop(e){return h("plugin:window|set_always_on_top",{label:this.label,value:e})}async setAlwaysOnBottom(e){return h("plugin:window|set_always_on_bottom",{label:this.label,value:e})}async setContentProtected(e){return h("plugin:window|set_content_protected",{label:this.label,value:e})}async setSize(e){return h("plugin:window|set_size",{label:this.label,value:e instanceof A?e:new A(e)})}async setMinSize(e){return h("plugin:window|set_min_size",{label:this.label,value:e instanceof A?e:e?new A(e):null})}async setMaxSize(e){return h("plugin:window|set_max_size",{label:this.label,value:e instanceof A?e:e?new A(e):null})}async setSizeConstraints(e){function n(e){return e?{Logical:e}:null}return h("plugin:window|set_size_constraints",{label:this.label,value:{minWidth:n(null==e?void 0:e.minWidth),minHeight:n(null==e?void 0:e.minHeight),maxWidth:n(null==e?void 0:e.maxWidth),maxHeight:n(null==e?void 0:e.maxHeight)}})}async setPosition(e){return h("plugin:window|set_position",{label:this.label,value:e instanceof E?e:new E(e)})}async setFullscreen(e){return h("plugin:window|set_fullscreen",{label:this.label,value:e})}async setSimpleFullscreen(e){return h("plugin:window|set_simple_fullscreen",{label:this.label,value:e})}async setFocus(){return h("plugin:window|set_focus",{label:this.label})}async setFocusable(e){return h("plugin:window|set_focusable",{label:this.label,value:e})}async setIcon(e){return h("plugin:window|set_icon",{label:this.label,value:g(e)})}async setSkipTaskbar(e){return h("plugin:window|set_skip_taskbar",{label:this.label,value:e})}async setCursorGrab(e){return h("plugin:window|set_cursor_grab",{label:this.label,value:e})}async setCursorVisible(e){return h("plugin:window|set_cursor_visible",{label:this.label,value:e})}async setCursorIcon(e){return h("plugin:window|set_cursor_icon",{label:this.label,value:e})}async setBackgroundColor(e){return h("plugin:window|set_background_color",{color:e})}async setCursorPosition(e){return h("plugin:window|set_cursor_position",{label:this.label,value:e instanceof E?e:new E(e)})}async setIgnoreCursorEvents(e){return h("plugin:window|set_ignore_cursor_events",{label:this.label,value:e})}async startDragging(){return h("plugin:window|start_dragging",{label:this.label})}async startResizeDragging(e){return h("plugin:window|start_resize_dragging",{label:this.label,value:e})}async setBadgeCount(e){return h("plugin:window|set_badge_count",{label:this.label,value:e})}async setBadgeLabel(e){return h("plugin:window|set_badge_label",{label:this.label,value:e})}async setOverlayIcon(e){return h("plugin:window|set_overlay_icon",{label:this.label,value:e?g(e):void 0})}async setProgressBar(e){return h("plugin:window|set_progress_bar",{label:this.label,value:e})}async setVisibleOnAllWorkspaces(e){return h("plugin:window|set_visible_on_all_workspaces",{label:this.label,value:e})}async setTitleBarStyle(e){return h("plugin:window|set_title_bar_style",{label:this.label,value:e})}async setTheme(e){return h("plugin:window|set_theme",{label:this.label,value:e})}async onResized(e){return this.listen(R.WINDOW_RESIZED,n=>{n.payload=new k(n.payload),e(n)})}async onMoved(e){return this.listen(R.WINDOW_MOVED,n=>{n.payload=new I(n.payload),e(n)})}async onCloseRequested(e){return this.listen(R.WINDOW_CLOSE_REQUESTED,async n=>{const t=new ie(n);await e(t),t.isPreventDefault()||await this.destroy()})}async onDragDropEvent(e){const n=await this.listen(R.DRAG_ENTER,n=>{e({...n,payload:{type:"enter",paths:n.payload.paths,position:new I(n.payload.position)}})}),t=await this.listen(R.DRAG_OVER,n=>{e({...n,payload:{type:"over",position:new I(n.payload.position)}})}),i=await this.listen(R.DRAG_DROP,n=>{e({...n,payload:{type:"drop",paths:n.payload.paths,position:new I(n.payload.position)}})}),r=await this.listen(R.DRAG_LEAVE,n=>{e({...n,payload:{type:"leave"}})});return()=>{n(),i(),t(),r()}}async onFocusChanged(e){const n=await this.listen(R.WINDOW_FOCUS,n=>{e({...n,payload:!0})}),t=await this.listen(R.WINDOW_BLUR,n=>{e({...n,payload:!1})});return()=>{n(),t()}}async onScaleChanged(e){return this.listen(R.WINDOW_SCALE_FACTOR_CHANGED,e)}async onThemeChanged(e){return this.listen(R.WINDOW_THEME_CHANGED,e)}}var oe,ue,ce,de;function pe(e){return null===e?null:{name:e.name,scaleFactor:e.scaleFactor,position:new I(e.position),size:new k(e.size),workArea:{position:new I(e.workArea.position),size:new k(e.workArea.size)}}}!function(e){e.Disabled="disabled",e.Throttle="throttle",e.Suspend="suspend"}(oe||(oe={})),function(e){e.Default="default",e.FluentOverlay="fluentOverlay"}(ue||(ue={})),function(e){e.AppearanceBased="appearanceBased",e.Light="light",e.Dark="dark",e.MediumLight="mediumLight",e.UltraDark="ultraDark",e.Titlebar="titlebar",e.Selection="selection",e.Menu="menu",e.Popover="popover",e.Sidebar="sidebar",e.HeaderView="headerView",e.Sheet="sheet",e.WindowBackground="windowBackground",e.HudWindow="hudWindow",e.FullScreenUI="fullScreenUI",e.Tooltip="tooltip",e.ContentBackground="contentBackground",e.UnderWindowBackground="underWindowBackground",e.UnderPageBackground="underPageBackground",e.Mica="mica",e.Blur="blur",e.Acrylic="acrylic",e.Tabbed="tabbed",e.TabbedDark="tabbedDark",e.TabbedLight="tabbedLight"}(ce||(ce={})),function(e){e.FollowsWindowActiveState="followsWindowActiveState",e.Active="active",e.Inactive="inactive"}(de||(de={}));var he=Object.freeze({__proto__:null,CloseRequestedEvent:ie,get Effect(){return ce},get EffectState(){return de},LogicalPosition:T,LogicalSize:v,PhysicalPosition:I,PhysicalSize:k,get ProgressBarStatus(){return ne},get UserAttentionType(){return ee},Window:le,availableMonitors:async function(){return h("plugin:window|available_monitors").then(e=>e.map(pe))},currentMonitor:async function(){return h("plugin:window|current_monitor").then(pe)},cursorPosition:async function(){return h("plugin:window|cursor_position").then(e=>new I(e))},getAllWindows:se,getCurrentWindow:re,monitorFromPoint:async function(e,n){return h("plugin:window|monitor_from_point",{x:e,y:n}).then(pe)},primaryMonitor:async function(){return h("plugin:window|primary_monitor").then(pe)}});function we(){return new ge(re(),window.__TAURI_INTERNALS__.metadata.currentWebview.label,{skip:!0})}async function _e(){return h("plugin:webview|get_all_webviews").then(e=>e.map(e=>new ge(new le(e.windowLabel,{skip:!0}),e.label,{skip:!0})))}const ye=["tauri://created","tauri://error"];class ge{constructor(e,n,t){this.window=e,this.label=n,this.listeners=Object.create(null),(null==t?void 0:t.skip)||h("plugin:webview|create_webview",{windowLabel:e.label,options:{...t,label:n}}).then(async()=>this.emit("tauri://created")).catch(async e=>this.emit("tauri://error",e))}static async getByLabel(e){var n;return null!==(n=(await _e()).find(n=>n.label===e))&&void 0!==n?n:null}static getCurrent(){return we()}static async getAll(){return _e()}async listen(e,n){return this._handleTauriEvent(e,n)?()=>{const t=this.listeners[e];t.splice(t.indexOf(n),1)}:N(e,n,{target:{kind:"Webview",label:this.label}})}async once(e,n){return this._handleTauriEvent(e,n)?()=>{const t=this.listeners[e];t.splice(t.indexOf(n),1)}:L(e,n,{target:{kind:"Webview",label:this.label}})}async emit(e,n){if(!ye.includes(e))return C(e,n);for(const t of this.listeners[e]||[])t({event:e,id:-1,payload:n})}async emitTo(e,n,t){if(!ye.includes(n))return x(e,n,t);for(const e of this.listeners[n]||[])e({event:n,id:-1,payload:t})}_handleTauriEvent(e,n){return!!ye.includes(e)&&(e in this.listeners?this.listeners[e].push(n):this.listeners[e]=[n],!0)}async position(){return h("plugin:webview|webview_position",{label:this.label}).then(e=>new I(e))}async size(){return h("plugin:webview|webview_size",{label:this.label}).then(e=>new k(e))}async close(){return h("plugin:webview|webview_close",{label:this.label})}async setSize(e){return h("plugin:webview|set_webview_size",{label:this.label,value:e instanceof A?e:new A(e)})}async setPosition(e){return h("plugin:webview|set_webview_position",{label:this.label,value:e instanceof E?e:new E(e)})}async setFocus(){return h("plugin:webview|set_webview_focus",{label:this.label})}async setAutoResize(e){return h("plugin:webview|set_webview_auto_resize",{label:this.label,value:e})}async hide(){return h("plugin:webview|webview_hide",{label:this.label})}async show(){return h("plugin:webview|webview_show",{label:this.label})}async setZoom(e){return h("plugin:webview|set_webview_zoom",{label:this.label,value:e})}async reparent(e){return h("plugin:webview|reparent",{label:this.label,window:"string"==typeof e?e:e.label})}async clearAllBrowsingData(){return h("plugin:webview|clear_all_browsing_data")}async setBackgroundColor(e){return h("plugin:webview|set_webview_background_color",{color:e})}async onDragDropEvent(e){const n=await this.listen(R.DRAG_ENTER,n=>{e({...n,payload:{type:"enter",paths:n.payload.paths,position:new I(n.payload.position)}})}),t=await this.listen(R.DRAG_OVER,n=>{e({...n,payload:{type:"over",position:new I(n.payload.position)}})}),i=await this.listen(R.DRAG_DROP,n=>{e({...n,payload:{type:"drop",paths:n.payload.paths,position:new I(n.payload.position)}})}),r=await this.listen(R.DRAG_LEAVE,n=>{e({...n,payload:{type:"leave"}})});return()=>{n(),i(),t(),r()}}}var be,me,fe=Object.freeze({__proto__:null,Webview:ge,getAllWebviews:_e,getCurrentWebview:we});function ve(){const e=we();return new Ae(e.label,{skip:!0})}async function ke(){return h("plugin:window|get_all_windows").then(e=>e.map(e=>new Ae(e,{skip:!0})))}class Ae{constructor(e,n={}){var t;this.label=e,this.listeners=Object.create(null),(null==n?void 0:n.skip)||h("plugin:webview|create_webview_window",{options:{...n,parent:"string"==typeof n.parent?n.parent:null===(t=n.parent)||void 0===t?void 0:t.label,label:e}}).then(async()=>this.emit("tauri://created")).catch(async e=>this.emit("tauri://error",e))}static async getByLabel(e){var n;const t=null!==(n=(await ke()).find(n=>n.label===e))&&void 0!==n?n:null;return t?new Ae(t.label,{skip:!0}):null}static getCurrent(){return ve()}static async getAll(){return ke()}async listen(e,n){return this._handleTauriEvent(e,n)?()=>{const t=this.listeners[e];t.splice(t.indexOf(n),1)}:N(e,n,{target:{kind:"WebviewWindow",label:this.label}})}async once(e,n){return this._handleTauriEvent(e,n)?()=>{const t=this.listeners[e];t.splice(t.indexOf(n),1)}:L(e,n,{target:{kind:"WebviewWindow",label:this.label}})}async setBackgroundColor(e){return h("plugin:window|set_background_color",{color:e}).then(()=>h("plugin:webview|set_webview_background_color",{color:e}))}}be=Ae,me=[le,ge],(Array.isArray(me)?me:[me]).forEach(e=>{Object.getOwnPropertyNames(e.prototype).forEach(n=>{var t;"object"==typeof be.prototype&&be.prototype&&n in be.prototype||Object.defineProperty(be.prototype,n,null!==(t=Object.getOwnPropertyDescriptor(e.prototype,n))&&void 0!==t?t:Object.create(null))})});var Te=Object.freeze({__proto__:null,WebviewWindow:Ae,getAllWebviewWindows:ke,getCurrentWebviewWindow:ve});return e.app=f,e.core=_,e.dpi=D,e.event=O,e.image=m,e.menu=J,e.mocks=K,e.path=Y,e.tray=te,e.webview=fe,e.webviewWindow=Te,e.window=he,e}({});window.__TAURI__=__TAURI_IIFE__; +var __TAURI_IIFE__=function(e){"use strict";function n(e,n,t,i){if("a"===t&&!i)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof n?e!==n||!i:!n.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===t?i:"a"===t?i.call(e):i?i.value:n.get(e)}function t(e,n,t,i,r){if("m"===i)throw new TypeError("Private method is not writable");if("a"===i&&!r)throw new TypeError("Private accessor was defined without a setter");if("function"==typeof n?e!==n||!r:!n.has(e))throw new TypeError("Cannot write private member to an object whose class did not declare it");return"a"===i?r.call(e,t):r?r.value=t:n.set(e,t),t}var i,r,s,a,l;"function"==typeof SuppressedError&&SuppressedError;const o="__TAURI_TO_IPC_KEY__";function u(e,n=!1){return window.__TAURI_INTERNALS__.transformCallback(e,n)}class c{constructor(e){i.set(this,void 0),r.set(this,0),s.set(this,[]),a.set(this,void 0),t(this,i,e||(()=>{}),"f"),this.id=u(e=>{const l=e.index;if("end"in e)return void(l==n(this,r,"f")?this.cleanupCallback():t(this,a,l,"f"));const o=e.message;if(l==n(this,r,"f")){for(n(this,i,"f").call(this,o),t(this,r,n(this,r,"f")+1,"f");n(this,r,"f")in n(this,s,"f");){const e=n(this,s,"f")[n(this,r,"f")];n(this,i,"f").call(this,e),delete n(this,s,"f")[n(this,r,"f")],t(this,r,n(this,r,"f")+1,"f")}n(this,r,"f")===n(this,a,"f")&&this.cleanupCallback()}else n(this,s,"f")[l]=o})}cleanupCallback(){window.__TAURI_INTERNALS__.unregisterCallback(this.id)}set onmessage(e){t(this,i,e,"f")}get onmessage(){return n(this,i,"f")}[(i=new WeakMap,r=new WeakMap,s=new WeakMap,a=new WeakMap,o)](){return`__CHANNEL__:${this.id}`}toJSON(){return this[o]()}}class d{constructor(e,n,t){this.plugin=e,this.event=n,this.channelId=t}async unregister(){return h(`plugin:${this.plugin}|remove_listener`,{event:this.event,channelId:this.channelId})}}async function p(e,n,t){const i=new c(t);try{return await h(`plugin:${e}|register_listener`,{event:n,handler:i}),new d(e,n,i.id)}catch{return await h(`plugin:${e}|registerListener`,{event:n,handler:i}),new d(e,n,i.id)}}async function h(e,n={},t){return window.__TAURI_INTERNALS__.invoke(e,n,t)}class w{get rid(){return n(this,l,"f")}constructor(e){l.set(this,void 0),t(this,l,e,"f")}async close(){return h("plugin:resources|close",{rid:this.rid})}}l=new WeakMap;var _=Object.freeze({__proto__:null,Channel:c,PluginListener:d,Resource:w,SERIALIZE_TO_IPC_FN:o,addPluginListener:p,checkPermissions:async function(e){return h(`plugin:${e}|check_permissions`)},convertFileSrc:function(e,n="asset"){return window.__TAURI_INTERNALS__.convertFileSrc(e,n)},invoke:h,isTauri:function(){return!!(globalThis||window).isTauri},requestPermissions:async function(e){return h(`plugin:${e}|request_permissions`)},transformCallback:u});class y extends w{constructor(e){super(e)}static async new(e,n,t){return h("plugin:image|new",{rgba:g(e),width:n,height:t}).then(e=>new y(e))}static async fromBytes(e){return h("plugin:image|from_bytes",{bytes:g(e)}).then(e=>new y(e))}static async fromPath(e){return h("plugin:image|from_path",{path:e}).then(e=>new y(e))}async rgba(){return h("plugin:image|rgba",{rid:this.rid}).then(e=>new Uint8Array(e))}async size(){return h("plugin:image|size",{rid:this.rid})}}function g(e){return null==e?null:"string"==typeof e?e:e instanceof y?e.rid:e}var b,m=Object.freeze({__proto__:null,Image:y,transformImage:g});!function(e){e.Nsis="nsis",e.Msi="msi",e.Deb="deb",e.Rpm="rpm",e.AppImage="appimage",e.App="app"}(b||(b={}));var f=Object.freeze({__proto__:null,get BundleType(){return b},defaultWindowIcon:async function(){return h("plugin:app|default_window_icon").then(e=>e?new y(e):null)},fetchDataStoreIdentifiers:async function(){return h("plugin:app|fetch_data_store_identifiers")},getBundleType:async function(){return h("plugin:app|bundle_type")},getIdentifier:async function(){return h("plugin:app|identifier")},getName:async function(){return h("plugin:app|name")},getTauriVersion:async function(){return h("plugin:app|tauri_version")},getVersion:async function(){return h("plugin:app|version")},hide:async function(){return h("plugin:app|app_hide")},onBackButtonPress:async function(e){return p("app","back-button",e)},removeDataStore:async function(e){return h("plugin:app|remove_data_store",{uuid:e})},setDockVisibility:async function(e){return h("plugin:app|set_dock_visibility",{visible:e})},setTheme:async function(e){return h("plugin:app|set_app_theme",{theme:e})},show:async function(){return h("plugin:app|app_show")},supportsMultipleWindows:async function(){return h("plugin:app|supports_multiple_windows")}});class v{constructor(...e){this.type="Logical",1===e.length?"Logical"in e[0]?(this.width=e[0].Logical.width,this.height=e[0].Logical.height):(this.width=e[0].width,this.height=e[0].height):(this.width=e[0],this.height=e[1])}toPhysical(e){return new k(this.width*e,this.height*e)}[o](){return{width:this.width,height:this.height}}toJSON(){return this[o]()}}class k{constructor(...e){this.type="Physical",1===e.length?"Physical"in e[0]?(this.width=e[0].Physical.width,this.height=e[0].Physical.height):(this.width=e[0].width,this.height=e[0].height):(this.width=e[0],this.height=e[1])}toLogical(e){return new v(this.width/e,this.height/e)}[o](){return{width:this.width,height:this.height}}toJSON(){return this[o]()}}class A{constructor(e){this.size=e}toLogical(e){return this.size instanceof v?this.size:this.size.toLogical(e)}toPhysical(e){return this.size instanceof k?this.size:this.size.toPhysical(e)}[o](){return{[`${this.size.type}`]:{width:this.size.width,height:this.size.height}}}toJSON(){return this[o]()}}class T{constructor(...e){this.type="Logical",1===e.length?"Logical"in e[0]?(this.x=e[0].Logical.x,this.y=e[0].Logical.y):(this.x=e[0].x,this.y=e[0].y):(this.x=e[0],this.y=e[1])}toPhysical(e){return new I(this.x*e,this.y*e)}[o](){return{x:this.x,y:this.y}}toJSON(){return this[o]()}}class I{constructor(...e){this.type="Physical",1===e.length?"Physical"in e[0]?(this.x=e[0].Physical.x,this.y=e[0].Physical.y):(this.x=e[0].x,this.y=e[0].y):(this.x=e[0],this.y=e[1])}toLogical(e){return new T(this.x/e,this.y/e)}[o](){return{x:this.x,y:this.y}}toJSON(){return this[o]()}}class E{constructor(e){this.position=e}toLogical(e){return this.position instanceof T?this.position:this.position.toLogical(e)}toPhysical(e){return this.position instanceof I?this.position:this.position.toPhysical(e)}[o](){return{[`${this.position.type}`]:{x:this.position.x,y:this.position.y}}}toJSON(){return this[o]()}}var R,D=Object.freeze({__proto__:null,LogicalPosition:T,LogicalSize:v,PhysicalPosition:I,PhysicalSize:k,Position:E,Size:A});async function S(e,n){window.__TAURI_EVENT_PLUGIN_INTERNALS__.unregisterListener(e,n),await h("plugin:event|unlisten",{event:e,eventId:n})}async function N(e,n,t){var i;const r="string"==typeof(null==t?void 0:t.target)?{kind:"AnyLabel",label:t.target}:null!==(i=null==t?void 0:t.target)&&void 0!==i?i:{kind:"Any"};return h("plugin:event|listen",{event:e,target:r,handler:u(n)}).then(n=>async()=>S(e,n))}async function L(e,n,t){return N(e,t=>{S(e,t.id),n(t)},t)}async function C(e,n){await h("plugin:event|emit",{event:e,payload:n})}async function x(e,n,t){const i="string"==typeof e?{kind:"AnyLabel",label:e}:e;await h("plugin:event|emit_to",{target:i,event:n,payload:t})}!function(e){e.WINDOW_RESIZED="tauri://resize",e.WINDOW_MOVED="tauri://move",e.WINDOW_CLOSE_REQUESTED="tauri://close-requested",e.WINDOW_DESTROYED="tauri://destroyed",e.WINDOW_FOCUS="tauri://focus",e.WINDOW_BLUR="tauri://blur",e.WINDOW_SCALE_FACTOR_CHANGED="tauri://scale-change",e.WINDOW_THEME_CHANGED="tauri://theme-changed",e.WINDOW_CREATED="tauri://window-created",e.WINDOW_SUSPENDED="tauri://suspended",e.WINDOW_RESUMED="tauri://resumed",e.WEBVIEW_CREATED="tauri://webview-created",e.DRAG_ENTER="tauri://drag-enter",e.DRAG_OVER="tauri://drag-over",e.DRAG_DROP="tauri://drag-drop",e.DRAG_LEAVE="tauri://drag-leave"}(R||(R={}));var W,P,z,O=Object.freeze({__proto__:null,get TauriEvent(){return R},emit:C,emitTo:x,listen:N,once:L});function U(e){var n;if("items"in e)e.items=null===(n=e.items)||void 0===n?void 0:n.map(e=>"rid"in e?e:U(e));else if("action"in e&&e.action){const n=new c;return n.onmessage=e.action,delete e.action,{...e,handler:n}}return e}async function F(e,n){const t=new c;if(n&&"object"==typeof n&&("action"in n&&n.action&&(t.onmessage=n.action,delete n.action),"item"in n&&n.item&&"object"==typeof n.item&&"About"in n.item&&n.item.About&&"object"==typeof n.item.About&&"icon"in n.item.About&&n.item.About.icon&&(n.item.About.icon=g(n.item.About.icon)),"icon"in n&&n.icon&&(n.icon=g(n.icon)),"items"in n&&n.items)){function i(e){var n;return"rid"in e?[e.rid,e.kind]:("item"in e&&"object"==typeof e.item&&(null===(n=e.item.About)||void 0===n?void 0:n.icon)&&(e.item.About.icon=g(e.item.About.icon)),"icon"in e&&e.icon&&(e.icon=g(e.icon)),"items"in e&&e.items&&(e.items=e.items.map(i)),U(e))}n.items=n.items.map(i)}return h("plugin:menu|new",{kind:e,options:n,handler:t})}class M extends w{get id(){return n(this,W,"f")}get kind(){return n(this,P,"f")}constructor(e,n,i){super(e),W.set(this,void 0),P.set(this,void 0),t(this,W,n,"f"),t(this,P,i,"f")}}W=new WeakMap,P=new WeakMap;class B extends M{constructor(e,n){super(e,n,"MenuItem")}static async new(e){return F("MenuItem",e).then(([e,n])=>new B(e,n))}async text(){return h("plugin:menu|text",{rid:this.rid,kind:this.kind})}async setText(e){return h("plugin:menu|set_text",{rid:this.rid,kind:this.kind,text:e})}async isEnabled(){return h("plugin:menu|is_enabled",{rid:this.rid,kind:this.kind})}async setEnabled(e){return h("plugin:menu|set_enabled",{rid:this.rid,kind:this.kind,enabled:e})}async setAccelerator(e){return h("plugin:menu|set_accelerator",{rid:this.rid,kind:this.kind,accelerator:e})}}class V extends M{constructor(e,n){super(e,n,"Check")}static async new(e){return F("Check",e).then(([e,n])=>new V(e,n))}async text(){return h("plugin:menu|text",{rid:this.rid,kind:this.kind})}async setText(e){return h("plugin:menu|set_text",{rid:this.rid,kind:this.kind,text:e})}async isEnabled(){return h("plugin:menu|is_enabled",{rid:this.rid,kind:this.kind})}async setEnabled(e){return h("plugin:menu|set_enabled",{rid:this.rid,kind:this.kind,enabled:e})}async setAccelerator(e){return h("plugin:menu|set_accelerator",{rid:this.rid,kind:this.kind,accelerator:e})}async isChecked(){return h("plugin:menu|is_checked",{rid:this.rid})}async setChecked(e){return h("plugin:menu|set_checked",{rid:this.rid,checked:e})}}!function(e){e.Add="Add",e.Advanced="Advanced",e.Bluetooth="Bluetooth",e.Bookmarks="Bookmarks",e.Caution="Caution",e.ColorPanel="ColorPanel",e.ColumnView="ColumnView",e.Computer="Computer",e.EnterFullScreen="EnterFullScreen",e.Everyone="Everyone",e.ExitFullScreen="ExitFullScreen",e.FlowView="FlowView",e.Folder="Folder",e.FolderBurnable="FolderBurnable",e.FolderSmart="FolderSmart",e.FollowLinkFreestanding="FollowLinkFreestanding",e.FontPanel="FontPanel",e.GoLeft="GoLeft",e.GoRight="GoRight",e.Home="Home",e.IChatTheater="IChatTheater",e.IconView="IconView",e.Info="Info",e.InvalidDataFreestanding="InvalidDataFreestanding",e.LeftFacingTriangle="LeftFacingTriangle",e.ListView="ListView",e.LockLocked="LockLocked",e.LockUnlocked="LockUnlocked",e.MenuMixedState="MenuMixedState",e.MenuOnState="MenuOnState",e.MobileMe="MobileMe",e.MultipleDocuments="MultipleDocuments",e.Network="Network",e.Path="Path",e.PreferencesGeneral="PreferencesGeneral",e.QuickLook="QuickLook",e.RefreshFreestanding="RefreshFreestanding",e.Refresh="Refresh",e.Remove="Remove",e.RevealFreestanding="RevealFreestanding",e.RightFacingTriangle="RightFacingTriangle",e.Share="Share",e.Slideshow="Slideshow",e.SmartBadge="SmartBadge",e.StatusAvailable="StatusAvailable",e.StatusNone="StatusNone",e.StatusPartiallyAvailable="StatusPartiallyAvailable",e.StatusUnavailable="StatusUnavailable",e.StopProgressFreestanding="StopProgressFreestanding",e.StopProgress="StopProgress",e.TrashEmpty="TrashEmpty",e.TrashFull="TrashFull",e.User="User",e.UserAccounts="UserAccounts",e.UserGroup="UserGroup",e.UserGuest="UserGuest"}(z||(z={}));class G extends M{constructor(e,n){super(e,n,"Icon")}static async new(e){return F("Icon",e).then(([e,n])=>new G(e,n))}async text(){return h("plugin:menu|text",{rid:this.rid,kind:this.kind})}async setText(e){return h("plugin:menu|set_text",{rid:this.rid,kind:this.kind,text:e})}async isEnabled(){return h("plugin:menu|is_enabled",{rid:this.rid,kind:this.kind})}async setEnabled(e){return h("plugin:menu|set_enabled",{rid:this.rid,kind:this.kind,enabled:e})}async setAccelerator(e){return h("plugin:menu|set_accelerator",{rid:this.rid,kind:this.kind,accelerator:e})}async setIcon(e){return h("plugin:menu|set_icon",{rid:this.rid,kind:this.kind,icon:g(e)})}}class j extends M{constructor(e,n){super(e,n,"Predefined")}static async new(e){return F("Predefined",e).then(([e,n])=>new j(e,n))}async text(){return h("plugin:menu|text",{rid:this.rid,kind:this.kind})}async setText(e){return h("plugin:menu|set_text",{rid:this.rid,kind:this.kind,text:e})}}function H([e,n,t]){switch(t){case"Submenu":return new $(e,n);case"Predefined":return new j(e,n);case"Check":return new V(e,n);case"Icon":return new G(e,n);default:return new B(e,n)}}class $ extends M{constructor(e,n){super(e,n,"Submenu")}static async new(e){return F("Submenu",e).then(([e,n])=>new $(e,n))}async text(){return h("plugin:menu|text",{rid:this.rid,kind:this.kind})}async setText(e){return h("plugin:menu|set_text",{rid:this.rid,kind:this.kind,text:e})}async isEnabled(){return h("plugin:menu|is_enabled",{rid:this.rid,kind:this.kind})}async setEnabled(e){return h("plugin:menu|set_enabled",{rid:this.rid,kind:this.kind,enabled:e})}async append(e){return h("plugin:menu|append",{rid:this.rid,kind:this.kind,items:(Array.isArray(e)?e:[e]).map(e=>"rid"in e?[e.rid,e.kind]:e)})}async prepend(e){return h("plugin:menu|prepend",{rid:this.rid,kind:this.kind,items:(Array.isArray(e)?e:[e]).map(e=>"rid"in e?[e.rid,e.kind]:e)})}async insert(e,n){return h("plugin:menu|insert",{rid:this.rid,kind:this.kind,items:(Array.isArray(e)?e:[e]).map(e=>"rid"in e?[e.rid,e.kind]:e),position:n})}async remove(e){return h("plugin:menu|remove",{rid:this.rid,kind:this.kind,item:[e.rid,e.kind]})}async removeAt(e){return h("plugin:menu|remove_at",{rid:this.rid,kind:this.kind,position:e}).then(H)}async items(){return h("plugin:menu|items",{rid:this.rid,kind:this.kind}).then(e=>e.map(H))}async get(e){return h("plugin:menu|get",{rid:this.rid,kind:this.kind,id:e}).then(e=>e?H(e):null)}async popup(e,n){var t;return h("plugin:menu|popup",{rid:this.rid,kind:this.kind,window:null!==(t=null==n?void 0:n.label)&&void 0!==t?t:null,at:e instanceof E?e:e?new E(e):null})}async setAsWindowsMenuForNSApp(){return h("plugin:menu|set_as_windows_menu_for_nsapp",{rid:this.rid})}async setAsHelpMenuForNSApp(){return h("plugin:menu|set_as_help_menu_for_nsapp",{rid:this.rid})}async setIcon(e){return h("plugin:menu|set_icon",{rid:this.rid,kind:this.kind,icon:g(e)})}}class q extends M{constructor(e,n){super(e,n,"Menu")}static async new(e){return F("Menu",e).then(([e,n])=>new q(e,n))}static async default(){return h("plugin:menu|create_default").then(([e,n])=>new q(e,n))}async append(e){return h("plugin:menu|append",{rid:this.rid,kind:this.kind,items:(Array.isArray(e)?e:[e]).map(e=>"rid"in e?[e.rid,e.kind]:e)})}async prepend(e){return h("plugin:menu|prepend",{rid:this.rid,kind:this.kind,items:(Array.isArray(e)?e:[e]).map(e=>"rid"in e?[e.rid,e.kind]:e)})}async insert(e,n){return h("plugin:menu|insert",{rid:this.rid,kind:this.kind,items:(Array.isArray(e)?e:[e]).map(e=>"rid"in e?[e.rid,e.kind]:e),position:n})}async remove(e){return h("plugin:menu|remove",{rid:this.rid,kind:this.kind,item:[e.rid,e.kind]})}async removeAt(e){return h("plugin:menu|remove_at",{rid:this.rid,kind:this.kind,position:e}).then(H)}async items(){return h("plugin:menu|items",{rid:this.rid,kind:this.kind}).then(e=>e.map(H))}async get(e){return h("plugin:menu|get",{rid:this.rid,kind:this.kind,id:e}).then(e=>e?H(e):null)}async popup(e,n){var t;return h("plugin:menu|popup",{rid:this.rid,kind:this.kind,window:null!==(t=null==n?void 0:n.label)&&void 0!==t?t:null,at:e instanceof E?e:e?new E(e):null})}async setAsAppMenu(){return h("plugin:menu|set_as_app_menu",{rid:this.rid}).then(e=>e?new q(e[0],e[1]):null)}async setAsWindowMenu(e){var n;return h("plugin:menu|set_as_window_menu",{rid:this.rid,window:null!==(n=null==e?void 0:e.label)&&void 0!==n?n:null}).then(e=>e?new q(e[0],e[1]):null)}}var J=Object.freeze({__proto__:null,CheckMenuItem:V,IconMenuItem:G,Menu:q,MenuItem:B,get NativeIcon(){return z},PredefinedMenuItem:j,Submenu:$,itemFromKind:H});function Q(){var e,n;window.__TAURI_INTERNALS__=null!==(e=window.__TAURI_INTERNALS__)&&void 0!==e?e:{},window.__TAURI_EVENT_PLUGIN_INTERNALS__=null!==(n=window.__TAURI_EVENT_PLUGIN_INTERNALS__)&&void 0!==n?n:{}}var Z,K=Object.freeze({__proto__:null,clearMocks:function(){"object"==typeof window.__TAURI_INTERNALS__&&(delete window.__TAURI_INTERNALS__.invoke,delete window.__TAURI_INTERNALS__.transformCallback,delete window.__TAURI_INTERNALS__.unregisterCallback,delete window.__TAURI_INTERNALS__.runCallback,delete window.__TAURI_INTERNALS__.callbacks,delete window.__TAURI_INTERNALS__.convertFileSrc,delete window.__TAURI_INTERNALS__.metadata,"object"==typeof window.__TAURI_EVENT_PLUGIN_INTERNALS__&&delete window.__TAURI_EVENT_PLUGIN_INTERNALS__.unregisterListener)},mockConvertFileSrc:function(e){Q(),window.__TAURI_INTERNALS__.convertFileSrc=function(n,t="asset"){const i=encodeURIComponent(n);return"windows"===e?`http://${t}.localhost/${i}`:`${t}://localhost/${i}`}},mockIPC:function(e,n){function t(e,n){switch(e){case"plugin:event|listen":return function(e){i.has(e.event)||i.set(e.event,[]);return i.get(e.event).push(e.handler),e.handler}(n);case"plugin:event|emit":return function(e){const n=i.get(e.event)||[];for(const t of n)a(t,e);return null}(n);case"plugin:event|unlisten":return function(e){const n=i.get(e.event);if(n){const t=n.indexOf(e.id);-1!==t&&n.splice(t,1)}}(n)}}Q();const i=new Map,r=new Map;function s(e){r.delete(e)}function a(e,n){const t=r.get(e);t?t(n):console.warn(`[TAURI] Couldn't find callback id ${e}. This might happen when the app is reloaded while Rust is running an asynchronous operation.`)}window.__TAURI_INTERNALS__.invoke=async function(i,r,s){return(null==n?void 0:n.shouldMockEvents)&&function(e){return e.startsWith("plugin:event|")}(i)?t(i,r):e(i,r)},window.__TAURI_INTERNALS__.transformCallback=function(e,n=!1){const t=window.crypto.getRandomValues(new Uint32Array(1))[0];return r.set(t,i=>(n&&s(t),e&&e(i))),t},window.__TAURI_INTERNALS__.unregisterCallback=s,window.__TAURI_INTERNALS__.runCallback=a,window.__TAURI_INTERNALS__.callbacks=r,window.__TAURI_EVENT_PLUGIN_INTERNALS__.unregisterListener=function(e,n){s(n)}},mockWindows:function(e,...n){Q(),window.__TAURI_INTERNALS__.metadata={currentWindow:{label:e},currentWebview:{windowLabel:e,label:e}}}});!function(e){e[e.Audio=1]="Audio",e[e.Cache=2]="Cache",e[e.Config=3]="Config",e[e.Data=4]="Data",e[e.LocalData=5]="LocalData",e[e.Document=6]="Document",e[e.Download=7]="Download",e[e.Picture=8]="Picture",e[e.Public=9]="Public",e[e.Video=10]="Video",e[e.Resource=11]="Resource",e[e.Temp=12]="Temp",e[e.AppConfig=13]="AppConfig",e[e.AppData=14]="AppData",e[e.AppLocalData=15]="AppLocalData",e[e.AppCache=16]="AppCache",e[e.AppLog=17]="AppLog",e[e.Desktop=18]="Desktop",e[e.Executable=19]="Executable",e[e.Font=20]="Font",e[e.Home=21]="Home",e[e.Runtime=22]="Runtime",e[e.Template=23]="Template"}(Z||(Z={}));var Y=Object.freeze({__proto__:null,get BaseDirectory(){return Z},appCacheDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.AppCache})},appConfigDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.AppConfig})},appDataDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.AppData})},appLocalDataDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.AppLocalData})},appLogDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.AppLog})},audioDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Audio})},basename:async function(e,n){return h("plugin:path|basename",{path:e,ext:n})},cacheDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Cache})},configDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Config})},dataDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Data})},delimiter:function(){return window.__TAURI_INTERNALS__.plugins.path.delimiter},desktopDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Desktop})},dirname:async function(e){return h("plugin:path|dirname",{path:e})},documentDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Document})},downloadDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Download})},executableDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Executable})},extname:async function(e){return h("plugin:path|extname",{path:e})},fontDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Font})},homeDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Home})},isAbsolute:async function(e){return h("plugin:path|is_absolute",{path:e})},join:async function(...e){return h("plugin:path|join",{paths:e})},localDataDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.LocalData})},normalize:async function(e){return h("plugin:path|normalize",{path:e})},pictureDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Picture})},publicDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Public})},resolve:async function(...e){return h("plugin:path|resolve",{paths:e})},resolveResource:async function(e){return h("plugin:path|resolve_directory",{directory:Z.Resource,path:e})},resourceDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Resource})},runtimeDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Runtime})},sep:function(){return window.__TAURI_INTERNALS__.plugins.path.sep},tempDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Temp})},templateDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Template})},videoDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Video})}});class X extends w{constructor(e,n){super(e),this.id=n}static async getById(e){return h("plugin:tray|get_by_id",{id:e}).then(n=>n?new X(n,e):null)}static async removeById(e){return h("plugin:tray|remove_by_id",{id:e})}static async new(e){(null==e?void 0:e.menu)&&(e.menu=[e.menu.rid,e.menu.kind]),(null==e?void 0:e.icon)&&(e.icon=g(e.icon));const n=new c;if(null==e?void 0:e.action){const t=e.action;n.onmessage=e=>t(function(e){const n=e;return n.position=new I(e.position),n.rect.position=new I(e.rect.position),n.rect.size=new k(e.rect.size),n}(e)),delete e.action}return h("plugin:tray|new",{options:null!=e?e:{},handler:n}).then(([e,n])=>new X(e,n))}async setIcon(e){let n=null;return e&&(n=g(e)),h("plugin:tray|set_icon",{rid:this.rid,icon:n})}async setMenu(e){return e&&(e=[e.rid,e.kind]),h("plugin:tray|set_menu",{rid:this.rid,menu:e})}async setTooltip(e){return h("plugin:tray|set_tooltip",{rid:this.rid,tooltip:e})}async setTitle(e){return h("plugin:tray|set_title",{rid:this.rid,title:e})}async setVisible(e){return h("plugin:tray|set_visible",{rid:this.rid,visible:e})}async setTempDirPath(e){return h("plugin:tray|set_temp_dir_path",{rid:this.rid,path:e})}async setIconAsTemplate(e){return h("plugin:tray|set_icon_as_template",{rid:this.rid,asTemplate:e})}async setIconWithAsTemplate(e,n){let t=null;return e&&(t=g(e)),h("plugin:tray|set_icon_with_as_template",{rid:this.rid,icon:t,asTemplate:n})}async setMenuOnLeftClick(e){return h("plugin:tray|set_show_menu_on_left_click",{rid:this.rid,onLeft:e})}async setShowMenuOnLeftClick(e){return h("plugin:tray|set_show_menu_on_left_click",{rid:this.rid,onLeft:e})}}var ee,ne,te=Object.freeze({__proto__:null,TrayIcon:X});!function(e){e[e.Critical=1]="Critical",e[e.Informational=2]="Informational"}(ee||(ee={}));class ie{constructor(e){this._preventDefault=!1,this.event=e.event,this.id=e.id}preventDefault(){this._preventDefault=!0}isPreventDefault(){return this._preventDefault}}function re(){return new le(window.__TAURI_INTERNALS__.metadata.currentWindow.label,{skip:!0})}async function se(){return h("plugin:window|get_all_windows").then(e=>e.map(e=>new le(e,{skip:!0})))}!function(e){e.None="none",e.Normal="normal",e.Indeterminate="indeterminate",e.Paused="paused",e.Error="error"}(ne||(ne={}));const ae=["tauri://created","tauri://error"];class le{constructor(e,n={}){var t;this.label=e,this.listeners=Object.create(null),(null==n?void 0:n.skip)||h("plugin:window|create",{options:{...n,parent:"string"==typeof n.parent?n.parent:null===(t=n.parent)||void 0===t?void 0:t.label,label:e}}).then(async()=>this.emit("tauri://created")).catch(async e=>this.emit("tauri://error",e))}static async getByLabel(e){var n;return null!==(n=(await se()).find(n=>n.label===e))&&void 0!==n?n:null}static getCurrent(){return re()}static async getAll(){return se()}static async getFocusedWindow(){for(const e of await se())if(await e.isFocused())return e;return null}async listen(e,n){return this._handleTauriEvent(e,n)?()=>{const t=this.listeners[e];t.splice(t.indexOf(n),1)}:N(e,n,{target:{kind:"Window",label:this.label}})}async once(e,n){return this._handleTauriEvent(e,n)?()=>{const t=this.listeners[e];t.splice(t.indexOf(n),1)}:L(e,n,{target:{kind:"Window",label:this.label}})}async emit(e,n){if(!ae.includes(e))return C(e,n);for(const t of this.listeners[e]||[])t({event:e,id:-1,payload:n})}async emitTo(e,n,t){if(!ae.includes(n))return x(e,n,t);for(const e of this.listeners[n]||[])e({event:n,id:-1,payload:t})}_handleTauriEvent(e,n){return!!ae.includes(e)&&(e in this.listeners?this.listeners[e].push(n):this.listeners[e]=[n],!0)}async scaleFactor(){return h("plugin:window|scale_factor",{label:this.label})}async innerPosition(){return h("plugin:window|inner_position",{label:this.label}).then(e=>new I(e))}async outerPosition(){return h("plugin:window|outer_position",{label:this.label}).then(e=>new I(e))}async innerSize(){return h("plugin:window|inner_size",{label:this.label}).then(e=>new k(e))}async outerSize(){return h("plugin:window|outer_size",{label:this.label}).then(e=>new k(e))}async isFullscreen(){return h("plugin:window|is_fullscreen",{label:this.label})}async isMinimized(){return h("plugin:window|is_minimized",{label:this.label})}async isMaximized(){return h("plugin:window|is_maximized",{label:this.label})}async isFocused(){return h("plugin:window|is_focused",{label:this.label})}async isDecorated(){return h("plugin:window|is_decorated",{label:this.label})}async isResizable(){return h("plugin:window|is_resizable",{label:this.label})}async isMaximizable(){return h("plugin:window|is_maximizable",{label:this.label})}async isMinimizable(){return h("plugin:window|is_minimizable",{label:this.label})}async isClosable(){return h("plugin:window|is_closable",{label:this.label})}async isVisible(){return h("plugin:window|is_visible",{label:this.label})}async title(){return h("plugin:window|title",{label:this.label})}async theme(){return h("plugin:window|theme",{label:this.label})}async isAlwaysOnTop(){return h("plugin:window|is_always_on_top",{label:this.label})}async activityName(){return h("plugin:window|activity_name",{label:this.label})}async sceneIdentifier(){return h("plugin:window|scene_identifier",{label:this.label})}async center(){return h("plugin:window|center",{label:this.label})}async requestUserAttention(e){let n=null;return e&&(n=e===ee.Critical?{type:"Critical"}:{type:"Informational"}),h("plugin:window|request_user_attention",{label:this.label,value:n})}async setResizable(e){return h("plugin:window|set_resizable",{label:this.label,value:e})}async setEnabled(e){return h("plugin:window|set_enabled",{label:this.label,value:e})}async isEnabled(){return h("plugin:window|is_enabled",{label:this.label})}async setMaximizable(e){return h("plugin:window|set_maximizable",{label:this.label,value:e})}async setMinimizable(e){return h("plugin:window|set_minimizable",{label:this.label,value:e})}async setClosable(e){return h("plugin:window|set_closable",{label:this.label,value:e})}async setTitle(e){return h("plugin:window|set_title",{label:this.label,value:e})}async maximize(){return h("plugin:window|maximize",{label:this.label})}async unmaximize(){return h("plugin:window|unmaximize",{label:this.label})}async toggleMaximize(){return h("plugin:window|toggle_maximize",{label:this.label})}async minimize(){return h("plugin:window|minimize",{label:this.label})}async unminimize(){return h("plugin:window|unminimize",{label:this.label})}async show(){return h("plugin:window|show",{label:this.label})}async hide(){return h("plugin:window|hide",{label:this.label})}async close(){return h("plugin:window|close",{label:this.label})}async destroy(){return h("plugin:window|destroy",{label:this.label})}async setDecorations(e){return h("plugin:window|set_decorations",{label:this.label,value:e})}async setShadow(e){return h("plugin:window|set_shadow",{label:this.label,value:e})}async setEffects(e){return h("plugin:window|set_effects",{label:this.label,value:e})}async clearEffects(){return h("plugin:window|set_effects",{label:this.label,value:null})}async setAlwaysOnTop(e){return h("plugin:window|set_always_on_top",{label:this.label,value:e})}async setAlwaysOnBottom(e){return h("plugin:window|set_always_on_bottom",{label:this.label,value:e})}async setContentProtected(e){return h("plugin:window|set_content_protected",{label:this.label,value:e})}async setSize(e){return h("plugin:window|set_size",{label:this.label,value:e instanceof A?e:new A(e)})}async setMinSize(e){return h("plugin:window|set_min_size",{label:this.label,value:e instanceof A?e:e?new A(e):null})}async setMaxSize(e){return h("plugin:window|set_max_size",{label:this.label,value:e instanceof A?e:e?new A(e):null})}async setSizeConstraints(e){function n(e){return e?{Logical:e}:null}return h("plugin:window|set_size_constraints",{label:this.label,value:{minWidth:n(null==e?void 0:e.minWidth),minHeight:n(null==e?void 0:e.minHeight),maxWidth:n(null==e?void 0:e.maxWidth),maxHeight:n(null==e?void 0:e.maxHeight)}})}async setPosition(e){return h("plugin:window|set_position",{label:this.label,value:e instanceof E?e:new E(e)})}async setFullscreen(e){return h("plugin:window|set_fullscreen",{label:this.label,value:e})}async setSimpleFullscreen(e){return h("plugin:window|set_simple_fullscreen",{label:this.label,value:e})}async setFocus(){return h("plugin:window|set_focus",{label:this.label})}async setFocusable(e){return h("plugin:window|set_focusable",{label:this.label,value:e})}async setIcon(e){return h("plugin:window|set_icon",{label:this.label,value:g(e)})}async setSkipTaskbar(e){return h("plugin:window|set_skip_taskbar",{label:this.label,value:e})}async setCursorGrab(e){return h("plugin:window|set_cursor_grab",{label:this.label,value:e})}async setCursorVisible(e){return h("plugin:window|set_cursor_visible",{label:this.label,value:e})}async setCursorIcon(e){return h("plugin:window|set_cursor_icon",{label:this.label,value:e})}async setBackgroundColor(e){return h("plugin:window|set_background_color",{color:e})}async setCursorPosition(e){return h("plugin:window|set_cursor_position",{label:this.label,value:e instanceof E?e:new E(e)})}async setIgnoreCursorEvents(e){return h("plugin:window|set_ignore_cursor_events",{label:this.label,value:e})}async startDragging(){return h("plugin:window|start_dragging",{label:this.label})}async startResizeDragging(e){return h("plugin:window|start_resize_dragging",{label:this.label,value:e})}async setBadgeCount(e){return h("plugin:window|set_badge_count",{label:this.label,value:e})}async setBadgeLabel(e){return h("plugin:window|set_badge_label",{label:this.label,value:e})}async setOverlayIcon(e){return h("plugin:window|set_overlay_icon",{label:this.label,value:e?g(e):void 0})}async setProgressBar(e){return h("plugin:window|set_progress_bar",{label:this.label,value:e})}async setVisibleOnAllWorkspaces(e){return h("plugin:window|set_visible_on_all_workspaces",{label:this.label,value:e})}async setTitleBarStyle(e){return h("plugin:window|set_title_bar_style",{label:this.label,value:e})}async setTheme(e){return h("plugin:window|set_theme",{label:this.label,value:e})}async onResized(e){return this.listen(R.WINDOW_RESIZED,n=>{n.payload=new k(n.payload),e(n)})}async onMoved(e){return this.listen(R.WINDOW_MOVED,n=>{n.payload=new I(n.payload),e(n)})}async onCloseRequested(e){return this.listen(R.WINDOW_CLOSE_REQUESTED,async n=>{const t=new ie(n);await e(t),t.isPreventDefault()||await this.destroy()})}async onDragDropEvent(e){const n=await this.listen(R.DRAG_ENTER,n=>{e({...n,payload:{type:"enter",paths:n.payload.paths,position:new I(n.payload.position)}})}),t=await this.listen(R.DRAG_OVER,n=>{e({...n,payload:{type:"over",position:new I(n.payload.position)}})}),i=await this.listen(R.DRAG_DROP,n=>{e({...n,payload:{type:"drop",paths:n.payload.paths,position:new I(n.payload.position)}})}),r=await this.listen(R.DRAG_LEAVE,n=>{e({...n,payload:{type:"leave"}})});return()=>{n(),i(),t(),r()}}async onFocusChanged(e){const n=await this.listen(R.WINDOW_FOCUS,n=>{e({...n,payload:!0})}),t=await this.listen(R.WINDOW_BLUR,n=>{e({...n,payload:!1})});return()=>{n(),t()}}async onScaleChanged(e){return this.listen(R.WINDOW_SCALE_FACTOR_CHANGED,e)}async onThemeChanged(e){return this.listen(R.WINDOW_THEME_CHANGED,e)}}var oe,ue,ce,de;function pe(e){return null===e?null:{name:e.name,scaleFactor:e.scaleFactor,position:new I(e.position),size:new k(e.size),workArea:{position:new I(e.workArea.position),size:new k(e.workArea.size)}}}!function(e){e.Disabled="disabled",e.Throttle="throttle",e.Suspend="suspend"}(oe||(oe={})),function(e){e.Default="default",e.FluentOverlay="fluentOverlay"}(ue||(ue={})),function(e){e.AppearanceBased="appearanceBased",e.Light="light",e.Dark="dark",e.MediumLight="mediumLight",e.UltraDark="ultraDark",e.Titlebar="titlebar",e.Selection="selection",e.Menu="menu",e.Popover="popover",e.Sidebar="sidebar",e.HeaderView="headerView",e.Sheet="sheet",e.WindowBackground="windowBackground",e.HudWindow="hudWindow",e.FullScreenUI="fullScreenUI",e.Tooltip="tooltip",e.ContentBackground="contentBackground",e.UnderWindowBackground="underWindowBackground",e.UnderPageBackground="underPageBackground",e.Mica="mica",e.Blur="blur",e.Acrylic="acrylic",e.Tabbed="tabbed",e.TabbedDark="tabbedDark",e.TabbedLight="tabbedLight"}(ce||(ce={})),function(e){e.FollowsWindowActiveState="followsWindowActiveState",e.Active="active",e.Inactive="inactive"}(de||(de={}));var he=Object.freeze({__proto__:null,CloseRequestedEvent:ie,get Effect(){return ce},get EffectState(){return de},LogicalPosition:T,LogicalSize:v,PhysicalPosition:I,PhysicalSize:k,get ProgressBarStatus(){return ne},get UserAttentionType(){return ee},Window:le,availableMonitors:async function(){return h("plugin:window|available_monitors").then(e=>e.map(pe))},currentMonitor:async function(){return h("plugin:window|current_monitor").then(pe)},cursorPosition:async function(){return h("plugin:window|cursor_position").then(e=>new I(e))},getAllWindows:se,getCurrentWindow:re,monitorFromPoint:async function(e,n){return h("plugin:window|monitor_from_point",{x:e,y:n}).then(pe)},primaryMonitor:async function(){return h("plugin:window|primary_monitor").then(pe)}});function we(){return new ge(re(),window.__TAURI_INTERNALS__.metadata.currentWebview.label,{skip:!0})}async function _e(){return h("plugin:webview|get_all_webviews").then(e=>e.map(e=>new ge(new le(e.windowLabel,{skip:!0}),e.label,{skip:!0})))}const ye=["tauri://created","tauri://error"];class ge{constructor(e,n,t){this.window=e,this.label=n,this.listeners=Object.create(null),(null==t?void 0:t.skip)||h("plugin:webview|create_webview",{windowLabel:e.label,options:{...t,label:n}}).then(async()=>this.emit("tauri://created")).catch(async e=>this.emit("tauri://error",e))}static async getByLabel(e){var n;return null!==(n=(await _e()).find(n=>n.label===e))&&void 0!==n?n:null}static getCurrent(){return we()}static async getAll(){return _e()}async listen(e,n){return this._handleTauriEvent(e,n)?()=>{const t=this.listeners[e];t.splice(t.indexOf(n),1)}:N(e,n,{target:{kind:"Webview",label:this.label}})}async once(e,n){return this._handleTauriEvent(e,n)?()=>{const t=this.listeners[e];t.splice(t.indexOf(n),1)}:L(e,n,{target:{kind:"Webview",label:this.label}})}async emit(e,n){if(!ye.includes(e))return C(e,n);for(const t of this.listeners[e]||[])t({event:e,id:-1,payload:n})}async emitTo(e,n,t){if(!ye.includes(n))return x(e,n,t);for(const e of this.listeners[n]||[])e({event:n,id:-1,payload:t})}_handleTauriEvent(e,n){return!!ye.includes(e)&&(e in this.listeners?this.listeners[e].push(n):this.listeners[e]=[n],!0)}async position(){return h("plugin:webview|webview_position",{label:this.label}).then(e=>new I(e))}async size(){return h("plugin:webview|webview_size",{label:this.label}).then(e=>new k(e))}async close(){return h("plugin:webview|webview_close",{label:this.label})}async setSize(e){return h("plugin:webview|set_webview_size",{label:this.label,value:e instanceof A?e:new A(e)})}async setPosition(e){return h("plugin:webview|set_webview_position",{label:this.label,value:e instanceof E?e:new E(e)})}async setFocus(){return h("plugin:webview|set_webview_focus",{label:this.label})}async setAutoResize(e){return h("plugin:webview|set_webview_auto_resize",{label:this.label,value:e})}async hide(){return h("plugin:webview|webview_hide",{label:this.label})}async show(){return h("plugin:webview|webview_show",{label:this.label})}async setZoom(e){return h("plugin:webview|set_webview_zoom",{label:this.label,value:e})}async reparent(e){return h("plugin:webview|reparent",{label:this.label,window:"string"==typeof e?e:e.label})}async clearAllBrowsingData(){return h("plugin:webview|clear_all_browsing_data")}async setBackgroundColor(e){return h("plugin:webview|set_webview_background_color",{color:e})}async onDragDropEvent(e){const n=await this.listen(R.DRAG_ENTER,n=>{e({...n,payload:{type:"enter",paths:n.payload.paths,position:new I(n.payload.position)}})}),t=await this.listen(R.DRAG_OVER,n=>{e({...n,payload:{type:"over",position:new I(n.payload.position)}})}),i=await this.listen(R.DRAG_DROP,n=>{e({...n,payload:{type:"drop",paths:n.payload.paths,position:new I(n.payload.position)}})}),r=await this.listen(R.DRAG_LEAVE,n=>{e({...n,payload:{type:"leave"}})});return()=>{n(),i(),t(),r()}}}var be,me,fe=Object.freeze({__proto__:null,Webview:ge,getAllWebviews:_e,getCurrentWebview:we});function ve(){const e=we();return new Ae(e.label,{skip:!0})}async function ke(){return h("plugin:window|get_all_windows").then(e=>e.map(e=>new Ae(e,{skip:!0})))}class Ae{constructor(e,n={}){var t;this.label=e,this.listeners=Object.create(null),(null==n?void 0:n.skip)||h("plugin:webview|create_webview_window",{options:{...n,parent:"string"==typeof n.parent?n.parent:null===(t=n.parent)||void 0===t?void 0:t.label,label:e}}).then(async()=>this.emit("tauri://created")).catch(async e=>this.emit("tauri://error",e))}static async getByLabel(e){var n;const t=null!==(n=(await ke()).find(n=>n.label===e))&&void 0!==n?n:null;return t?new Ae(t.label,{skip:!0}):null}static getCurrent(){return ve()}static async getAll(){return ke()}async listen(e,n){return this._handleTauriEvent(e,n)?()=>{const t=this.listeners[e];t.splice(t.indexOf(n),1)}:N(e,n,{target:{kind:"WebviewWindow",label:this.label}})}async once(e,n){return this._handleTauriEvent(e,n)?()=>{const t=this.listeners[e];t.splice(t.indexOf(n),1)}:L(e,n,{target:{kind:"WebviewWindow",label:this.label}})}async setBackgroundColor(e){return h("plugin:window|set_background_color",{color:e}).then(()=>h("plugin:webview|set_webview_background_color",{color:e}))}}be=Ae,me=[le,ge],(Array.isArray(me)?me:[me]).forEach(e=>{Object.getOwnPropertyNames(e.prototype).forEach(n=>{var t;"object"==typeof be.prototype&&be.prototype&&n in be.prototype||Object.defineProperty(be.prototype,n,null!==(t=Object.getOwnPropertyDescriptor(e.prototype,n))&&void 0!==t?t:Object.create(null))})});var Te=Object.freeze({__proto__:null,WebviewWindow:Ae,getAllWebviewWindows:ke,getCurrentWebviewWindow:ve});return e.app=f,e.core=_,e.dpi=D,e.event=O,e.image=m,e.menu=J,e.mocks=K,e.path=Y,e.tray=te,e.webview=fe,e.webviewWindow=Te,e.window=he,e}({});window.__TAURI__=__TAURI_IIFE__; diff --git a/crates/tauri/src/app.rs b/crates/tauri/src/app.rs index 47cc6d71fb66..7eca3172f4e4 100644 --- a/crates/tauri/src/app.rs +++ b/crates/tauri/src/app.rs @@ -67,6 +67,9 @@ pub type SetupHook = Box) -> std::result::Result<(), Box> + Send>; /// A closure that is run every time a page starts or finishes loading. pub type OnPageLoad = dyn Fn(&Webview, &PageLoadPayload<'_>) + Send + Sync + 'static; +/// A closure that is run when the web content process terminates. +#[cfg(any(target_os = "macos", target_os = "ios"))] +pub type OnWebContentProcessTerminate = dyn Fn(&Webview) + Send + Sync + 'static; pub type ChannelInterceptor = Box, CallbackFn, usize, &InvokeResponseBody) -> bool + Send + Sync + 'static>; @@ -146,6 +149,24 @@ pub enum WindowEvent { /// /// - **Linux**: Not supported. ThemeChanged(Theme), + /// Emitted when the application has been suspended. + /// + /// ## Platform-specific + /// + /// - **Android**: This is triggered by `onPause` method of the Activity. + /// - **iOS**: This is triggered by `applicationWillResignActive` method of the UIApplicationDelegate. + /// - **Linux / macOS / Windows**: Unsupported. + #[cfg(mobile)] + Suspended, + /// Emitted when the application has been resumed. + /// + /// ## Platform-specific + /// + /// - **Android**: This is triggered by `onResume` method of the Activity. The first onResume() is ignored to match the iOS implementation, since that is called on activity creation. + /// - **iOS**: This is triggered by `applicationWillEnterForeground` method of the UIApplicationDelegate. + /// - **Linux / macOS / Windows**: Unsupported. + #[cfg(mobile)] + Resumed, } impl From for WindowEvent { @@ -167,6 +188,10 @@ impl From for WindowEvent { }, RuntimeWindowEvent::DragDrop(event) => Self::DragDrop(event), RuntimeWindowEvent::ThemeChanged(theme) => Self::ThemeChanged(theme), + #[cfg(mobile)] + RuntimeWindowEvent::Suspended => Self::Suspended, + #[cfg(mobile)] + RuntimeWindowEvent::Resumed => Self::Resumed, } } } @@ -250,6 +275,20 @@ pub enum RunEvent { /// Indicates whether the NSApplication object found any visible windows in your application. has_visible_windows: bool, }, + /// Emitted when a scene is requested by the system. + /// + /// This event is emitted when a scene is requested by the system. + /// Scenes created by [`Window::new`] are not emitted with this event. + /// It is also not emitted for the main scene. + #[cfg(target_os = "ios")] + SceneRequested { + /// Scene that was requested by the system. + scene: objc2::rc::Retained, + /// Options that were used to request the scene. + /// + /// This lets you determine why the scene was requested. + options: objc2::rc::Retained, + }, } impl From for RunEvent { @@ -263,7 +302,7 @@ impl From for RunEvent { } } -/// The asset resolver is a helper to access the [`tauri_utils::assets::Assets`] interface. +/// The asset resolver is a helper to access the [`crate::Assets`] interface. #[derive(Debug, Clone)] pub struct AssetResolver { manager: Arc>, @@ -628,6 +667,18 @@ impl AppHandle { pub fn set_device_event_filter(&self, filter: DeviceEventFilter) { self.runtime_handle.set_device_event_filter(filter); } + + /// Whether the application supports multiple windows. + #[cfg(target_os = "ios")] + pub fn supports_multiple_windows(&self) -> bool { + let (tx, rx) = std::sync::mpsc::channel(); + self.run_on_main_thread(move || unsafe { + let mtm = objc2::MainThreadMarker::new().unwrap(); + let ui_application = objc2_ui_kit::UIApplication::sharedApplication(mtm); + tx.send(ui_application.supportsMultipleScenes()).unwrap(); + }); + rx.recv().unwrap() + } } impl Manager for AppHandle { @@ -652,6 +703,16 @@ impl ManagerBase for AppHandle { fn managed_app_handle(&self) -> &AppHandle { self } + + #[cfg(target_os = "android")] + fn activity_name(&self) -> Option> { + None + } + + #[cfg(target_os = "ios")] + fn scene_identifier(&self) -> Option> { + None + } } /// The instance of the currently running application. @@ -702,6 +763,16 @@ impl ManagerBase for App { fn managed_app_handle(&self) -> &AppHandle { self.handle() } + + #[cfg(target_os = "android")] + fn activity_name(&self) -> Option> { + None + } + + #[cfg(target_os = "ios")] + fn scene_identifier(&self) -> Option> { + None + } } /// APIs specific to the wry runtime. @@ -1041,6 +1112,40 @@ macro_rules! shared_app_impl { pub fn invoke_key(&self) -> &str { self.manager.invoke_key() } + + /// Whether the application supports multiple windows. + #[cfg(desktop)] + pub fn supports_multiple_windows(&self) -> bool { + true + } + + /// Whether the application supports multiple windows. + #[cfg(target_os = "android")] + pub fn supports_multiple_windows(&self) -> bool { + let runtime_handle = match self.runtime() { + RuntimeOrDispatch::Runtime(runtime) => runtime.handle(), + RuntimeOrDispatch::RuntimeHandle(handle) => handle, + _ => unreachable!(), + }; + + let (tx, rx) = std::sync::mpsc::channel(); + + runtime_handle.run_on_android_context(move |env, _activity, _webview| { + let supports = (|| { + let version_class = env.find_class("android/os/Build$VERSION")?; + let sdk = env + .get_static_field(version_class, "SDK_INT", "I")? + .i() + .unwrap_or_default(); + crate::Result::Ok(sdk >= 32) + })() + .unwrap_or(false); + + let _ = tx.send(supports); + }); + + rx.recv().unwrap_or(false) + } } impl Listener for $app { @@ -1140,6 +1245,16 @@ impl App { &self.handle } + /// Whether the application supports multiple windows. + #[cfg(target_os = "ios")] + pub fn supports_multiple_windows(&self) -> bool { + unsafe { + let mtm = objc2::MainThreadMarker::new().unwrap(); + let ui_application = objc2_ui_kit::UIApplication::sharedApplication(mtm); + ui_application.supportsMultipleScenes() + } + } + /// Sets the activation policy for the application. It is set to `NSApplicationActivationPolicyRegular` by default. /// /// # Examples @@ -1386,6 +1501,10 @@ pub struct Builder { /// Page load hook. on_page_load: Option>>, + /// Web content process termination hook. + #[cfg(any(target_os = "macos", target_os = "ios"))] + on_web_content_process_terminate: Option>>, + /// All passed plugins plugins: PluginStore, @@ -1483,6 +1602,8 @@ impl Builder { .into_string(), channel_interceptor: None, on_page_load: None, + #[cfg(any(target_os = "macos", target_os = "ios"))] + on_web_content_process_terminate: None, plugins: PluginStore::default(), uri_scheme_protocols: Default::default(), state: StateManager::new(), @@ -1542,6 +1663,20 @@ impl Builder { ); self } + + /// Sets the disk cache directory for CEF (`Settings::cache_path`). + /// + /// Calling this more than once keeps the path from the last call. + /// If omitted, the cache defaults to `{user cache directory}/{identifier}/cef`. + #[cfg(feature = "cef")] + pub fn root_cache_path>(mut self, path: P) -> Self { + self + .platform_specific_attributes + .push(tauri_runtime_cef::RuntimeInitAttribute::CachePath { + path: path.as_ref().to_path_buf(), + }); + self + } } impl Builder { @@ -1738,6 +1873,23 @@ tauri::Builder::::new() self } + /// Defines the web content process termination hook. + /// + /// ## Platform-specific + /// + /// - **Linux / Windows / Android:** Unsupported. + #[cfg(any(target_os = "macos", target_os = "ios"))] + #[must_use] + pub fn on_web_content_process_terminate(mut self, on_web_content_process_terminate: F) -> Self + where + F: Fn(&Webview) + Send + Sync + 'static, + { + self + .on_web_content_process_terminate + .replace(Arc::new(on_web_content_process_terminate)); + self + } + /// Adds a Tauri application plugin. /// /// A plugin is created using the [`crate::plugin::Builder`] struct.Check its documentation for more information. @@ -2193,6 +2345,8 @@ tauri::Builder::::new() self.plugins, self.invoke_handler, self.on_page_load, + #[cfg(any(target_os = "macos", target_os = "ios"))] + self.on_web_content_process_terminate, self.uri_scheme_protocols, self.state, #[cfg(desktop)] @@ -2581,6 +2735,10 @@ fn on_event_loop_event( } => RunEvent::Reopen { has_visible_windows, }, + #[cfg(target_os = "ios")] + RuntimeRunEvent::SceneRequested { scene, options } => { + RunEvent::SceneRequested { scene, options } + } _ => unimplemented!(), }; diff --git a/crates/tauri/src/app/plugin.rs b/crates/tauri/src/app/plugin.rs index d9d32b7f114f..d550e26c00b2 100644 --- a/crates/tauri/src/app/plugin.rs +++ b/crates/tauri/src/app/plugin.rs @@ -114,6 +114,11 @@ pub fn bundle_type() -> Option { tauri_utils::platform::bundle_type() } +#[command(root = "crate")] +pub fn supports_multiple_windows(app: AppHandle) -> bool { + app.supports_multiple_windows() +} + pub fn init() -> TauriPlugin { Builder::new("app") .invoke_handler(crate::generate_handler![ @@ -130,6 +135,7 @@ pub fn init() -> TauriPlugin { set_app_theme, set_dock_visibility, bundle_type, + supports_multiple_windows, ]) .setup(|_app, _api| { #[cfg(target_os = "android")] diff --git a/crates/tauri/src/async_runtime.rs b/crates/tauri/src/async_runtime.rs index a1783e064cec..bfa80f5bd916 100644 --- a/crates/tauri/src/async_runtime.rs +++ b/crates/tauri/src/async_runtime.rs @@ -42,6 +42,7 @@ impl GlobalRuntime { } } + #[track_caller] fn spawn(&self, task: F) -> JoinHandle where F: Future + Send + 'static, @@ -54,6 +55,7 @@ impl GlobalRuntime { } } + #[track_caller] pub fn spawn_blocking(&self, func: F) -> JoinHandle where F: FnOnce() -> R + Send + 'static, @@ -66,6 +68,7 @@ impl GlobalRuntime { } } + #[track_caller] fn block_on(&self, task: F) -> F::Output { if let Some(r) = &self.runtime { r.block_on(task) @@ -95,6 +98,7 @@ impl Runtime { } } + #[track_caller] /// Spawns a future onto the runtime. pub fn spawn(&self, task: F) -> JoinHandle where @@ -109,6 +113,7 @@ impl Runtime { } } + #[track_caller] /// Runs the provided function on an executor dedicated to blocking operations. pub fn spawn_blocking(&self, func: F) -> JoinHandle where @@ -120,6 +125,7 @@ impl Runtime { } } + #[track_caller] /// Runs a future to completion on runtime. pub fn block_on(&self, task: F) -> F::Output { match self { @@ -177,6 +183,7 @@ impl RuntimeHandle { h } + #[track_caller] /// Runs the provided function on an executor dedicated to blocking operations. pub fn spawn_blocking(&self, func: F) -> JoinHandle where @@ -188,6 +195,7 @@ impl RuntimeHandle { } } + #[track_caller] /// Spawns a future onto the runtime. pub fn spawn(&self, task: F) -> JoinHandle where @@ -202,6 +210,7 @@ impl RuntimeHandle { } } + #[track_caller] /// Runs a future to completion on runtime. pub fn block_on(&self, task: F) -> F::Output { match self { @@ -258,12 +267,14 @@ pub fn handle() -> RuntimeHandle { runtime.handle() } +#[track_caller] /// Runs a future to completion on runtime. pub fn block_on(task: F) -> F::Output { let runtime = RUNTIME.get_or_init(default_runtime); runtime.block_on(task) } +#[track_caller] /// Spawns a future onto the runtime. pub fn spawn(task: F) -> JoinHandle where @@ -274,6 +285,7 @@ where runtime.spawn(task) } +#[track_caller] /// Runs the provided function on an executor dedicated to blocking operations. pub fn spawn_blocking(func: F) -> JoinHandle where @@ -284,6 +296,7 @@ where runtime.spawn_blocking(func) } +#[track_caller] #[allow(dead_code)] pub(crate) fn safe_block_on(task: F) -> F::Output where diff --git a/crates/tauri/src/image/mod.rs b/crates/tauri/src/image/mod.rs index 9dec4165802b..c25e98de5f29 100644 --- a/crates/tauri/src/image/mod.rs +++ b/crates/tauri/src/image/mod.rs @@ -194,7 +194,7 @@ impl JsImage { /// This will retrieve the image from the passed [`ResourceTable`] if it is [`JsImage::Resource`] /// and will return an error if it doesn't exist in the passed [`ResourceTable`] so make sure /// the passed [`ResourceTable`] is the same one used to store the image, usually this should be - /// the webview [resources table](crate::webview::Webview::resources_table). + /// the webview resources table. pub fn into_img(self, resources_table: &ResourceTable) -> crate::Result>> { match self { Self::Resource(rid) => resources_table.get::>(rid), diff --git a/crates/tauri/src/ipc/authority.rs b/crates/tauri/src/ipc/authority.rs index b64130fdc1a6..2db4da637968 100644 --- a/crates/tauri/src/ipc/authority.rs +++ b/crates/tauri/src/ipc/authority.rs @@ -4,7 +4,7 @@ use std::collections::BTreeMap; use std::fmt::{Debug, Display}; -use std::sync::Arc; +use std::sync::{Arc, Mutex, OnceLock}; use serde::de::DeserializeOwned; @@ -24,14 +24,47 @@ use crate::{Runtime, ipc::InvokeError, sealed::ManagerBase}; use super::{CommandArg, CommandItem}; -/// The runtime authority used to authorize IPC execution based on the Access Control List. -pub struct RuntimeAuthority { +/// Materialized authority data derived from the resolved ACL. +/// +/// Built lazily (see [`RuntimeAuthority::inner`]) because constructing it is the dominant cost +/// of `generate_context!`: the resolved ACL (`allowed_commands` / `denied_commands` / scopes for +/// every command) is emitted by `tauri-codegen` as a large literal built at runtime. It is only +/// needed at command-dispatch time, never during startup, so building it off-thread keeps that +/// cost off the startup critical path. Deep-link forwards exit without ever dispatching a +/// command, so they never block on the build: it runs on the background thread and is discarded +/// when the process exits. +struct RuntimeAuthorityInner { + /// Raw ACL manifests. Only read on the command-denied error path + /// ([`RuntimeAuthority::resolve_access_message`]) and by the `dynamic-acl` feature; dropped + /// entirely in release builds. #[cfg(any(feature = "dynamic-acl", debug_assertions))] acl: BTreeMap, has_app_acl: bool, allowed_commands: BTreeMap>, denied_commands: BTreeMap>, - pub(crate) scope_manager: ScopeManager, + scope_manager: ScopeManager, +} + +/// `Send` inputs to a [`RuntimeAuthorityInner`], produced off-thread by +/// [`RuntimeAuthority::new_async`]. +/// +/// Only the expensive, `Send` data (the resolved ACL and raw manifests) is built on the +/// background thread; the empty `StateManager` scope caches are assembled on the consuming +/// thread in [`RuntimeAuthority::build_inner`] (they are not `Send` and are cheap to create). +struct ResolvedAcl { + #[cfg(any(feature = "dynamic-acl", debug_assertions))] + acl: BTreeMap, + resolved: Resolved, +} + +/// The runtime authority used to authorize IPC execution based on the Access Control List. +pub struct RuntimeAuthority { + /// Materialized authority data. Populated eagerly by [`Self::new`] or lazily by + /// [`Self::inner`], which joins the background builder on first access. + inner: OnceLock, + /// Background builder for [`Self::inner`], joined on first access. `None` for the eager + /// [`Self::new`] constructor. + pending: Option>>>, } /// The origin trying to access the IPC. @@ -79,7 +112,12 @@ impl Origin { #[macro_export] macro_rules! runtime_authority { ($acl:expr, $resolved_acl:expr) => { - $crate::ipc::RuntimeAuthority::new($acl, $resolved_acl) + // Build the (expensive) resolved ACL and raw ACL on a background thread; the first + // command-authorization check blocks on the result. `|| $acl` / `|| $resolved_acl` are + // non-capturing (both are emitted as compile-time literals), so they coerce to `fn` + // pointers. This keeps ACL construction — the dominant cost of `generate_context!` — off + // the startup critical path. See `RuntimeAuthority`. + $crate::ipc::RuntimeAuthority::new_async(|| $acl, || $resolved_acl) }; } @@ -96,46 +134,149 @@ macro_rules! runtime_authority { #[macro_export] macro_rules! runtime_authority { ($_acl:expr, $resolved_acl:expr) => { - $crate::ipc::RuntimeAuthority::new($resolved_acl) + // Release builds drop the raw ACL entirely; only the resolved ACL is built, off-thread. + $crate::ipc::RuntimeAuthority::new_async(|| $resolved_acl) }; } impl RuntimeAuthority { - /// Contruct a new [`RuntimeAuthority`] from the ACL + /// Assembles [`RuntimeAuthorityInner`] from resolved ACL data. /// - /// **Please prefer using the [`runtime_authority`] macro instead of calling this directly** - #[doc(hidden)] - pub fn new( - #[cfg(any(feature = "dynamic-acl", debug_assertions))] acl: BTreeMap, - resolved_acl: Resolved, - ) -> Self { - let command_cache = resolved_acl + /// The expensive part (the [`Resolved`] literal) is already built; this only wires up the + /// empty [`StateManager`] scope caches, so it is cheap and runs on the consuming thread. + fn build_inner(resolved_acl: ResolvedAcl) -> RuntimeAuthorityInner { + let ResolvedAcl { + #[cfg(any(feature = "dynamic-acl", debug_assertions))] + acl, + resolved, + } = resolved_acl; + let command_cache = resolved .command_scope .keys() .map(|key| (*key, StateManager::new())) .collect(); - Self { + RuntimeAuthorityInner { #[cfg(any(feature = "dynamic-acl", debug_assertions))] acl, - has_app_acl: resolved_acl.has_app_acl, - allowed_commands: resolved_acl.allowed_commands, - denied_commands: resolved_acl.denied_commands, + has_app_acl: resolved.has_app_acl, + allowed_commands: resolved.allowed_commands, + denied_commands: resolved.denied_commands, scope_manager: ScopeManager { - command_scope: resolved_acl.command_scope, - global_scope: resolved_acl.global_scope, + command_scope: resolved.command_scope, + global_scope: resolved.global_scope, command_cache, global_scope_cache: StateManager::new(), }, } } + /// Construct a new [`RuntimeAuthority`] from already-resolved ACL data (built eagerly). + /// + /// Prefer the [`runtime_authority`] macro, which builds the ACL lazily off-thread via + /// [`Self::new_async`]. This eager constructor is for callers that already have the resolved + /// ACL in hand (e.g. tests). + #[doc(hidden)] + pub fn new( + #[cfg(any(feature = "dynamic-acl", debug_assertions))] acl: BTreeMap, + resolved_acl: Resolved, + ) -> Self { + let inner = OnceLock::new(); + let _ = inner.set(Self::build_inner(ResolvedAcl { + #[cfg(any(feature = "dynamic-acl", debug_assertions))] + acl, + resolved: resolved_acl, + })); + Self { + inner, + pending: None, + } + } + + /// Construct a new [`RuntimeAuthority`], building the resolved ACL on a background thread. + /// + /// The resolved ACL (and raw ACL) is the dominant cost of `generate_context!`, yet it is only + /// needed at command-dispatch time. Building it off-thread keeps it off the startup critical + /// path; the first read via [`Self::inner`] (e.g. [`Self::resolve_access`]) blocks on the + /// result. The [`runtime_authority`] macro passes non-capturing closures over compile-time + /// literals; the `Send + 'static` bound also lets callers (e.g. tests) pass capturing + /// closures. + /// + /// The thread spawn + join is pure overhead for trivial ACLs (e.g. tests and small apps), so + /// this only pays off above a non-trivial resolved-ACL size; the size is not known until the + /// ACL is built, so the trade-off cannot be gated at runtime. Use [`Self::new`] when the + /// resolved ACL is already in hand and the overhead is not worth it. + /// + /// **Please prefer using the [`runtime_authority`] macro instead of calling this directly** + #[doc(hidden)] + pub fn new_async( + #[cfg(any(feature = "dynamic-acl", debug_assertions))] acl_builder: impl FnOnce() -> BTreeMap< + String, + Manifest, + > + Send + + 'static, + resolved_builder: impl FnOnce() -> Resolved + Send + 'static, + ) -> Self { + let handle = std::thread::Builder::new() + .name(String::from("tauri runtime authority")) + // The resolved-ACL literal construction is deep; give it the headroom the generated + // context-creation thread used to need (it no longer constructs the ACL inline). + .stack_size(8 * 1024 * 1024) + .spawn(move || ResolvedAcl { + #[cfg(any(feature = "dynamic-acl", debug_assertions))] + acl: acl_builder(), + resolved: resolved_builder(), + }) + .expect("failed to spawn runtime authority builder thread"); + Self { + inner: OnceLock::new(), + pending: Some(Mutex::new(Some(handle))), + } + } + + /// Returns the materialized authority data, blocking on the background builder on first + /// access if this authority was constructed via [`Self::new_async`]. + fn inner(&self) -> &RuntimeAuthorityInner { + self.inner.get_or_init(|| { + let handle = self + .pending + .as_ref() + .expect("runtime authority has neither resolved data nor a pending builder") + .lock() + .unwrap() + .take() + // The handle is taken exactly once. If it is already gone while `inner` is still unset, + // a previous `inner()` call took it and panicked joining the builder thread, so report + // that cause rather than the misleading "neither data nor builder". + .expect("runtime authority builder thread panicked"); + let resolved_acl = handle + .join() + .expect("runtime authority builder thread panicked"); + Self::build_inner(resolved_acl) + }) + } + + /// Mutable access to the materialized authority data, materializing it first if needed. + fn inner_mut(&mut self) -> &mut RuntimeAuthorityInner { + // Force materialization (the returned shared borrow ends immediately), then hand out `&mut`. + let _ = self.inner(); + self + .inner + .get_mut() + .expect("runtime authority materialized above") + } + + /// The scope manager, materializing the authority data first if needed. + pub(crate) fn scope_manager(&self) -> &ScopeManager { + &self.inner().scope_manager + } + pub(crate) fn has_app_manifest(&self) -> bool { - self.has_app_acl + self.inner().has_app_acl } #[doc(hidden)] pub fn __allow_command(&mut self, command: String, context: ExecutionContext) { - self.allowed_commands.insert( + self.inner_mut().allowed_commands.insert( command, vec![ResolvedCommand { context, @@ -171,26 +312,29 @@ impl RuntimeAuthority { } } + // Resolve against the raw ACL (materializes the authority) before taking `&mut` below. let resolved = Resolved::resolve( - &self.acl, + &self.inner().acl, capabilities, tauri_utils::platform::Target::current(), ) .unwrap(); + let inner = self.inner_mut(); + // fill global scope for (plugin, global_scope) in resolved.global_scope { - let global_scope_entry = self.scope_manager.global_scope.entry(plugin).or_default(); + let global_scope_entry = inner.scope_manager.global_scope.entry(plugin).or_default(); global_scope_entry.allow.extend(global_scope.allow); global_scope_entry.deny.extend(global_scope.deny); - self.scope_manager.global_scope_cache = StateManager::new(); + inner.scope_manager.global_scope_cache = StateManager::new(); } // denied commands for (cmd_key, resolved_cmds) in resolved.denied_commands { - let entry = self.denied_commands.entry(cmd_key).or_default(); + let entry = inner.denied_commands.entry(cmd_key).or_default(); entry.extend(resolved_cmds); } @@ -201,7 +345,7 @@ impl RuntimeAuthority { if let Some(scope_id) = resolved_cmd.scope_id { let command_scope = resolved.command_scope.get(&scope_id).unwrap(); - let command_scope_entry = self + let command_scope_entry = inner .scope_manager .command_scope .entry(scope_id) @@ -211,14 +355,14 @@ impl RuntimeAuthority { .extend(command_scope.allow.clone()); command_scope_entry.deny.extend(command_scope.deny.clone()); - self + inner .scope_manager .command_cache .insert(scope_id, StateManager::new()); } } - let entry = self.allowed_commands.entry(cmd_key).or_default(); + let entry = inner.allowed_commands.entry(cmd_key).or_default(); entry.extend(resolved_cmds); } @@ -298,22 +442,34 @@ impl RuntimeAuthority { manifest: &Manifest, set: &crate::utils::acl::PermissionSet, command: &str, + allow_wildcard: bool, ) -> bool { for permission_id in &set.permissions { if permission_id == "default" { if let Some(default) = &manifest.default_permission - && has_permissions_allowing_command(manifest, default, command) + && has_permissions_allowing_command(manifest, default, command, allow_wildcard) { return true; } - } else if let Some(ref_set) = manifest.permission_sets.get(permission_id) { - if has_permissions_allowing_command(manifest, ref_set, command) { - return true; - } + } else if let Some(ref_set) = manifest.permission_sets.get(permission_id) + && has_permissions_allowing_command(manifest, ref_set, command, allow_wildcard) + { + return true; } else if let Some(permission) = manifest.permissions.get(permission_id) && permission.commands.allow.contains(&command.into()) { return true; + } else if let Some(permission) = manifest.command_permission(permission_id, allow_wildcard) + { + // `*` is the wildcard command produced by the `allow-*` permission + if permission + .commands + .allow + .iter() + .any(|c| c == command || c == "*") + { + return true; + } } } false @@ -331,15 +487,18 @@ impl RuntimeAuthority { format!("{key}.{command_name}") }; - if let Some(resolved) = self.denied_commands.get(&command) { + // Materialize the authority once; everything below reads the resolved ACL. + let inner = self.inner(); + + if let Some(resolved) = inner.denied_commands.get(&command) { format!( "{command_pretty_name} explicitly denied on origin {origin}\n\nreferenced by: {}", print_references(resolved) ) } else { - let command_matches = self.allowed_commands.get(&command); + let command_matches = inner.allowed_commands.get(&command); - if let Some(resolved) = self.allowed_commands.get(&command) { + if let Some(resolved) = inner.allowed_commands.get(&command) { let resolved_matching_origin = resolved .iter() .filter(|cmd| origin.matches(&cmd.context)) @@ -364,20 +523,23 @@ impl RuntimeAuthority { ) } } else { - let permission_error_detail = if let Some((key, manifest)) = self + let permission_error_detail = if let Some((key, manifest)) = inner .acl .get_key_value(key) - .or_else(|| self.acl.get_key_value(&format!("core:{key}"))) + .or_else(|| inner.acl.get_key_value(&format!("core:{key}"))) { let mut permissions_referencing_command = Vec::new(); + // the `allow-*`/`deny-*` wildcards are only available for the app manifest + let allow_wildcard = key == APP_ACL_KEY; + if let Some(default) = &manifest.default_permission - && has_permissions_allowing_command(manifest, default, command_name) + && has_permissions_allowing_command(manifest, default, command_name, allow_wildcard) { permissions_referencing_command.push("default".into()); } for set in manifest.permission_sets.values() { - if has_permissions_allowing_command(manifest, set, command_name) { + if has_permissions_allowing_command(manifest, set, command_name, allow_wildcard) { permissions_referencing_command.push(set.identifier.clone()); } } @@ -386,6 +548,13 @@ impl RuntimeAuthority { permissions_referencing_command.push(permission.identifier.clone()); } } + if manifest.commands.iter().any(|c| c == command_name) { + permissions_referencing_command + .push(format!("allow-{}", command_name.replace('_', "-"))); + } + if allow_wildcard && !manifest.commands.is_empty() { + permissions_referencing_command.push("allow-*".to_string()); + } permissions_referencing_command.sort(); @@ -443,34 +612,55 @@ impl RuntimeAuthority { webview: &str, origin: &Origin, ) -> Option> { - if self + // First command dispatch blocks here if the resolved ACL is still building on the + // background thread (see `RuntimeAuthority::new_async`); subsequent calls are cached. + let inner = self.inner(); + // the `allow-*`/`deny-*` wildcard permissions resolve to a single `*` command (per manifest) + // instead of one entry per command, so we also look the command up under its wildcard key. + let wildcard = wildcard_command(command); + if inner .denied_commands .get(command) + .or_else(|| inner.denied_commands.get(&wildcard)) .map(|resolved| resolved.iter().any(|cmd| origin.matches(&cmd.context))) .is_some() { None } else { - self.allowed_commands.get(command).and_then(|resolved| { - let resolved_cmds = resolved - .iter() - .filter(|cmd| { - origin.matches(&cmd.context) - && (cmd.webviews.iter().any(|w| w.matches(webview)) - || cmd.windows.iter().any(|w| w.matches(window))) - }) - .cloned() - .collect::>(); - if resolved_cmds.is_empty() { - None - } else { - Some(resolved_cmds) - } - }) + let resolved_cmds = inner + .allowed_commands + .get(command) + .into_iter() + .chain(inner.allowed_commands.get(&wildcard)) + .flatten() + .filter(|cmd| { + origin.matches(&cmd.context) + && (cmd.webviews.iter().any(|w| w.matches(webview)) + || cmd.windows.iter().any(|w| w.matches(window))) + }) + .cloned() + .collect::>(); + if resolved_cmds.is_empty() { + None + } else { + Some(resolved_cmds) + } } } } +/// The wildcard command key that matches every command of the same manifest: +/// `*` for app commands and `plugin:$name|*` for plugin commands. +/// +/// Used to resolve the implicit `allow-*`/`deny-*` permissions without expanding them +/// into one entry per command in the resolved ACL. +fn wildcard_command(command: &str) -> String { + match command.rsplit_once('|') { + Some((prefix, _)) => format!("{prefix}|*"), + None => "*".to_string(), + } +} + /// List of allowed and denied objects that match either the command-specific or plugin global scope criteria. #[derive(Debug)] pub struct ScopeValue { @@ -518,7 +708,7 @@ impl CommandScope { .runtime_authority .lock() .unwrap() - .scope_manager + .scope_manager() .get_command_scope_typed::(webview.app_handle(), &scope_id)?; for s in scope.allows() { @@ -637,7 +827,7 @@ impl GlobalScope { .runtime_authority .lock() .unwrap() - .scope_manager + .scope_manager() .get_global_scope_typed(webview.app_handle(), plugin) .map(Self) } @@ -859,6 +1049,46 @@ mod tests { ); } + #[test] + fn wildcard_command_allows_any_app_command() { + let window = "main"; + let webview = "main"; + + // a single `*` entry stands in for every app command (the `allow-*` permission) + let resolved_cmd = vec![ResolvedCommand { + windows: vec![Pattern::new(window).unwrap()], + ..Default::default() + }]; + let allowed_commands = [("*".to_string(), resolved_cmd.clone())] + .into_iter() + .collect(); + + let authority = RuntimeAuthority::new( + Default::default(), + Resolved { + allowed_commands, + ..Default::default() + }, + ); + + // an arbitrary app command (never listed explicitly) is allowed through the wildcard entry + assert_eq!( + authority.resolve_access("some_command", window, webview, &Origin::Local), + Some(resolved_cmd.clone()) + ); + assert_eq!( + authority.resolve_access("another_command", window, webview, &Origin::Local), + Some(resolved_cmd) + ); + + // plugin commands use a per-plugin wildcard key, so the app wildcard does not allow them + assert!( + authority + .resolve_access("plugin:fs|read", window, webview, &Origin::Local) + .is_none() + ); + } + #[test] fn webview_glob_pattern_matches() { let command = "my-command"; @@ -1107,6 +1337,7 @@ mod tests { default_permission: None, permissions: Default::default(), permission_sets: Default::default(), + commands: Default::default(), global_scope_schema: None, }, )] @@ -1194,4 +1425,127 @@ mod tests { "myplugin.my-command-webview-window not allowed on window \"main-*\", webview \"webview-*\", URL: http://localhost:123/\n\nallowed on: [windows: \"main-*\", webviews: \"webview-*\", URL: local], [windows: \"main-*\", webviews: \"webview-*\", URL: http://localhost:8080]\n\nreferenced by: capability: maincap, permission: allow-command || capability: maincap, permission: allow-command" ); } + + // ============================================================================ + // Async (background-built) authority + // ============================================================================ + // + // The `runtime_authority!` macro builds the resolved ACL on a background thread via + // `new_async`; the first authorization read joins it (see `RuntimeAuthority::inner`). These + // tests assert the background-built authority authorizes identically to the eager `new` path. + + /// A resolved ACL with one allowed command (window `main-*`, command scope `1`), one denied + /// command (any window), and a command scope. Built fresh on each call so it can be used both + /// eagerly and as a `new_async` builder. + fn sample_resolved() -> Resolved { + use tauri_utils::acl::resolved::{ResolvedScope, ScopeKey}; + + let allowed_commands = [( + "allowed-command".to_string(), + vec![ResolvedCommand { + windows: vec![Pattern::new("main-*").unwrap()], + scope_id: Some(1 as ScopeKey), + ..Default::default() + }], + )] + .into_iter() + .collect(); + let denied_commands = [( + "denied-command".to_string(), + vec![ResolvedCommand { + windows: vec![Pattern::new("*").unwrap()], + ..Default::default() + }], + )] + .into_iter() + .collect(); + let command_scope = [(1 as ScopeKey, ResolvedScope::default())] + .into_iter() + .collect(); + + Resolved { + allowed_commands, + denied_commands, + command_scope, + ..Default::default() + } + } + + #[test] + fn async_authority_resolves_allowed_command() { + // Built off-thread; `resolve_access` must block on the builder, then authorize correctly. + let authority = RuntimeAuthority::new_async(|| Default::default(), sample_resolved); + + assert!( + authority + .resolve_access("allowed-command", "main-window", "wv", &Origin::Local) + .is_some(), + "allowed command should resolve through the background-built authority" + ); + assert!( + authority + .resolve_access("unknown-command", "main-window", "wv", &Origin::Local) + .is_none(), + "unknown command must not be allowed" + ); + } + + #[test] + fn async_authority_denied_takes_precedence() { + let authority = RuntimeAuthority::new_async(|| Default::default(), sample_resolved); + assert!( + authority + .resolve_access("denied-command", "anything", "wv", &Origin::Local) + .is_none(), + "denied command must be rejected through the background-built authority" + ); + } + + #[test] + fn async_and_eager_authority_agree() { + let eager = RuntimeAuthority::new(Default::default(), sample_resolved()); + let lazy = RuntimeAuthority::new_async(|| Default::default(), sample_resolved); + + for (command, window) in [ + ("allowed-command", "main-1"), + ("allowed-command", "other-1"), + ("denied-command", "main-1"), + ("unknown-command", "main-1"), + ] { + assert_eq!( + eager.resolve_access(command, window, "wv", &Origin::Local), + lazy.resolve_access(command, window, "wv", &Origin::Local), + "eager and background-built authority disagree for command={command} window={window}" + ); + } + } + + #[test] + fn async_authority_scope_manager_materializes() { + // `scope_manager()` must also join the background builder and expose the resolved scopes. + let authority = RuntimeAuthority::new_async(|| Default::default(), sample_resolved); + assert!( + authority.scope_manager().command_scope.contains_key(&1), + "scope manager should expose the resolved command scope after materialization" + ); + } + + #[cfg(debug_assertions)] + #[test] + fn async_authority_resolve_access_message_materializes() { + // The debug-only error path reads the raw ACL through the background-built authority; ensure + // it materializes and produces a denial message without panicking. + let authority = RuntimeAuthority::new_async(|| Default::default(), sample_resolved); + let message = authority.resolve_access_message( + super::APP_ACL_KEY, + "denied-command", + "win", + "wv", + &Origin::Local, + ); + assert!( + message.contains("denied"), + "expected a denial message, got: {message}" + ); + } } diff --git a/crates/tauri/src/ipc/protocol.rs b/crates/tauri/src/ipc/protocol.rs index 3ecd1b7f4ef1..9f3860b9dee8 100644 --- a/crates/tauri/src/ipc/protocol.rs +++ b/crates/tauri/src/ipc/protocol.rs @@ -466,15 +466,11 @@ fn parse_invoke_request( (body, content_type) = crate::utils::pattern::isolation::RawIsolationPayload::try_from(&body) .and_then(|raw| { - let content_type = raw.content_type().clone(); - crypto_keys.decrypt(raw).map(|decrypted| { - ( - decrypted, - content_type - .parse() - .unwrap_or(mime::APPLICATION_OCTET_STREAM), - ) - }) + let content_type = raw + .content_type() + .parse() + .unwrap_or(mime::APPLICATION_OCTET_STREAM); + Ok((crypto_keys.decrypt(raw)?, content_type)) }) .map_err(|e| e.to_string())?; } @@ -571,6 +567,8 @@ mod tests { PluginStore::default(), Box::new(|_| false), None, + #[cfg(any(target_os = "macos", target_os = "ios"))] + None, Default::default(), StateManager::new(), Default::default(), @@ -687,6 +685,8 @@ mod tests { PluginStore::default(), Box::new(|_| false), None, + #[cfg(any(target_os = "macos", target_os = "ios"))] + None, Default::default(), StateManager::new(), Default::default(), diff --git a/crates/tauri/src/lib.rs b/crates/tauri/src/lib.rs index afee655adf04..fde25aa012a3 100644 --- a/crates/tauri/src/lib.rs +++ b/crates/tauri/src/lib.rs @@ -14,6 +14,7 @@ //! - **cef**: Enables the [CEF](https://github.com/chromiumembedded/cef) runtime. // - **common-controls-v6** *(enabled by default)*: Enables [Common Controls v6](https://learn.microsoft.com/en-us/windows/win32/controls/common-control-versions) support on Windows, mainly for the predefined `about` menu item. //! - **x11** *(enabled by default)*: Enables X11 support. Disable this if you only target Wayland. +//! - **dbus** *(enabled by default)*: Enables dbus dependency for theme support on Linux. Disable this if you do not need theme support or don't want to build the dbus rust crate. The WebView dependencies use dbus either way. //! - **unstable**: Enables unstable features. Be careful, it might introduce breaking changes in future minor releases. //! - **tracing**: Enables [`tracing`](https://docs.rs/tracing/latest/tracing) for window startup, plugins, `Window::eval`, events, IPC, updater and custom protocol request handlers. //! - **test**: Enables the [`mod@test`] module exposing unit test helpers. @@ -33,10 +34,10 @@ //! - **compression** *(enabled by default): Enables asset compression. You should only disable this if you want faster compile times in release builds - it produces larger binaries. //! - **config-json5**: Adds support to JSON5 format for `tauri.conf.json`. //! - **config-toml**: Adds support to TOML format for the configuration `Tauri.toml`. -//! - **image-ico**: Adds support to parse `.ico` image, see [`Image`]. -//! - **image-png**: Adds support to parse `.png` image, see [`Image`]. +//! - **image-ico**: Adds support to parse `.ico` image, see [`image::Image`]. +//! - **image-png**: Adds support to parse `.png` image, see [`image::Image`]. //! - **macos-proxy**: Adds support for [`WebviewBuilder::proxy_url`] on macOS. Requires macOS 14+. -//! - **specta**: Add support for [`specta::specta`](https://docs.rs/specta/%5E2.0.0-rc.9/specta/attr.specta.html) with Tauri arguments such as [`State`](crate::State), [`Window`](crate::Window) and [`AppHandle`](crate::AppHandle) +//! - **specta**: Add support for [`specta::specta`](https://docs.rs/specta/%5E2.0.0-rc.9/specta/attr.specta.html) with Tauri arguments such as [`State`], [`Window`] and [`AppHandle`] //! - **dynamic-acl** *(enabled by default)*: Enables you to add ACLs at runtime, notably it enables the [`Manager::add_capability`] function. //! //! ## Cargo allowlist features @@ -159,14 +160,7 @@ macro_rules! android_binding { ::tauri::wry::android_binding!($domain, $app_name, $wry); - ::tauri::tao::android_binding!( - $domain, - $app_name, - WryActivity, - android_setup, - $main, - ::tauri::tao - ); + ::tauri::tao::android_binding!($domain, $app_name, Rust, android_setup, $main, ::tauri::tao); // be careful when renaming this, the `Java_app_tauri_plugin_PluginManager_handlePluginResponse` symbol is checked by the CLI ::tauri::tao::platform::android::prelude::android_fn!( @@ -223,14 +217,6 @@ use std::{ }; use utils::assets::{AssetKey, CspHash, EmbeddedAssets}; -#[cfg(feature = "wry")] -#[cfg_attr(docsrs, doc(cfg(feature = "wry")))] -pub use tauri_runtime_wry::webview_version; - -#[cfg(feature = "cef")] -#[cfg_attr(docsrs, doc(cfg(feature = "cef")))] -pub use tauri_runtime_cef::webview_version; - #[cfg(target_os = "macos")] #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] pub use runtime::ActivationPolicy; @@ -341,6 +327,22 @@ pub const fn is_dev() -> bool { !cfg!(feature = "custom-protocol") } +// TODO: Fix the error types +/// Get WebView/Webkit version on current platform. +pub fn webview_version() -> Result { + #[cfg(feature = "cef")] + if let Ok(v) = tauri_runtime_cef::webview_version() { + return Ok(v); + } + + #[cfg(feature = "wry")] + if let Ok(v) = tauri_runtime_wry::webview_version() { + return Ok(v); + } + + Ok("0.0.0".to_string()) +} + /// Represents a container of file assets that are retrievable during runtime. pub trait Assets: Send + Sync + 'static { /// Initialize the asset provider. @@ -1093,6 +1095,10 @@ pub(crate) mod sealed { fn manager_owned(&self) -> Arc>; fn runtime(&self) -> RuntimeOrDispatch<'_, R>; fn managed_app_handle(&self) -> &AppHandle; + #[cfg(target_os = "android")] + fn activity_name(&self) -> Option>; + #[cfg(target_os = "ios")] + fn scene_identifier(&self) -> Option>; } } diff --git a/crates/tauri/src/manager/mod.rs b/crates/tauri/src/manager/mod.rs index 9f9e36b2328a..bee66cc57576 100644 --- a/crates/tauri/src/manager/mod.rs +++ b/crates/tauri/src/manager/mod.rs @@ -31,6 +31,9 @@ use crate::{ utils::{PackageInfo, config::Config}, }; +#[cfg(any(target_os = "macos", target_os = "ios"))] +use crate::app::OnWebContentProcessTerminate; + #[cfg(desktop)] mod menu; #[cfg(all(desktop, feature = "tray-icon"))] @@ -251,6 +254,9 @@ impl AppManager { plugins: PluginStore, invoke_handler: Box>, on_page_load: Option>>, + #[cfg(any(target_os = "macos", target_os = "ios"))] on_web_content_process_terminate: Option< + Arc>, + >, uri_scheme_protocols: HashMap>>, state: StateManager, #[cfg(desktop)] menu_event_listener: Vec>>, @@ -284,6 +290,8 @@ impl AppManager { webviews: Mutex::default(), invoke_handler, on_page_load, + #[cfg(any(target_os = "macos", target_os = "ios"))] + on_web_content_process_terminate, uri_scheme_protocols: Mutex::new(uri_scheme_protocols), event_listeners: Arc::new(webview_event_listeners), invoke_initialization_script, @@ -360,6 +368,7 @@ impl AppManager { } } + // TODO: Change to return `crate::Result` here in v3 pub fn get_asset( &self, mut path: String, @@ -405,46 +414,39 @@ impl AppManager { asset_path = fallback; asset }) - .ok_or_else(|| crate::Error::AssetNotFound(path.clone())) - .map(Cow::into_owned); + .ok_or_else(|| { + let error = crate::Error::AssetNotFound(path.clone()); + log::error!("{error}"); + Box::new(error) + })?; let mut csp_header = None; let is_html = asset_path.as_ref().ends_with(".html"); - match asset_response { - Ok(asset) => { - let final_data = if is_html { - let mut asset = String::from_utf8_lossy(&asset).into_owned(); - if let Some(csp) = self.csp() { - #[allow(unused_mut)] - let mut csp_map = set_csp(&mut asset, &self.assets, &asset_path, self, csp); - #[cfg(feature = "isolation")] - if let Pattern::Isolation { schema, .. } = &*self.pattern { - let default_src = csp_map - .entry("default-src".into()) - .or_insert_with(Default::default); - default_src.push(R::custom_scheme_url(schema, _use_https_schema)); - } - - csp_header.replace(Csp::DirectiveMap(csp_map).to_string()); - } + let final_data = if is_html { + let mut asset = String::from_utf8_lossy(&asset_response).into_owned(); + if let Some(csp) = self.csp() { + #[allow(unused_mut)] + let mut csp_map = set_csp(&mut asset, &self.assets, &asset_path, self, csp); + #[cfg(feature = "isolation")] + if let Pattern::Isolation { schema, .. } = &*self.pattern { + let default_src = csp_map.entry("default-src".to_owned()).or_default(); + default_src.push(R::custom_scheme_url(schema, _use_https_schema)); + } - asset.into_bytes() - } else { - asset - }; - let mime_type = tauri_utils::mime_type::MimeType::parse(&final_data, &path); - Ok(Asset { - bytes: final_data, - mime_type, - csp_header, - }) - } - Err(e) => { - log::error!("{:?}", e); - Err(Box::new(e)) + csp_header.replace(Csp::DirectiveMap(csp_map).to_string()); } - } + + asset.into_bytes() + } else { + asset_response.into_owned() + }; + let mime_type = tauri_utils::mime_type::MimeType::parse(&final_data, &path); + Ok(Asset { + bytes: final_data, + mime_type, + csp_header, + }) } pub(crate) fn listeners(&self) -> &Listeners { @@ -747,6 +749,8 @@ mod test { PluginStore::default(), Box::new(|_| false), None, + #[cfg(any(target_os = "macos", target_os = "ios"))] + None, Default::default(), StateManager::new(), Default::default(), diff --git a/crates/tauri/src/manager/webview.rs b/crates/tauri/src/manager/webview.rs index de6e6675dd49..463497dfeacb 100644 --- a/crates/tauri/src/manager/webview.rs +++ b/crates/tauri/src/manager/webview.rs @@ -6,7 +6,6 @@ use std::{ borrow::Cow, collections::{HashMap, HashSet}, fmt, - fs::create_dir_all, sync::{Arc, Mutex, MutexGuard}, }; @@ -28,6 +27,9 @@ use crate::{ webview::PageLoadPayload, }; +#[cfg(any(target_os = "macos", target_os = "ios"))] +use crate::app::OnWebContentProcessTerminate; + use super::{ window::{DRAG_DROP_EVENT, DRAG_ENTER_EVENT, DRAG_LEAVE_EVENT, DRAG_OVER_EVENT, DragDropPayload}, {AppManager, EmitPayload}, @@ -37,7 +39,7 @@ use super::{ // and we do not get a secure context without the custom protocol that proxies to the dev server // additionally, we need the custom protocol to inject the initialization scripts on Android // must also keep in sync with the `let mut response` assignment in prepare_uri_scheme_protocol -pub(crate) const PROXY_DEV_SERVER: bool = cfg!(all(dev, any(mobile, feature = "cef"))); +pub(crate) const PROXY_DEV_SERVER: bool = cfg!(all(dev, any(mobile))); pub(crate) const PROCESS_IPC_MESSAGE_FN: &str = include_str!("../../scripts/process-ipc-message-fn.js"); @@ -70,6 +72,9 @@ pub struct WebviewManager { pub invoke_handler: Box>, /// The page load hook, invoked when the webview performs a navigation. pub on_page_load: Option>>, + /// The web content process termination hook. + #[cfg(any(target_os = "macos", target_os = "ios"))] + pub on_web_content_process_terminate: Option>>, /// The webview protocols available to all webviews. pub uri_scheme_protocols: Mutex>>>, /// Webview event listeners to all webviews. @@ -299,6 +304,28 @@ impl WebviewManager { } })); + #[cfg(any(target_os = "macos", target_os = "ios"))] + if pending.on_web_content_process_terminate_handler.is_none() { + let app_manager_ = manager.manager_owned(); + if app_manager_ + .webview + .on_web_content_process_terminate + .is_some() + { + let label_ = pending.label.clone(); + pending + .on_web_content_process_terminate_handler + .replace(Box::new(move || { + if let Some(w) = app_manager_.get_webview(&label_) + && let Some(on_web_content_process_terminate) = + &app_manager_.webview.on_web_content_process_terminate + { + on_web_content_process_terminate(&w); + } + })); + } + } + #[cfg(feature = "protocol-asset")] if !registered_scheme_protocols.contains(&"asset".into()) { let asset_scope = app_manager @@ -474,8 +501,8 @@ impl WebviewManager { let html = String::from_utf8_lossy(&body).into_owned(); // naive way to check if it's an html if html.contains('<') && html.contains('>') { - let document = tauri_utils::html::parse(html); - tauri_utils::html::inject_csp(&document, &csp.to_string()); + let document = tauri_utils::html2::parse(html); + tauri_utils::html2::inject_csp(&document, &csp.to_string()); url.set_path(&format!("{},{document}", mime::TEXT_HTML)); } } @@ -527,13 +554,6 @@ impl WebviewManager { } } - // make sure the directory is created and available to prevent a panic - if let Some(user_data_dir) = &pending.webview_attributes.data_directory - && !user_data_dir.exists() - { - create_dir_all(user_data_dir)?; - } - #[cfg(all(desktop, not(target_os = "windows")))] if pending.webview_attributes.zoom_hotkeys_enabled { #[derive(Template)] @@ -644,7 +664,9 @@ impl WebviewManager { { webview .with_webview(|w| { - unsafe { crate::ios::on_webview_created(w.inner() as _, w.view_controller() as _) }; + if let Some(w) = w.as_any().downcast_ref::() { + unsafe { crate::ios::on_webview_created(w.inner() as _, w.view_controller() as _) }; + } }) .expect("failed to run on_webview_created hook"); } diff --git a/crates/tauri/src/manager/window.rs b/crates/tauri/src/manager/window.rs index 0db48529f8b2..f91bc42d0262 100644 --- a/crates/tauri/src/manager/window.rs +++ b/crates/tauri/src/manager/window.rs @@ -37,6 +37,10 @@ pub(crate) const DRAG_ENTER_EVENT: EventName<&str> = EventName::from_str("tauri: pub(crate) const DRAG_OVER_EVENT: EventName<&str> = EventName::from_str("tauri://drag-over"); pub(crate) const DRAG_DROP_EVENT: EventName<&str> = EventName::from_str("tauri://drag-drop"); pub(crate) const DRAG_LEAVE_EVENT: EventName<&str> = EventName::from_str("tauri://drag-leave"); +#[cfg(mobile)] +pub(crate) const WINDOW_SUSPENDED_EVENT: EventName<&str> = EventName::from_str("tauri://suspended"); +#[cfg(mobile)] +pub(crate) const WINDOW_RESUMED_EVENT: EventName<&str> = EventName::from_str("tauri://resumed"); pub struct WindowManager { pub windows: Mutex>>, @@ -265,6 +269,10 @@ fn on_window_event(window: &Window, event: &WindowEvent) -> crate _ => unimplemented!(), }, WindowEvent::ThemeChanged(theme) => window.emit_to_window(WINDOW_THEME_CHANGED, &theme)?, + #[cfg(mobile)] + WindowEvent::Suspended => window.emit_to_window(WINDOW_SUSPENDED_EVENT, &())?, + #[cfg(mobile)] + WindowEvent::Resumed => window.emit_to_window(WINDOW_RESUMED_EVENT, &())?, } Ok(()) } diff --git a/crates/tauri/src/menu/builders/menu.rs b/crates/tauri/src/menu/builders/menu.rs index 3da31f52a746..61819abf2a42 100644 --- a/crates/tauri/src/menu/builders/menu.rs +++ b/crates/tauri/src/menu/builders/menu.rs @@ -642,6 +642,31 @@ macro_rules! shared_menu_builder { .push(PredefinedMenuItem::services(self.manager, Some(text.as_ref())).map(|i| i.kind())); self } + + /// Add Bring All to Front menu item to the menu. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn bring_all_to_front(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::bring_all_to_front(self.manager, None).map(|i| i.kind())); + self + } + + /// Add Bring All to Front menu item with specified text to the menu. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn bring_all_to_front_with_text>(mut self, text: S) -> Self { + self.items.push( + PredefinedMenuItem::bring_all_to_front(self.manager, Some(text.as_ref())) + .map(|i| i.kind()), + ); + self + } } }; } diff --git a/crates/tauri/src/menu/plugin.rs b/crates/tauri/src/menu/plugin.rs index 308421531e88..748c131b465c 100644 --- a/crates/tauri/src/menu/plugin.rs +++ b/crates/tauri/src/menu/plugin.rs @@ -90,6 +90,7 @@ enum Predefined { Quit, About(Option), Services, + BringAllToFront, } #[derive(Deserialize)] @@ -302,6 +303,9 @@ impl PredefinedMenuItemPayload { PredefinedMenuItem::about(webview, self.text.as_deref(), metadata) } Predefined::Services => PredefinedMenuItem::services(webview, self.text.as_deref()), + Predefined::BringAllToFront => { + PredefinedMenuItem::bring_all_to_front(webview, self.text.as_deref()) + } } } } @@ -810,7 +814,7 @@ fn set_as_windows_menu_for_nsapp( { let resources_table = webview.resources_table(); let submenu = resources_table.get::>(rid)?; - submenu.set_as_help_menu_for_nsapp()?; + submenu.set_as_windows_menu_for_nsapp()?; } let _ = rid; diff --git a/crates/tauri/src/menu/predefined.rs b/crates/tauri/src/menu/predefined.rs index 1023f8162470..cfb4b2a8296e 100644 --- a/crates/tauri/src/menu/predefined.rs +++ b/crates/tauri/src/menu/predefined.rs @@ -384,6 +384,29 @@ impl PredefinedMenuItem { Ok(Self(Arc::new(item))) } + /// Bring All to Front menu item + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn bring_all_to_front>(manager: &M, text: Option<&str>) -> crate::Result { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + + let text = text.map(|t| t.to_owned()); + + let item = run_main_thread!(handle, || { + let item = muda::PredefinedMenuItem::bring_all_to_front(text.as_deref()); + PredefinedMenuItemInner { + id: item.id().clone(), + inner: Some(item), + app_handle, + } + })?; + + Ok(Self(Arc::new(item))) + } + /// Returns a unique identifier associated with this menu item. pub fn id(&self) -> &MenuId { &self.0.id diff --git a/crates/tauri/src/path/mod.rs b/crates/tauri/src/path/mod.rs index 7e57e8795e97..4188611c7ce6 100644 --- a/crates/tauri/src/path/mod.rs +++ b/crates/tauri/src/path/mod.rs @@ -73,6 +73,12 @@ impl FromStr for SafePathBuf { } } +impl From for PathBuf { + fn from(path: SafePathBuf) -> Self { + path.0 + } +} + impl<'de> Deserialize<'de> for SafePathBuf { fn deserialize(deserializer: D) -> std::result::Result where diff --git a/crates/tauri/src/plugin.rs b/crates/tauri/src/plugin.rs index f4e07f9fe7e9..f9dbbdd9dcbe 100644 --- a/crates/tauri/src/plugin.rs +++ b/crates/tauri/src/plugin.rs @@ -173,7 +173,7 @@ impl PluginApi { .runtime_authority .lock() .unwrap() - .scope_manager + .scope_manager() .get_global_scope_typed(&self.handle, self.name) } } diff --git a/crates/tauri/src/plugin/mobile.rs b/crates/tauri/src/plugin/mobile.rs index eb09b2e0fe59..f023d250f284 100644 --- a/crates/tauri/src/plugin/mobile.rs +++ b/crates/tauri/src/plugin/mobile.rs @@ -95,12 +95,15 @@ pub fn handle_android_plugin_response( (false, false) => unreachable!(), }; - if let Some(handler) = PENDING_PLUGIN_CALLS + // Drop the lock before invoking the handler: it delivers the command response + // to the webview (which can block on the UI thread), and holding + // PENDING_PLUGIN_CALLS across that call deadlocks a concurrent `run_command`. + let handler = PENDING_PLUGIN_CALLS .get_or_init(Default::default) .lock() .unwrap() - .remove(&id) - { + .remove(&id); + if let Some(handler) = handler { handler(if is_ok { Ok(payload) } else { Err(payload) }); } } @@ -115,12 +118,16 @@ pub fn send_channel_data( let data: serde_json::Value = serde_json::from_str(env.get_string(&data_str).unwrap().to_str().unwrap()).unwrap(); - if let Some(channel) = CHANNELS + // Clone the channel out and drop the lock before send(): send() can block + // delivering to the webview, and holding CHANNELS across it deadlocks a + // concurrent channel registration/send. + let channel = CHANNELS .get_or_init(Default::default) .lock() .unwrap() .get(&(channel_id as u32)) - { + .cloned(); + if let Some(channel) = channel { let _ = channel.send(data); } } @@ -165,12 +172,17 @@ impl PluginApi { let config = self.raw_config.clone(); webview .with_webview(move |w| { + let webview_ptr = w + .as_any() + .downcast_ref::() + .map(|w| w.inner()) + .unwrap_or(std::ptr::null_mut()); unsafe { crate::ios::register_plugin( &name.into(), init_fn(), &serde_json::to_string(&config).unwrap().as_str().into(), - w.inner() as _, + webview_ptr as _, ) }; tx.send(()).unwrap(); @@ -373,12 +385,12 @@ pub(crate) fn run_command, F: FnOnce(PluginResponse) + CStr::from_ptr(payload) }; - if let Some(handler) = PENDING_PLUGIN_CALLS + let handler = PENDING_PLUGIN_CALLS .get_or_init(Default::default) .lock() .unwrap() - .remove(&id) - { + .remove(&id); + if let Some(handler) = handler { let json = payload.to_str().unwrap(); match serde_json::from_str(json) { Ok(payload) => { @@ -401,12 +413,16 @@ pub(crate) fn run_command, F: FnOnce(PluginResponse) + CStr::from_ptr(payload) }; - if let Some(channel) = CHANNELS + // Clone the channel out and drop the lock before send(): send() can block + // delivering to the webview, and holding CHANNELS across it deadlocks a + // concurrent channel registration/send. + let channel = CHANNELS .get_or_init(Default::default) .lock() .unwrap() .get(&(id as u32)) - { + .cloned(); + if let Some(channel) = channel { let payload: serde_json::Value = serde_json::from_str(payload.to_str().unwrap()).unwrap(); let _ = channel.send(payload); } diff --git a/crates/tauri/src/protocol/asset.rs b/crates/tauri/src/protocol/asset.rs index 86334ffc08c9..7403428cecd7 100644 --- a/crates/tauri/src/protocol/asset.rs +++ b/crates/tauri/src/protocol/asset.rs @@ -5,10 +5,10 @@ use crate::{path::SafePathBuf, scope, webview::UriSchemeProtocolHandler}; use http::{Request, Response, header::*, status::StatusCode}; use http_range::HttpRange; +use std::fs::File; +use std::io::{Read, Seek, Write}; use std::{borrow::Cow, io::SeekFrom}; use tauri_utils::mime_type::MimeType; -use tokio::fs::File; -use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt}; pub fn get(scope: scope::fs::Scope, window_origin: String) -> UriSchemeProtocolHandler { Box::new( @@ -49,7 +49,7 @@ fn get_response( } // Separate block for easier error handling - let mut file = match crate::async_runtime::safe_block_on(File::open(path.clone())) { + let mut file = match File::open(path.clone()) { Ok(file) => file, Err(e) => { #[cfg(target_os = "android")] @@ -74,32 +74,20 @@ fn get_response( } }; - let (mut file, len, mime_type, read_bytes) = crate::async_runtime::safe_block_on(async move { - // get file length - let len = { - let old_pos = file.stream_position().await?; - let len = file.seek(SeekFrom::End(0)).await?; - file.seek(SeekFrom::Start(old_pos)).await?; - len - }; - + let len = file.metadata()?.len(); + let (mime_type, read_bytes) = { // get file mime type - let (mime_type, read_bytes) = { - let nbytes = len.min(8192); - let mut magic_buf = Vec::with_capacity(nbytes as usize); - let old_pos = file.stream_position().await?; - (&mut file).take(nbytes).read_to_end(&mut magic_buf).await?; - file.seek(SeekFrom::Start(old_pos)).await?; - ( - MimeType::parse(&magic_buf, &path), - // return the `magic_bytes` if we read the whole file - // to avoid reading it again later if this is not a range request - if len < 8192 { Some(magic_buf) } else { None }, - ) - }; - - Ok::<(File, u64, String, Option>), anyhow::Error>((file, len, mime_type, read_bytes)) - })?; + let nbytes = len.min(8192); + let mut magic_buf = Vec::with_capacity(nbytes as usize); + (&mut file).take(nbytes).read_to_end(&mut magic_buf)?; + file.rewind()?; + ( + MimeType::parse(&magic_buf, &path), + // return the `magic_bytes` if we read the whole file + // to avoid reading it again later if this is not a range request + if len < 8192 { Some(magic_buf) } else { None }, + ) + }; resp = resp.header(CONTENT_TYPE, &mime_type); @@ -152,12 +140,12 @@ fn get_response( // calculate number of bytes needed to be read let nbytes = end + 1 - start; - let buf = crate::async_runtime::safe_block_on(async move { + let buf = { let mut buf = Vec::with_capacity(nbytes as usize); - file.seek(SeekFrom::Start(start)).await?; - file.take(nbytes).read_to_end(&mut buf).await?; - Ok::, anyhow::Error>(buf) - })?; + file.seek(SeekFrom::Start(start))?; + file.take(nbytes).read_to_end(&mut buf)?; + buf + }; resp = resp.header(CONTENT_RANGE, format!("bytes {start}-{end}/{len}")); resp = resp.header(CONTENT_LENGTH, end + 1 - start); @@ -190,38 +178,34 @@ fn get_response( format!("multipart/byteranges; boundary={boundary}"), ); - let buf = crate::async_runtime::safe_block_on(async move { + let buf = { // multi-part range header let mut buf = Vec::new(); for (start, end) in ranges { // a new range is being written, write the range boundary - buf.write_all(boundary_sep.as_bytes()).await?; + buf.write_all(boundary_sep.as_bytes())?; // write the needed headers `Content-Type` and `Content-Range` - buf - .write_all(format!("{CONTENT_TYPE}: {mime_type}\r\n").as_bytes()) - .await?; - buf - .write_all(format!("{CONTENT_RANGE}: bytes {start}-{end}/{len}\r\n").as_bytes()) - .await?; + buf.write_all(format!("{CONTENT_TYPE}: {mime_type}\r\n").as_bytes())?; + buf.write_all(format!("{CONTENT_RANGE}: bytes {start}-{end}/{len}\r\n").as_bytes())?; // write the separator to indicate the start of the range body - buf.write_all("\r\n".as_bytes()).await?; + buf.write_all("\r\n".as_bytes())?; // calculate number of bytes needed to be read let nbytes = end + 1 - start; let mut local_buf = Vec::with_capacity(nbytes as usize); - file.seek(SeekFrom::Start(start)).await?; - (&mut file).take(nbytes).read_to_end(&mut local_buf).await?; + file.seek(SeekFrom::Start(start))?; + (&mut file).take(nbytes).read_to_end(&mut local_buf)?; buf.extend_from_slice(&local_buf); } // all ranges have been written, write the closing boundary - buf.write_all(boundary_closer.as_bytes()).await?; + buf.write_all(boundary_closer.as_bytes())?; - Ok::, anyhow::Error>(buf) - })?; + buf + }; resp.body(buf.into()) } } else if request.method() == http::Method::HEAD { @@ -234,11 +218,9 @@ fn get_response( let buf = if let Some(b) = read_bytes { b } else { - crate::async_runtime::safe_block_on(async move { - let mut local_buf = Vec::with_capacity(len as usize); - file.read_to_end(&mut local_buf).await?; - Ok::, anyhow::Error>(local_buf) - })? + let mut local_buf = Vec::with_capacity(len as usize); + file.read_to_end(&mut local_buf)?; + local_buf }; resp = resp.header(CONTENT_LENGTH, len); resp.body(buf.into()) diff --git a/crates/tauri/src/protocol/tauri.rs b/crates/tauri/src/protocol/tauri.rs index 95617fe8698c..db2cd5a7465d 100644 --- a/crates/tauri/src/protocol/tauri.rs +++ b/crates/tauri/src/protocol/tauri.rs @@ -21,19 +21,17 @@ use crate::{ struct CachedResponse { status: http::StatusCode, headers: http::HeaderMap, - body: bytes::Bytes, + body: Vec, } pub fn get( - #[allow(unused_variables)] manager: Arc>, + manager: Arc>, window_origin: &str, web_resource_request_handler: Option>, ) -> UriSchemeProtocolHandler { + let use_https = window_origin.starts_with("https"); let url = { - let mut url = manager - .get_app_url(window_origin.starts_with("https")) - .as_str() - .to_string(); + let mut url = manager.get_app_url(use_https).as_str().to_string(); if url.ends_with('/') { url.pop(); } @@ -42,36 +40,98 @@ pub fn get( let window_origin = window_origin.to_string(); - let response_cache = Arc::new(Mutex::new(HashMap::new())); + #[allow(unused_mut)] + let mut client_builder = reqwest::ClientBuilder::new(); + if use_https { + #[cfg(feature = "rustls-tls")] + if rustls::crypto::CryptoProvider::get_default().is_none() { + let _ = rustls::crypto::ring::default_provider().install_default(); + } - Box::new(move |_, request, responder| { - match get_response( - request, - &manager, - &window_origin, - web_resource_request_handler.as_deref(), - (&url, &response_cache), - ) { - Ok(response) => responder.respond(response), - Err(e) => responder.respond( - HttpResponse::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .header(CONTENT_TYPE, mime::TEXT_PLAIN.essence_str()) - .header("Access-Control-Allow-Origin", &window_origin) - .body(e.to_string().into_bytes()) - .unwrap(), - ), + // we can't load env vars at runtime, gotta embed them in the lib + #[allow(unused_variables)] + if let Some(cert_pem) = option_env!("TAURI_DEV_ROOT_CERTIFICATE") { + #[cfg(any( + feature = "native-tls", + feature = "native-tls-vendored", + feature = "rustls-tls" + ))] + { + log::info!("adding dev server root certificate"); + let certificate = reqwest::Certificate::from_pem(cert_pem.as_bytes()) + .expect("failed to parse TAURI_DEV_ROOT_CERTIFICATE"); + client_builder = client_builder.tls_certs_merge([certificate]); + } + + #[cfg(not(any( + feature = "native-tls", + feature = "native-tls-vendored", + feature = "rustls-tls" + )))] + { + log::warn!( + "the dev root-certificate-path option was provided, but you must enable one of the following Tauri features in Cargo.toml: native-tls, native-tls-vendored, rustls-tls" + ); + } + } else { + log::warn!( + "loading HTTPS URL; you might need to provide a certificate via the `dev --root-certificate-path` option. You must enable one of the following Tauri features in Cargo.toml: native-tls, native-tls-vendored, rustls-tls" + ); } + } + let client = client_builder.build().unwrap(); + + let response_cache = Mutex::new(HashMap::new()); + + let context = Arc::new(Context { + manager, + web_resource_request_handler, + window_origin, + client, + url, + response_cache, + }); + + Box::new(move |_, request, responder| { + let context = context.clone(); + crate::async_runtime::spawn(async move { + match get_response(&context, request).await { + Ok(response) => responder.respond(response), + Err(e) => responder.respond( + HttpResponse::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .header(CONTENT_TYPE, mime::TEXT_PLAIN.essence_str()) + .header("Access-Control-Allow-Origin", &context.window_origin) + .body(e.to_string().into_bytes()) + .unwrap(), + ), + } + }); }) } -fn get_response( - #[allow(unused_mut)] mut request: Request>, - #[allow(unused_variables)] manager: &AppManager, - window_origin: &str, - web_resource_request_handler: Option<&WebResourceRequestHandler>, - (url, response_cache): (&str, &Arc>>), +struct Context { + manager: Arc>, + window_origin: String, + web_resource_request_handler: Option>, + url: String, + client: reqwest::Client, + response_cache: Mutex>, +} + +async fn get_response( + context: &Context, + request: Request>, ) -> Result>, Box> { + let Context { + manager, + web_resource_request_handler, + window_origin, + client, + url, + response_cache, + } = context; + let proxy_dev_server = PROXY_DEV_SERVER && manager.assets.iter().next().is_none(); // use the entire URI as we are going to proxy the request let path = if proxy_dev_server { @@ -95,115 +155,13 @@ fn get_response( .map(|p| p.to_string()) .unwrap_or_default(); + #[allow(unused_mut)] let mut builder = HttpResponse::builder() .add_configured_headers(manager.config.app.security.headers.as_ref()) .header("Access-Control-Allow-Origin", window_origin); let mut response = if proxy_dev_server { - let decoded_path = percent_encoding::percent_decode(path.as_bytes()) - .decode_utf8_lossy() - .to_string(); - let url = format!( - "{}/{}", - url.trim_end_matches('/'), - decoded_path.trim_start_matches('/') - ); - - #[cfg(feature = "rustls-tls")] - if rustls::crypto::CryptoProvider::get_default().is_none() { - let _ = rustls::crypto::ring::default_provider().install_default(); - } - - #[allow(unused_mut)] - let mut client = reqwest::ClientBuilder::new(); - - if url.starts_with("https://") { - // we can't load env vars at runtime, gotta embed them in the lib - if let Some(cert_pem) = option_env!("TAURI_DEV_ROOT_CERTIFICATE") { - #[cfg(any( - feature = "native-tls", - feature = "native-tls-vendored", - feature = "rustls-tls" - ))] - { - log::info!("adding dev server root certificate"); - let certificate = reqwest::Certificate::from_pem(cert_pem.as_bytes()) - .expect("failed to parse TAURI_DEV_ROOT_CERTIFICATE"); - client = client.tls_certs_merge([certificate]); - } - - #[cfg(not(any( - feature = "native-tls", - feature = "native-tls-vendored", - feature = "rustls-tls" - )))] - { - let _cert_pem = cert_pem; - log::warn!( - "the dev root-certificate-path option was provided, but you must enable one of the following Tauri features in Cargo.toml: native-tls, native-tls-vendored, rustls-tls" - ); - } - } else { - log::warn!( - "loading HTTPS URL; you might need to provide a certificate via the `dev --root-certificate-path` option. You must enable one of the following Tauri features in Cargo.toml: native-tls, native-tls-vendored, rustls-tls" - ); - } - } - - let mut proxy_builder = client - .build() - .unwrap() - .request(request.method().clone(), &url); - proxy_builder = proxy_builder.body(std::mem::take(request.body_mut())); - for (name, value) in request.headers() { - proxy_builder = proxy_builder.header(name, value); - } - proxy_builder = proxy_builder.body(request.body().clone()); - match crate::async_runtime::safe_block_on(proxy_builder.send()) { - Ok(r) => { - let mut response_cache_ = response_cache.lock().unwrap(); - let mut response = None; - if r.status() == http::StatusCode::NOT_MODIFIED { - response = response_cache_.get(&url); - } - let response = if let Some(r) = response { - r - } else { - let status = r.status(); - let headers = r.headers().clone(); - let body = crate::async_runtime::safe_block_on(r.bytes())?; - let response = CachedResponse { - status, - headers, - body, - }; - response_cache_.insert(url.clone(), response); - response_cache_.get(&url).unwrap() - }; - for (name, value) in &response.headers { - builder = builder.header(name, value); - } - builder - .status(response.status) - .body(response.body.to_vec().into())? - } - Err(e) => { - let error_message = format!( - "Failed to request {}: {}{}", - url.as_str(), - e, - if let Some(s) = e.status() { - format!("status code: {}", s.as_u16()) - } else if cfg!(target_os = "ios") { - ", did you grant local network permissions? That is required to reach the development server. Please grant the permission via the prompt or in `Settings > Privacy & Security > Local Network` and restart the app. See https://support.apple.com/en-us/102229 for more information.".to_string() - } else { - "".to_string() - } - ); - log::error!("{error_message}"); - return Err(error_message.into()); - } - } + proxy_dev_request(client, url, response_cache, path, builder, &request).await? } else { let use_https_scheme = request.uri().scheme() == Some(&http::uri::Scheme::HTTPS); let asset = manager.get_asset(path, use_https_scheme)?; @@ -220,3 +178,76 @@ fn get_response( Ok(response) } + +async fn proxy_dev_request( + client: &reqwest::Client, + url: &str, + response_cache: &Mutex>, + path: String, + mut builder: http::response::Builder, + request: &Request>, +) -> Result>, Box> { + let decoded_path = percent_encoding::percent_decode(path.as_bytes()) + .decode_utf8_lossy() + .to_string(); + let url = format!( + "{}/{}", + url.trim_end_matches('/'), + decoded_path.trim_start_matches('/') + ); + + let mut proxy_builder = client.request(request.method().clone(), &url); + for (name, value) in request.headers() { + proxy_builder = proxy_builder.header(name, value); + } + proxy_builder = proxy_builder.body(request.body().clone()); + + let response = proxy_builder.send().await.map_err(|e|{ + let error_message = format!( + "Failed to request {url}: {e}{}", + if let Some(s) = e.status() { + format!("status code: {}", s.as_u16()) + } else if cfg!(target_os = "ios") { + ", did you grant local network permissions? That is required to reach the development server. Please grant the permission via the prompt or in `Settings > Privacy & Security > Local Network` and restart the app. See https://support.apple.com/en-us/102229 for more information.".to_string() + } else { + "".to_string() + } + ); + log::error!("{error_message}"); + error_message + })?; + + let status = response.status(); + + if status == http::StatusCode::NOT_MODIFIED + && let Some(response) = response_cache.lock().unwrap().get(&url).cloned() + { + for (name, value) in &response.headers { + builder = builder.header(name, value); + } + + return Ok(builder.status(response.status).body(response.body.into())?); + } + + let headers = response.headers().clone(); + let body = response.bytes().await?.to_vec(); + let response = CachedResponse { + status, + headers, + body, + }; + + response_cache + .lock() + .unwrap() + .insert(url.clone(), response.clone()); + + for (name, value) in &response.headers { + builder = builder.header(name, value); + } + + builder + .status(response.status) + .body(response.body.into()) + .map_err(Into::into) +} diff --git a/crates/tauri/src/resources/mod.rs b/crates/tauri/src/resources/mod.rs index 7515d46fd833..9561558585ff 100644 --- a/crates/tauri/src/resources/mod.rs +++ b/crates/tauri/src/resources/mod.rs @@ -140,7 +140,7 @@ impl ResourceTable { } /// Returns a reference counted pointer to the resource of the given `rid`. - /// If `rid` is not present, this function returns [`Error::BadResourceId`]. + /// If `rid` is not present, this function returns [`crate::Error::BadResourceId`]. pub fn get_any(&self, rid: ResourceId) -> crate::Result> { self .index diff --git a/crates/tauri/src/scope/fs.rs b/crates/tauri/src/scope/fs.rs index f22b142b17e2..cbeb735a4816 100644 --- a/crates/tauri/src/scope/fs.rs +++ b/crates/tauri/src/scope/fs.rs @@ -77,7 +77,7 @@ fn push_pattern, F: Fn(&str) -> Result crate::Result<()> { - // Reconstruct pattern path components with appropraite separator + // Reconstruct pattern path components with appropriate separator // so `some\path/to/dir/**\*` would be `some/path/to/dir/**/*` on Unix // and `some\path\to\dir\**\*` on Windows. let path: PathBuf = pattern.as_ref().components().collect(); diff --git a/crates/tauri/src/test/mock_runtime.rs b/crates/tauri/src/test/mock_runtime.rs index 12aae4291d3a..081c02637481 100644 --- a/crates/tauri/src/test/mock_runtime.rs +++ b/crates/tauri/src/test/mock_runtime.rs @@ -538,6 +538,21 @@ impl WindowBuilder for MockWindowBuilder { fn background_color(self, _color: tauri_utils::config::Color) -> Self { self } + + #[cfg(target_os = "android")] + fn activity_name>(self, _class_name: S) -> Self { + self + } + + #[cfg(target_os = "android")] + fn created_by_activity_name>(self, _class_name: S) -> Self { + self + } + + #[cfg(target_os = "ios")] + fn requested_by_scene_identifier>(self, _identifier: S) -> Self { + self + } } impl WebviewDispatch for MockWebviewDispatcher { @@ -554,7 +569,7 @@ impl WebviewDispatch for MockWebviewDispatcher { self.context.next_window_event_id() } - fn with_webview) + Send + 'static>(&self, f: F) -> Result<()> { + fn with_webview(&self, _f: F) -> Result<()> { Ok(()) } @@ -582,6 +597,19 @@ impl WebviewDispatch for MockWebviewDispatcher { Ok(()) } + fn eval_script_with_callback>( + &self, + script: S, + callback: impl Fn(String) + Send + 'static, + ) -> Result<()> { + self + .last_evaluated_script + .lock() + .unwrap() + .replace(script.into()); + Ok(()) + } + fn url(&self) -> Result { Ok(self.url.lock().unwrap().clone()) } @@ -816,6 +844,16 @@ impl WindowDispatch for MockWindowDispatcher { unimplemented!() } + #[cfg(target_os = "android")] + fn activity_name(&self) -> Result { + unimplemented!() + } + + #[cfg(target_os = "ios")] + fn scene_identifier(&self) -> Result { + unimplemented!() + } + fn window_handle( &self, ) -> std::result::Result, raw_window_handle::HandleError> { @@ -1168,6 +1206,7 @@ impl Runtime for MockRuntime { type PlatformSpecificWebviewAttribute = (); type PlatformSpecificInitAttribute = (); type WindowOpener = (); + type Webview = (); fn new(_args: RuntimeInitArgs<()>) -> Result { Ok(Self::init()) diff --git a/crates/tauri/src/test/mod.rs b/crates/tauri/src/test/mod.rs index 41d843a34422..c8d31d8b05c7 100644 --- a/crates/tauri/src/test/mod.rs +++ b/crates/tauri/src/test/mod.rs @@ -216,7 +216,11 @@ pub fn mock_app() -> App { /// cmd: "ping".into(), /// callback: tauri::ipc::CallbackFn(0), /// error: tauri::ipc::CallbackFn(1), -/// url: "http://tauri.localhost".parse().unwrap(), +/// url: if cfg!(any(windows, target_os = "android")) { +/// "http://tauri.localhost" +/// } else { +/// "tauri://localhost" +/// }.parse().unwrap(), /// body: tauri::ipc::InvokeBody::default(), /// headers: Default::default(), /// invoke_key: tauri::test::INVOKE_KEY.to_string(), @@ -275,7 +279,11 @@ pub fn assert_ipc_response< /// cmd: "ping".into(), /// callback: tauri::ipc::CallbackFn(0), /// error: tauri::ipc::CallbackFn(1), -/// url: "http://tauri.localhost".parse().unwrap(), +/// url: if cfg!(any(windows, target_os = "android")) { +/// "http://tauri.localhost" +/// } else { +/// "tauri://localhost" +/// }.parse().unwrap(), /// body: tauri::ipc::InvokeBody::default(), /// headers: Default::default(), /// invoke_key: tauri::test::INVOKE_KEY.to_string(), diff --git a/crates/tauri/src/tray/mod.rs b/crates/tauri/src/tray/mod.rs index 67a33e53aa83..f459200870a4 100644 --- a/crates/tauri/src/tray/mod.rs +++ b/crates/tauri/src/tray/mod.rs @@ -566,6 +566,38 @@ impl TrayIcon { Ok(()) } + /// Sets the tray icon and template status atomically. **macOS only**. + /// + /// On macOS, calling `set_icon` followed by `set_icon_as_template` causes a visible + /// flicker as the icon is rendered twice. This method sets both atomically to prevent that. + /// + /// ## Platform-specific: + /// + /// - **Linux / Windows:** Falls back to calling `set_icon`. + pub fn set_icon_with_as_template( + &self, + icon: Option>, + #[allow(unused)] is_template: bool, + ) -> crate::Result<()> { + #[cfg(target_os = "macos")] + { + let tray_icon = match icon { + Some(i) => Some(i.try_into()?), + None => None, + }; + run_item_main_thread!(self, |self_: Self| { + self_ + .inner + .set_icon_with_as_template(tray_icon, is_template) + })??; + } + #[cfg(not(target_os = "macos"))] + { + self.set_icon(icon)?; + } + Ok(()) + } + /// Disable or enable showing the tray menu on left click. /// /// diff --git a/crates/tauri/src/tray/plugin.rs b/crates/tauri/src/tray/plugin.rs index 0d0483a5e4ba..1aa75d700467 100644 --- a/crates/tauri/src/tray/plugin.rs +++ b/crates/tauri/src/tray/plugin.rs @@ -202,6 +202,24 @@ fn set_icon_as_template( tray.set_icon_as_template(as_template) } +#[command(root = "crate")] +fn set_icon_with_as_template( + app: AppHandle, + webview: Webview, + rid: ResourceId, + icon: Option, + as_template: bool, +) -> crate::Result<()> { + let resources_table = app.resources_table(); + let tray = resources_table.get::>(rid)?; + let webview_resources_table = webview.resources_table(); + let icon = match icon { + Some(i) => Some(i.into_img(&webview_resources_table)?.as_ref().clone()), + None => None, + }; + tray.set_icon_with_as_template(icon, as_template) +} + #[command(root = "crate")] fn set_show_menu_on_left_click( app: AppHandle, @@ -227,6 +245,7 @@ pub(crate) fn init() -> TauriPlugin { set_visible, set_temp_dir_path, set_icon_as_template, + set_icon_with_as_template, set_show_menu_on_left_click, ]) .build() diff --git a/crates/tauri/src/webview/mod.rs b/crates/tauri/src/webview/mod.rs index 89ed4ab29f47..41a2b242056f 100644 --- a/crates/tauri/src/webview/mod.rs +++ b/crates/tauri/src/webview/mod.rs @@ -81,6 +81,14 @@ pub(crate) struct CreatedEvent { pub(crate) label: String, } +fn is_url_for_custom_protocol(current_url: &Url, protocol: &str, protocol_url: &Url) -> bool { + if protocol_url.scheme() == protocol { + current_url.scheme() == protocol + } else { + current_url.scheme() == protocol_url.scheme() && current_url.domain() == protocol_url.domain() + } +} + /// Download event for the [`WebviewBuilder#method.on_download`] hook. #[non_exhaustive] pub enum DownloadEvent<'a> { @@ -156,93 +164,32 @@ pub struct InvokeRequest { pub invoke_key: String, } -/// The platform webview handle. Accessed with [`Webview#method.with_webview`]; -#[cfg(feature = "wry")] -#[cfg_attr(docsrs, doc(cfg(feature = "wry")))] -pub struct PlatformWebview(tauri_runtime_wry::Webview); - -#[cfg(feature = "wry")] -impl PlatformWebview { - /// Returns [`webkit2gtk::WebView`] handle. - #[cfg(any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" - ))] - #[cfg_attr( - docsrs, - doc(cfg(any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" - ))) - )] - pub fn inner(&self) -> webkit2gtk::WebView { - self.0.clone() - } - - /// Returns the WebView2 controller. - #[cfg(all(windows, feature = "wry"))] - #[cfg_attr(docsrs, doc(cfg(windows)))] - pub fn controller( - &self, - ) -> webview2_com::Microsoft::Web::WebView2::Win32::ICoreWebView2Controller { - self.0.controller.clone() - } - - /// Returns the WebView2 environment. - #[cfg(all(windows, feature = "wry"))] - #[cfg_attr(docsrs, doc(cfg(windows)))] - pub fn environment( - &self, - ) -> webview2_com::Microsoft::Web::WebView2::Win32::ICoreWebView2Environment { - self.0.environment.clone() - } - - /// Returns the [WKWebView] handle. - /// - /// [WKWebView]: https://developer.apple.com/documentation/webkit/wkwebview - #[cfg(any(target_os = "macos", target_os = "ios"))] - #[cfg_attr(docsrs, doc(cfg(any(target_os = "macos", target_os = "ios"))))] - pub fn inner(&self) -> *mut std::ffi::c_void { - self.0.webview - } - - /// Returns WKWebView [controller] handle. - /// - /// [controller]: https://developer.apple.com/documentation/webkit/wkusercontentcontroller - #[cfg(any(target_os = "macos", target_os = "ios"))] - #[cfg_attr(docsrs, doc(cfg(any(target_os = "macos", target_os = "ios"))))] - pub fn controller(&self) -> *mut std::ffi::c_void { - self.0.manager - } +/// The platform webview handle. Accessed with [`Webview#method.with_webview`]. +/// +/// This dereferences to the webview type defined by the active runtime +/// (e.g. [`tauri_runtime_wry::Webview`] for the wry runtime or +/// [`tauri_runtime_cef::Webview`] for the CEF runtime), which exposes the +/// platform webview APIs. +#[cfg(any(feature = "wry", feature = "cef"))] +#[cfg_attr(docsrs, doc(cfg(any(feature = "wry", feature = "cef"))))] +pub struct PlatformWebview(R::Webview); - /// Returns [NSWindow] associated with the WKWebView webview. - /// - /// [NSWindow]: https://developer.apple.com/documentation/appkit/nswindow - #[cfg(target_os = "macos")] - #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] - pub fn ns_window(&self) -> *mut std::ffi::c_void { - self.0.ns_window - } +#[cfg(any(feature = "wry", feature = "cef"))] +impl std::ops::Deref for PlatformWebview { + type Target = R::Webview; - /// Returns [UIViewController] used by the WKWebView webview NSWindow. - /// - /// [UIViewController]: https://developer.apple.com/documentation/uikit/uiviewcontroller - #[cfg(target_os = "ios")] - #[cfg_attr(docsrs, doc(cfg(target_os = "ios")))] - pub fn view_controller(&self) -> *mut std::ffi::c_void { - self.0.view_controller + fn deref(&self) -> &Self::Target { + &self.0 } +} - /// Returns handle for JNI execution. - #[cfg(target_os = "android")] - pub fn jni_handle(&self) -> tauri_runtime_wry::wry::JniHandle { - self.0 +#[cfg(all(target_os = "ios", feature = "wry"))] +impl PlatformWebview { + /// Borrows the inner runtime webview handle as a [`std::any::Any`] so the + /// framework can downcast it to the concrete runtime webview type for + /// platform-specific internals. + pub(crate) fn as_any(&self) -> &dyn std::any::Any { + &self.0 } } @@ -254,8 +201,8 @@ pub enum NewWindowResponse { /// /// ## Platform-specific: /// - /// **Linux**: The webview must be related to the caller webview. See [`WebviewBuilder::related_view`]. - /// **Windows**: The webview must use the same environment as the caller webview. See [`WebviewBuilder::environment`]. + /// **Linux**: The webview must be related to the caller webview. See [`WebviewBuilder::with_related_view`]. + /// **Windows**: The webview must use the same environment as the caller webview. See [`WebviewBuilder::with_environment`]. /// **macOS**: The webview must use the same webview configuration as the caller webview. See [`WebviewBuilder::with_webview_configuration`] and [`NewWindowFeatures::webview_configuration`]. Create { /// Window that was created. @@ -623,7 +570,6 @@ tauri::Builder::::new() /// # Platform-specific /// /// - **Android / iOS**: Not supported. - /// - **Windows**: The closure is executed on a separate thread to prevent a deadlock. /// /// [window.open]: https://developer.mozilla.org/en-US/docs/Web/API/Window/open pub fn on_new_window< @@ -1231,7 +1177,7 @@ fn main() { /// - **iOS**: Supported since version 17.0+. /// - **macOS**: Supported since version 14.0+. /// - /// see https://github.com/tauri-apps/tauri/issues/5250#issuecomment-2569380578 + /// see #[must_use] pub fn background_throttling(mut self, policy: BackgroundThrottlingPolicy) -> Self { self.webview_attributes.background_throttling = Some(policy); @@ -1264,6 +1210,29 @@ fn main() { self } + /// Controls the WebView's browser-level general autofill behavior. + /// + /// **This option does not disable password or credit card autofill.** + /// + /// When set to `false`, the WebView will not automatically populate + /// general form fields using previously stored data such as addresses + /// or contact information. + /// + /// By default, this is `true`. + /// + /// ## Platform-specific + /// + /// - **Windows**: Supported. WebView2's autofill feature (called + /// "Suggestions") may not honor `autocomplete="off"` on input + /// elements in some cases. + /// - **Linux / Android / iOS / macOS**: Unsupported and performs no + /// operation. + #[must_use] + pub fn general_autofill_enabled(mut self, enabled: bool) -> Self { + self.webview_attributes = self.webview_attributes.general_autofill_enabled(enabled); + self + } + /// Whether to show a link preview when long pressing on links. Available on macOS and iOS only. /// /// Default is true. @@ -1685,13 +1654,25 @@ impl Webview { /// /// The closure is executed on the main thread. /// - /// Note that `webview2-com`, `webkit2gtk`, `objc2_web_kit` and similar crates may be updated in minor releases of Tauri. + /// Note that `webview2-com`, `webkit2gtk`, `objc2_web_kit`, `cef` (in case of CEF runtime) and similar crates may be updated in minor releases of Tauri. /// Therefore it's recommended to pin Tauri to at least a minor version when you're using `with_webview`. /// + /// The closure receives a [`PlatformWebview`], which dereferences to the webview type + /// defined by the active runtime: + /// + #[cfg_attr( + feature = "wry", + doc = "- With the wry runtime: [`tauri_runtime_wry::Webview`]." + )] + #[cfg_attr( + feature = "cef", + doc = "- With the CEF runtime: [`tauri_runtime_cef::Webview`], whose underlying CEF browser is accessible via [`browser`](tauri_runtime_cef::Webview::browser)." + )] + /// /// # Examples /// #[cfg_attr( - feature = "unstable", + all(feature = "unstable", feature = "wry"), doc = r####" ```rust,no_run use tauri::Manager; @@ -1739,16 +1720,16 @@ tauri::Builder::::new() ``` "#### )] - #[cfg(feature = "wry")] - #[cfg_attr(docsrs, doc(feature = "wry"))] - pub fn with_webview( + #[cfg(any(feature = "wry", feature = "cef"))] + #[cfg_attr(docsrs, doc(cfg(any(feature = "wry", feature = "cef"))))] + pub fn with_webview) + Send + 'static>( &self, f: F, ) -> crate::Result<()> { self .webview .dispatcher - .with_webview(|w| f(PlatformWebview(*w.downcast().unwrap()))) + .with_webview(|w| f(PlatformWebview(w))) .map_err(Into::into) } @@ -1810,8 +1791,13 @@ tauri::Builder::::new() // or from a custom protocol registered by the user || ({ - let protocol_urls = self.manager().webview.uri_scheme_protocols.lock().unwrap().keys().map(|url| Url::parse(&R::custom_scheme_url(url, uses_https)).unwrap()).collect::>(); - protocol_urls.iter().any(|url| url.scheme() == current_url.scheme() && url.domain() == current_url.domain()) + let protocols = self.manager().webview.uri_scheme_protocols.lock().unwrap(); + + protocols.keys().any(|protocol| { + let protocol_url = Url::parse(&R::custom_scheme_url(protocol, uses_https)).unwrap(); + + is_url_for_custom_protocol(current_url, protocol, &protocol_url) + }) }) } @@ -1893,8 +1879,11 @@ tauri::Builder::::new() (plugin, command) }); - // we only check ACL on plugin commands or if the app defined its ACL manifest - if (plugin_command.is_some() || has_app_acl_manifest) + // Check ACL on plugin commands, when the app defined its ACL manifest, + // or when the request comes from a non-local (remote) origin. This + // ensures remote content can never reach custom commands unless an + // explicit `remote` capability has been configured for them. + if (plugin_command.is_some() || has_app_acl_manifest || !is_local) // TODO: Remove this special check in v3 && request.cmd != crate::ipc::channel::FETCH_CHANNEL_DATA_COMMAND && invoke.acl.is_none() @@ -1996,6 +1985,22 @@ tauri::Builder::::new() .map_err(Into::into) } + /// Evaluate JavaScript with callback function on this webview. + /// The evaluation result will be serialized into a JSON string and passed to the callback function. + /// + /// Exception is ignored because of the limitation on Windows. You can catch it yourself and return as string as a workaround. + pub fn eval_with_callback( + &self, + js: impl Into, + callback: impl Fn(String) + Send + 'static, + ) -> crate::Result<()> { + self + .webview + .dispatcher + .eval_script_with_callback(js.into(), callback) + .map_err(Into::into) + } + /// Register a JS event listener and return its identifier. pub(crate) fn listen_js( &self, @@ -2442,6 +2447,16 @@ impl ManagerBase for Webview { fn managed_app_handle(&self) -> &AppHandle { &self.app_handle } + + #[cfg(target_os = "android")] + fn activity_name(&self) -> Option> { + Some(self.window().activity_name()) + } + + #[cfg(target_os = "ios")] + fn scene_identifier(&self) -> Option> { + Some(self.window().scene_identifier()) + } } impl<'de, R: Runtime> CommandArg<'de, R> for Webview { @@ -2471,25 +2486,142 @@ impl ResolvedScope { #[cfg(test)] mod tests { + use url::Url; + + fn test_webview_window() -> crate::WebviewWindow { + use crate::test::{mock_builder, mock_context, noop_assets}; + + // Create a mock app with proper context + let app = mock_builder().build(mock_context(noop_assets())).unwrap(); + + // Create a webview window + crate::WebviewWindowBuilder::new(&app, "test", crate::WebviewUrl::default()) + .build() + .unwrap() + } + #[test] fn webview_is_send_sync() { crate::test_utils::assert_send::(); crate::test_utils::assert_sync::(); } - #[cfg(target_os = "macos")] #[test] - fn test_webview_window_has_set_simple_fullscreen_method() { + fn tauri_protocol_is_local() { + let webview = test_webview_window().webview; + + assert!(webview.is_local_url(&Url::parse("tauri://localhost/").unwrap())); + } + + #[test] + fn direct_custom_protocol_is_local() { use crate::test::{mock_builder, mock_context, noop_assets}; - // Create a mock app with proper context + let app = mock_builder() + .register_uri_scheme_protocol("myproto", |_, _| { + http::Response::builder().body(Vec::new()).unwrap() + }) + .build(mock_context(noop_assets())) + .unwrap(); + let webview = crate::WebviewWindowBuilder::new(&app, "test", crate::WebviewUrl::default()) + .build() + .unwrap() + .webview; + + let url = |s| Url::parse(s).unwrap(); + + assert!(webview.is_local_url(&url("myproto://localhost/"))); + assert!(!webview.is_local_url(&url("https://myproto.localhost/"))); + } + + #[test] + fn http_custom_protocol_rejects_spoofed_domain() { + let protocol_url = Url::parse("https://myproto.localhost/").unwrap(); + let url = |s| Url::parse(s).unwrap(); + + assert!(super::is_url_for_custom_protocol( + &url("https://myproto.localhost/"), + "myproto", + &protocol_url + )); + + // Attacker domain that starts with a registered protocol name must not be local. + assert!(!super::is_url_for_custom_protocol( + &url("https://myproto.evil.com/"), + "myproto", + &protocol_url + )); + assert!(!super::is_url_for_custom_protocol( + &url("https://notregistered.localhost/"), + "myproto", + &protocol_url + )); + } + + /// Custom (non-plugin) commands must be rejected when the IPC request + /// originates from a remote URL, even when no `AppManifest` has been + /// configured. Only local (bundled) origins should be able to reach + /// custom commands. + #[test] + fn remote_origin_blocked_for_custom_commands_without_app_manifest() { + use crate::test::{INVOKE_KEY, mock_builder, mock_context, noop_assets}; + use crate::webview::InvokeRequest; + let app = mock_builder().build(mock_context(noop_assets())).unwrap(); - // Get or create a webview window - let webview_window = - crate::WebviewWindowBuilder::new(&app, "test", crate::WebviewUrl::default()) - .build() - .unwrap(); + let webview = crate::WebviewWindowBuilder::new(&app, "main", Default::default()) + .build() + .unwrap(); + + // Request from a remote origin for a custom (non-plugin) command + // - should be rejected even without an AppManifest. + let remote_result = crate::test::get_ipc_response( + &webview, + InvokeRequest { + cmd: "any_custom_command".into(), + callback: crate::ipc::CallbackFn(0), + error: crate::ipc::CallbackFn(1), + url: "https://evil.com".parse().unwrap(), + body: crate::ipc::InvokeBody::default(), + headers: Default::default(), + invoke_key: INVOKE_KEY.to_string(), + }, + ); + assert!( + remote_result.is_err(), + "custom command should be rejected from a remote origin" + ); + + // Same command from the local origin - should NOT be rejected by the + // remote-origin guard (it may still fail because the command doesn't + // exist, but the error message will be different). + let local_result = crate::test::get_ipc_response( + &webview, + InvokeRequest { + cmd: "any_custom_command".into(), + callback: crate::ipc::CallbackFn(0), + error: crate::ipc::CallbackFn(1), + url: "tauri://localhost".parse().unwrap(), + body: crate::ipc::InvokeBody::default(), + headers: Default::default(), + invoke_key: INVOKE_KEY.to_string(), + }, + ); + // The local request should either succeed or fail for a reason OTHER + // than "not allowed from remote context". + if let Err(e) = &local_result { + let msg = e.to_string(); + assert!( + !msg.contains("not allowed from remote context"), + "local origin should not be blocked by the remote-origin guard, got: {msg}" + ); + } + } + + #[cfg(target_os = "macos")] + #[test] + fn test_webview_window_has_set_simple_fullscreen_method() { + let webview_window = test_webview_window(); // This should compile if set_simple_fullscreen exists let result = webview_window.set_simple_fullscreen(true); diff --git a/crates/tauri/src/webview/plugin.rs b/crates/tauri/src/webview/plugin.rs index 8f48d1dc3f9a..fb37b5c22a7e 100644 --- a/crates/tauri/src/webview/plugin.rs +++ b/crates/tauri/src/webview/plugin.rs @@ -5,86 +5,49 @@ //! The tauri plugin to create and manipulate windows from JS. use crate::{ - Runtime, + AppHandle, Runtime, WebviewWindowBuilder, command, plugin::{Builder, TauriPlugin}, + sealed::ManagerBase, + utils::config::WindowConfig, }; -#[cfg(desktop)] -mod desktop_commands { +#[derive(serde::Serialize)] +struct WebviewRef { + window_label: String, + label: String, +} - use serde::Serialize; - use tauri_runtime::dpi::{Position, Size}; - use tauri_utils::config::WindowConfig; +#[command(root = "crate")] +async fn get_all_webviews(app: AppHandle) -> Vec { + app + .manager() + .webviews() + .values() + .map(|webview| WebviewRef { + window_label: webview.window_ref().label().into(), + label: webview.label().into(), + }) + .collect() +} + +#[command(root = "crate")] +async fn create_webview_window( + app: AppHandle, + options: WindowConfig, +) -> crate::Result<()> { + WebviewWindowBuilder::from_config(&app, &options)?.build()?; + Ok(()) +} +#[cfg(desktop)] +mod desktop_commands { use super::*; use crate::{ - AppHandle, Webview, WebviewWindowBuilder, command, sealed::ManagerBase, webview::Color, + Webview, command, + runtime::dpi::{Position, Size}, + utils::config::Color, }; - #[derive(Serialize)] - pub struct WebviewRef { - window_label: String, - label: String, - } - - #[command(root = "crate")] - pub async fn get_all_webviews(app: AppHandle) -> Vec { - app - .manager() - .webviews() - .values() - .map(|webview| WebviewRef { - window_label: webview.window_ref().label().into(), - label: webview.label().into(), - }) - .collect() - } - - #[command(root = "crate")] - pub async fn create_webview_window( - app: AppHandle, - options: WindowConfig, - ) -> crate::Result<()> { - WebviewWindowBuilder::from_config(&app, &options)?.build()?; - Ok(()) - } - - #[cfg(not(feature = "unstable"))] - #[command(root = "crate")] - pub async fn create_webview() -> crate::Result<()> { - Err(crate::Error::UnstableFeatureNotSupported) - } - - #[cfg(feature = "unstable")] - #[command(root = "crate")] - pub async fn create_webview( - app: AppHandle, - window_label: String, - options: WindowConfig, - ) -> crate::Result<()> { - use anyhow::Context; - - let window = app - .manager() - .get_window(&window_label) - .ok_or(crate::Error::WindowNotFound)?; - - let x = options.x.context("missing parameter `options.x`")?; - let y = options.y.context("missing parameter `options.y`")?; - let width = options.width; - let height = options.height; - - let builder = crate::webview::WebviewBuilder::from_config(&options); - - window.add_child( - builder, - tauri_runtime::dpi::LogicalPosition::new(x, y), - tauri_runtime::dpi::LogicalSize::new(width, height), - )?; - - Ok(()) - } - fn get_webview( webview: Webview, label: Option, @@ -163,6 +126,42 @@ mod desktop_commands { ); setter!(clear_all_browsing_data, clear_all_browsing_data); + #[cfg(not(feature = "unstable"))] + #[command(root = "crate")] + pub async fn create_webview() -> crate::Result<()> { + Err(crate::Error::UnstableFeatureNotSupported) + } + + #[cfg(feature = "unstable")] + #[command(root = "crate")] + pub async fn create_webview( + app: crate::AppHandle, + window_label: String, + options: WindowConfig, + ) -> crate::Result<()> { + use anyhow::Context; + + let window = app + .manager() + .get_window(&window_label) + .ok_or(crate::Error::WindowNotFound)?; + + let x = options.x.context("missing parameter `options.x`")?; + let y = options.y.context("missing parameter `options.y`")?; + let width = options.width; + let height = options.height; + + let builder = crate::webview::WebviewBuilder::from_config(&options); + + window.add_child( + builder, + tauri_runtime::dpi::LogicalPosition::new(x, y), + tauri_runtime::dpi::LogicalSize::new(width, height), + )?; + + Ok(()) + } + #[command(root = "crate")] pub async fn reparent( webview: crate::Webview, @@ -231,39 +230,29 @@ pub fn init() -> TauriPlugin { } builder - .invoke_handler( - #[cfg(desktop)] - crate::generate_handler![ - #![plugin(webview)] - desktop_commands::create_webview, - desktop_commands::create_webview_window, - // getters - desktop_commands::get_all_webviews, - desktop_commands::webview_position, - desktop_commands::webview_size, - // setters - desktop_commands::webview_close, - desktop_commands::set_webview_size, - desktop_commands::set_webview_position, - desktop_commands::set_webview_focus, - desktop_commands::set_webview_auto_resize, - desktop_commands::set_webview_background_color, - desktop_commands::set_webview_zoom, - desktop_commands::webview_hide, - desktop_commands::webview_show, - desktop_commands::print, - desktop_commands::reparent, - desktop_commands::clear_all_browsing_data, - #[cfg(any(debug_assertions, feature = "devtools"))] - desktop_commands::internal_toggle_devtools, - ], - #[cfg(mobile)] - |invoke| { - invoke - .resolver - .reject("Webview API not available on mobile"); - true - }, - ) + .invoke_handler(crate::generate_handler![ + #![plugin(webview)] + create_webview_window, + get_all_webviews, + #[cfg(desktop)] desktop_commands::create_webview, + // getters + #[cfg(desktop)] desktop_commands::webview_position, + #[cfg(desktop)] desktop_commands::webview_size, + // setters + #[cfg(desktop)] desktop_commands::webview_close, + #[cfg(desktop)] desktop_commands::set_webview_size, + #[cfg(desktop)] desktop_commands::set_webview_position, + #[cfg(desktop)] desktop_commands::set_webview_focus, + #[cfg(desktop)] desktop_commands::set_webview_auto_resize, + #[cfg(desktop)] desktop_commands::set_webview_background_color, + #[cfg(desktop)] desktop_commands::set_webview_zoom, + #[cfg(desktop)] desktop_commands::webview_hide, + #[cfg(desktop)] desktop_commands::webview_show, + #[cfg(desktop)] desktop_commands::print, + #[cfg(desktop)] desktop_commands::clear_all_browsing_data, + #[cfg(desktop)] desktop_commands::reparent, + #[cfg(all(desktop, any(debug_assertions, feature = "devtools")))] + desktop_commands::internal_toggle_devtools, + ]) .build() } diff --git a/crates/tauri/src/webview/webview_window.rs b/crates/tauri/src/webview/webview_window.rs index cfa88a89dea7..68450871559e 100644 --- a/crates/tauri/src/webview/webview_window.rs +++ b/crates/tauri/src/webview/webview_window.rs @@ -14,7 +14,7 @@ use crate::{ Emitter, EventName, Listener, ResourceTable, Window, event::EventTarget, ipc::ScopeObject, - runtime::dpi::{PhysicalPosition, PhysicalSize}, + runtime::dpi::{PhysicalPosition, PhysicalSize, Position, Size}, webview::{NewWindowResponse, ScrollBarStyle}, window::Monitor, }; @@ -22,11 +22,7 @@ use crate::{ use crate::{ image::Image, menu::{ContextMenu, Menu}, - runtime::{ - UserAttentionType, - dpi::{Position, Size}, - window::CursorIcon, - }, + runtime::{UserAttentionType, window::CursorIcon}, }; use tauri_runtime::webview::NewWindowFeatures; use tauri_utils::config::{BackgroundThrottlingPolicy, Color, WebviewUrl, WindowConfig}; @@ -64,15 +60,6 @@ impl<'a, M: Manager> WebviewWindowBuilder<'a, crate::Cef, M> { self.webview_builder = self.webview_builder.browser_runtime_style(style); self } - - /// Crete a full Chrome browser window. - /// - /// In this case most window builder options are ignored, - /// as we can only control the size and position of the window. - pub fn browser_window(mut self) -> Self { - self.window_builder.window_builder = self.window_builder.window_builder.browser_window(); - self - } } impl<'a, R: Runtime, M: Manager> WebviewWindowBuilder<'a, R, M> { @@ -352,7 +339,6 @@ impl<'a, R: Runtime, M: Manager> WebviewWindowBuilder<'a, R, M> { /// # Platform-specific /// /// - **Android / iOS**: Not supported. - /// - **Windows**: The closure is executed on a separate thread to prevent a deadlock. /// /// [window.open]: https://developer.mozilla.org/en-US/docs/Web/API/Window/open pub fn on_new_window< @@ -506,44 +492,6 @@ impl<'a, R: Runtime, M: Manager> WebviewWindowBuilder<'a, R, M> { self } - /// The initial position of the window in logical pixels. - #[must_use] - pub fn position(mut self, x: f64, y: f64) -> Self { - self.window_builder = self.window_builder.position(x, y); - self - } - - /// Window size in logical pixels. - #[must_use] - pub fn inner_size(mut self, width: f64, height: f64) -> Self { - self.window_builder = self.window_builder.inner_size(width, height); - self - } - - /// Window min inner size in logical pixels. - #[must_use] - pub fn min_inner_size(mut self, min_width: f64, min_height: f64) -> Self { - self.window_builder = self.window_builder.min_inner_size(min_width, min_height); - self - } - - /// Window max inner size in logical pixels. - #[must_use] - pub fn max_inner_size(mut self, max_width: f64, max_height: f64) -> Self { - self.window_builder = self.window_builder.max_inner_size(max_width, max_height); - self - } - - /// Window inner size constraints. - #[must_use] - pub fn inner_size_constraints( - mut self, - constraints: tauri_runtime::window::WindowSizeConstraints, - ) -> Self { - self.window_builder = self.window_builder.inner_size_constraints(constraints); - self - } - /// Prevent the window from overflowing the working area (e.g. monitor size - taskbar size) /// on creation, which means the window size will be limited to `monitor size - taskbar size` /// @@ -572,14 +520,6 @@ impl<'a, R: Runtime, M: Manager> WebviewWindowBuilder<'a, R, M> { self } - /// Whether the window is resizable or not. - /// When resizable is set to false, native window's maximize button is automatically disabled. - #[must_use] - pub fn resizable(mut self, resizable: bool) -> Self { - self.window_builder = self.window_builder.resizable(resizable); - self - } - /// Whether the window's native maximize button is enabled or not. /// If resizable is set to false, this setting is ignored. /// @@ -617,13 +557,6 @@ impl<'a, R: Runtime, M: Manager> WebviewWindowBuilder<'a, R, M> { self } - /// The title of the window in the title bar. - #[must_use] - pub fn title>(mut self, title: S) -> Self { - self.window_builder = self.window_builder.title(title); - self - } - /// Whether to start the window in fullscreen or not. #[must_use] pub fn fullscreen(mut self, fullscreen: bool) -> Self { @@ -631,25 +564,6 @@ impl<'a, R: Runtime, M: Manager> WebviewWindowBuilder<'a, R, M> { self } - /// Sets the window to be initially focused. - #[must_use] - #[deprecated( - since = "1.2.0", - note = "The window is automatically focused by default. This function Will be removed in 3.0.0. Use `focused` instead." - )] - pub fn focus(mut self) -> Self { - self.window_builder = self.window_builder.focused(true); - self.webview_builder = self.webview_builder.focused(true); - self - } - - /// Whether the window will be focusable or not. - #[must_use] - pub fn focusable(mut self, focusable: bool) -> Self { - self.window_builder = self.window_builder.focusable(focusable); - self - } - /// Whether the window will be initially focused or not. #[must_use] pub fn focused(mut self, focused: bool) -> Self { @@ -665,24 +579,6 @@ impl<'a, R: Runtime, M: Manager> WebviewWindowBuilder<'a, R, M> { self } - /// Whether the window should be immediately visible upon creation. - #[must_use] - pub fn visible(mut self, visible: bool) -> Self { - self.window_builder = self.window_builder.visible(visible); - self - } - - /// Forces a theme or uses the system settings if None was provided. - /// - /// ## Platform-specific - /// - /// - **macOS**: Only supported on macOS 10.14+. - #[must_use] - pub fn theme(mut self, theme: Option) -> Self { - self.window_builder = self.window_builder.theme(theme); - self - } - /// Whether the window should have borders and bars. #[must_use] pub fn decorations(mut self, decorations: bool) -> Self { @@ -713,13 +609,6 @@ impl<'a, R: Runtime, M: Manager> WebviewWindowBuilder<'a, R, M> { self } - /// Prevents the window contents from being captured by other apps. - #[must_use] - pub fn content_protected(mut self, protected: bool) -> Self { - self.window_builder = self.window_builder.content_protected(protected); - self - } - /// Sets the window icon. pub fn icon(mut self, icon: Image<'a>) -> crate::Result { self.window_builder = self.window_builder.icon(icon)?; @@ -941,6 +830,106 @@ impl<'a, R: Runtime, M: Manager> WebviewWindowBuilder<'a, R, M> { } } +/// Window APIs. +impl<'a, R: Runtime, M: Manager> WebviewWindowBuilder<'a, R, M> { + /// The initial position of the window in logical pixels. + #[must_use] + pub fn position(mut self, x: f64, y: f64) -> Self { + self.window_builder = self.window_builder.position(x, y); + self + } + + /// Window size in logical pixels. + #[must_use] + pub fn inner_size(mut self, width: f64, height: f64) -> Self { + self.window_builder = self.window_builder.inner_size(width, height); + self + } + + /// Window min inner size in logical pixels. + #[must_use] + pub fn min_inner_size(mut self, min_width: f64, min_height: f64) -> Self { + self.window_builder = self.window_builder.min_inner_size(min_width, min_height); + self + } + + /// Window max inner size in logical pixels. + #[must_use] + pub fn max_inner_size(mut self, max_width: f64, max_height: f64) -> Self { + self.window_builder = self.window_builder.max_inner_size(max_width, max_height); + self + } + + /// Window inner size constraints. + #[must_use] + pub fn inner_size_constraints( + mut self, + constraints: tauri_runtime::window::WindowSizeConstraints, + ) -> Self { + self.window_builder = self.window_builder.inner_size_constraints(constraints); + self + } + + /// Whether the window is resizable or not. + /// When resizable is set to false, native window's maximize button is automatically disabled. + #[must_use] + pub fn resizable(mut self, resizable: bool) -> Self { + self.window_builder = self.window_builder.resizable(resizable); + self + } + + /// The title of the window in the title bar. + #[must_use] + pub fn title>(mut self, title: S) -> Self { + self.window_builder = self.window_builder.title(title); + self + } + + /// Sets the window to be initially focused. + #[must_use] + #[deprecated( + since = "1.2.0", + note = "The window is automatically focused by default. This function Will be removed in 3.0.0. Use `focused` instead." + )] + pub fn focus(mut self) -> Self { + self.window_builder = self.window_builder.focused(true); + self.webview_builder = self.webview_builder.focused(true); + self + } + + /// Whether the window will be focusable or not. + #[must_use] + pub fn focusable(mut self, focusable: bool) -> Self { + self.window_builder = self.window_builder.focusable(focusable); + self + } + + /// Whether the window should be immediately visible upon creation. + #[must_use] + pub fn visible(mut self, visible: bool) -> Self { + self.window_builder = self.window_builder.visible(visible); + self + } + + /// Forces a theme or uses the system settings if None was provided. + /// + /// ## Platform-specific + /// + /// - **macOS**: Only supported on macOS 10.14+. + #[must_use] + pub fn theme(mut self, theme: Option) -> Self { + self.window_builder = self.window_builder.theme(theme); + self + } + + /// Prevents the window contents from being captured by other apps. + #[must_use] + pub fn content_protected(mut self, protected: bool) -> Self { + self.window_builder = self.window_builder.content_protected(protected); + self + } +} + /// Webview attributes. impl> WebviewWindowBuilder<'_, R, M> { /// Sets whether clicking an inactive window also clicks through to the webview. @@ -987,6 +976,9 @@ impl> WebviewWindowBuilder<'_, R, M> { /// }); /// } /// ``` + /// + /// [addDocumentStartJavaScript]: https://developer.android.com/reference/androidx/webkit/WebViewCompat#addDocumentStartJavaScript(android.webkit.WebView,java.lang.String,java.util.Set%3Cjava.lang.String%3E) + /// [onPageStarted]: https://developer.android.com/reference/android/webkit/WebViewClient#onPageStarted(android.webkit.WebView,%20java.lang.String,%20android.graphics.Bitmap) #[must_use] pub fn initialization_script(mut self, script: impl Into) -> Self { self.webview_builder = self.webview_builder.initialization_script(script); @@ -1029,6 +1021,9 @@ impl> WebviewWindowBuilder<'_, R, M> { /// }); /// } /// ``` + /// + /// [addDocumentStartJavaScript]: https://developer.android.com/reference/androidx/webkit/WebViewCompat#addDocumentStartJavaScript(android.webkit.WebView,java.lang.String,java.util.Set%3Cjava.lang.String%3E) + /// [onPageStarted]: https://developer.android.com/reference/android/webkit/WebViewClient#onPageStarted(android.webkit.WebView,%20java.lang.String,%20android.graphics.Bitmap) #[must_use] pub fn initialization_script_for_all_frames(mut self, script: impl Into) -> Self { self.webview_builder = self @@ -1242,7 +1237,7 @@ impl> WebviewWindowBuilder<'_, R, M> { /// - **iOS**: Supported since version 17.0+. /// - **macOS**: Supported since version 14.0+. /// - /// see https://github.com/tauri-apps/tauri/issues/5250#issuecomment-2569380578 + /// see #[must_use] pub fn background_throttling(mut self, policy: BackgroundThrottlingPolicy) -> Self { self.webview_builder = self.webview_builder.background_throttling(policy); @@ -1275,6 +1270,29 @@ impl> WebviewWindowBuilder<'_, R, M> { self } + /// Controls the WebView's browser-level general autofill behavior. + /// + /// **This option does not disable password or credit card autofill.** + /// + /// When set to `false`, the WebView will not automatically populate + /// general form fields using previously stored data such as addresses + /// or contact information. + /// + /// By default, this is `true`. + /// + /// ## Platform-specific + /// + /// - **Windows**: Supported. WebView2's autofill feature (called + /// "Suggestions") may not honor `autocomplete="off"` on input + /// elements in some cases. + /// - **Linux / Android / iOS / macOS**: Unsupported and performs no + /// operation. + #[must_use] + pub fn general_autofill_enabled(mut self, enabled: bool) -> Self { + self.webview_builder = self.webview_builder.general_autofill_enabled(enabled); + self + } + /// Allows overriding the keyboard accessory view on iOS. /// Returning `None` effectively removes the view. /// @@ -1398,6 +1416,40 @@ impl> WebviewWindowBuilder<'_, crate::Wry, M> { } } +// Android specific APIs +#[cfg(target_os = "android")] +impl> WebviewWindowBuilder<'_, R, M> { + /// The name of the activity to create for this webview window. + pub fn activity_name>(mut self, class_name: S) -> Self { + self.window_builder = self.window_builder.activity_name(class_name); + self + } + + /// Sets the name of the activity that is creating this webview window. + /// + /// This is important to determine which stack the activity will belong to. + pub fn created_by_activity_name>(mut self, class_name: S) -> Self { + self.window_builder = self.window_builder.created_by_activity_name(class_name); + self + } +} + +/// iOS specific APIs +#[cfg(target_os = "ios")] +impl> WebviewWindowBuilder<'_, R, M> { + /// Sets the identifier of the scene that is requesting the new scene, + /// establishing a relationship between the two scenes. + /// + /// By default the system uses the foreground scene. + #[cfg(target_os = "ios")] + pub fn requested_by_scene_identifier(mut self, identifier: String) -> Self { + self.window_builder = self + .window_builder + .requested_by_scene_identifier(identifier); + self + } +} + /// A type that wraps a [`Window`] together with a [`Webview`]. #[default_runtime(crate::Wry, wry)] #[derive(Debug)] @@ -1827,6 +1879,12 @@ impl WebviewWindow { self.window.default_vbox() } + /// Returns the name of the Android activity associated with this window. + #[cfg(target_os = "android")] + pub fn activity_name(&self) -> crate::Result { + self.window.activity_name() + } + /// Returns the current window theme. /// /// ## Platform-specific @@ -1879,17 +1937,6 @@ impl WebviewWindow { self.window.request_user_attention(request_type) } - /// Determines if this window should be resizable. - /// When resizable is set to false, native window's maximize button is automatically disabled. - pub fn set_resizable(&self, resizable: bool) -> crate::Result<()> { - self.window.set_resizable(resizable) - } - - /// Enable or disable the window. - pub fn set_enabled(&self, enabled: bool) -> crate::Result<()> { - self.webview.window().set_enabled(enabled) - } - /// Determines if this window's native maximize button should be enabled. /// If resizable is set to false, this setting is ignored. /// @@ -1921,11 +1968,6 @@ impl WebviewWindow { self.window.set_closable(closable) } - /// Set this window's title. - pub fn set_title(&self, title: &str) -> crate::Result<()> { - self.window.set_title(title) - } - /// Maximizes this window. pub fn maximize(&self) -> crate::Result<()> { self.window.maximize() @@ -1946,26 +1988,6 @@ impl WebviewWindow { self.window.unminimize() } - /// Show this window. - pub fn show(&self) -> crate::Result<()> { - self.window.show() - } - - /// Hide this window. - pub fn hide(&self) -> crate::Result<()> { - self.window.hide() - } - - /// Closes this window. It emits [`crate::RunEvent::CloseRequested`] first like a user-initiated close request so you can intercept it. - pub fn close(&self) -> crate::Result<()> { - self.window.close() - } - - /// Destroys this window. Similar to [`Self::close`] but does not emit any events and force close the window instead. - pub fn destroy(&self) -> crate::Result<()> { - self.window.destroy() - } - /// Determines if this window should be [decorated]. /// /// [decorated]: https://en.wikipedia.org/wiki/Window_(computing)#Window_decoration @@ -2041,39 +2063,6 @@ impl WebviewWindow { .set_visible_on_all_workspaces(visible_on_all_workspaces) } - /// Prevents the window contents from being captured by other apps. - pub fn set_content_protected(&self, protected: bool) -> crate::Result<()> { - self.window.set_content_protected(protected) - } - - /// Resizes this window. - pub fn set_size>(&self, size: S) -> crate::Result<()> { - self.window.set_size(size.into()) - } - - /// Sets this window's minimum inner size. - pub fn set_min_size>(&self, size: Option) -> crate::Result<()> { - self.window.set_min_size(size.map(|s| s.into())) - } - - /// Sets this window's maximum inner size. - pub fn set_max_size>(&self, size: Option) -> crate::Result<()> { - self.window.set_max_size(size.map(|s| s.into())) - } - - /// Sets this window's minimum inner width. - pub fn set_size_constraints( - &self, - constraints: tauri_runtime::window::WindowSizeConstraints, - ) -> crate::Result<()> { - self.window.set_size_constraints(constraints) - } - - /// Sets this window's position. - pub fn set_position>(&self, position: Pos) -> crate::Result<()> { - self.window.set_position(position) - } - /// Determines if this window should be fullscreen. pub fn set_fullscreen(&self, fullscreen: bool) -> crate::Result<()> { self.window.set_fullscreen(fullscreen) @@ -2093,41 +2082,11 @@ impl WebviewWindow { self.window.set_simple_fullscreen(enable) } - /// Bring the window to front and focus. - pub fn set_focus(&self) -> crate::Result<()> { - self.window.set_focus() - } - - /// Sets whether the window can be focused. - /// - /// ## Platform-specific - /// - /// - **macOS**: If the window is already focused, it is not possible to unfocus it after calling `set_focusable(false)`. - /// In this case, you might consider calling [`Window::set_focus`] but it will move the window to the back i.e. at the bottom in terms of z-order. - pub fn set_focusable(&self, focusable: bool) -> crate::Result<()> { - self.window.set_focusable(focusable) - } - /// Sets this window' icon. pub fn set_icon(&self, icon: Image<'_>) -> crate::Result<()> { self.window.set_icon(icon) } - /// Sets the window background color. - /// - /// ## Platform-specific: - /// - /// - **iOS / Android:** Unsupported. - /// - **macOS**: Not implemented for the webview layer.. - /// - **Windows**: - /// - alpha channel is ignored for the window layer. - /// - On Windows 7, transparency is not supported and the alpha value will be ignored for the webview layer.. - /// - On Windows 8 and newer: translucent colors are not supported so any alpha value other than `0` will be replaced by `255` for the webview layer. - pub fn set_background_color(&self, color: Option) -> crate::Result<()> { - self.window.set_background_color(color)?; - self.webview.set_background_color(color) - } - /// Whether to hide the window icon from the taskbar or not. /// /// ## Platform-specific @@ -2236,6 +2195,108 @@ impl WebviewWindow { pub fn set_traffic_light_position(&self, position: Position) -> crate::Result<()> { self.window.set_traffic_light_position(position) } +} + +/// Desktop window setters and actions. +impl WebviewWindow { + /// Determines if this window should be resizable. + /// When resizable is set to false, native window's maximize button is automatically disabled. + pub fn set_resizable(&self, resizable: bool) -> crate::Result<()> { + self.window.set_resizable(resizable) + } + + /// Enable or disable the window. + pub fn set_enabled(&self, enabled: bool) -> crate::Result<()> { + self.webview.window().set_enabled(enabled) + } + + /// Set this window's title. + pub fn set_title(&self, title: &str) -> crate::Result<()> { + self.window.set_title(title) + } + + /// Show this window. + pub fn show(&self) -> crate::Result<()> { + self.window.show() + } + + /// Hide this window. + pub fn hide(&self) -> crate::Result<()> { + self.window.hide() + } + + /// Closes this window. It emits [`crate::WindowEvent::CloseRequested`] first like a user-initiated close request so you can intercept it. + pub fn close(&self) -> crate::Result<()> { + self.window.close() + } + + /// Destroys this window. Similar to [`Self::close`] but does not emit any events and force close the window instead. + pub fn destroy(&self) -> crate::Result<()> { + self.window.destroy() + } + + /// Prevents the window contents from being captured by other apps. + pub fn set_content_protected(&self, protected: bool) -> crate::Result<()> { + self.window.set_content_protected(protected) + } + + /// Resizes this window. + pub fn set_size>(&self, size: S) -> crate::Result<()> { + self.window.set_size(size.into()) + } + + /// Sets this window's minimum inner size. + pub fn set_min_size>(&self, size: Option) -> crate::Result<()> { + self.window.set_min_size(size.map(|s| s.into())) + } + + /// Sets this window's maximum inner size. + pub fn set_max_size>(&self, size: Option) -> crate::Result<()> { + self.window.set_max_size(size.map(|s| s.into())) + } + + /// Sets this window's minimum inner width. + pub fn set_size_constraints( + &self, + constraints: tauri_runtime::window::WindowSizeConstraints, + ) -> crate::Result<()> { + self.window.set_size_constraints(constraints) + } + + /// Sets this window's position. + pub fn set_position>(&self, position: Pos) -> crate::Result<()> { + self.window.set_position(position) + } + + /// Bring the window to front and focus. + pub fn set_focus(&self) -> crate::Result<()> { + self.window.set_focus() + } + + /// Sets whether the window can be focused. + /// + /// ## Platform-specific + /// + /// - **macOS**: If the window is already focused, it is not possible to unfocus it after calling `set_focusable(false)`. + /// In this case, you might consider calling [`Window::set_focus`] but it will move the window to the back i.e. at the bottom in terms of z-order. + pub fn set_focusable(&self, focusable: bool) -> crate::Result<()> { + self.window.set_focusable(focusable) + } + + /// Sets the window background color. + /// + /// ## Platform-specific: + /// + /// - **iOS / Android:** Unsupported. + /// - **macOS**: Not implemented for the webview layer.. + /// - **Windows**: + /// - alpha channel is ignored for the window layer. + /// - On Windows 7, transparency is not supported and the alpha value will be ignored for the webview layer.. + /// - On Windows 8 and newer: translucent colors are not supported so any alpha value other than `0` will be replaced by `255` for the webview layer. + pub fn set_background_color(&self, color: Option) -> crate::Result<()> { + self.window.set_background_color(color)?; + self.webview.set_background_color(color) + } /// Sets the theme for this window. /// @@ -2248,7 +2309,7 @@ impl WebviewWindow { } } -/// Desktop webview setters and actions. +/// Desktop webview APIs. #[cfg(desktop)] impl WebviewWindow { /// Opens the dialog to prints the contents of the webview. @@ -2265,9 +2326,21 @@ impl WebviewWindow { /// /// The closure is executed on the main thread. /// - /// Note that `webview2-com`, `webkit2gtk`, `objc2_web_kit` and similar crates may be updated in minor releases of Tauri. + /// Note that `webview2-com`, `webkit2gtk`, `objc2_web_kit`, `cef` (in case of CEF runtime) and similar crates may be updated in minor releases of Tauri. /// Therefore it's recommended to pin Tauri to at least a minor version when you're using `with_webview`. /// + /// The closure receives a [`PlatformWebview`](crate::webview::PlatformWebview), which dereferences + /// to the webview type defined by the active runtime: + /// + #[cfg_attr( + feature = "wry", + doc = "- With the wry runtime: [`tauri_runtime_wry::Webview`]." + )] + #[cfg_attr( + feature = "cef", + doc = "- With the CEF runtime: [`tauri_runtime_cef::Webview`], whose underlying CEF browser is accessible via [`browser`](tauri_runtime_cef::Webview::browser)." + )] + /// /// # Examples /// /// ```rust,no_run @@ -2317,9 +2390,9 @@ impl WebviewWindow { /// } /// ``` #[allow(clippy::needless_doctest_main)] // To avoid a large diff - #[cfg(feature = "wry")] - #[cfg_attr(docsrs, doc(feature = "wry"))] - pub fn with_webview( + #[cfg(any(feature = "wry", feature = "cef"))] + #[cfg_attr(docsrs, doc(cfg(any(feature = "wry", feature = "cef"))))] + pub fn with_webview) + Send + 'static>( &self, f: F, ) -> crate::Result<()> { @@ -2375,6 +2448,18 @@ impl WebviewWindow { self.webview.eval(js) } + /// Evaluate JavaScript with callback function on this webview. + /// The evaluation result will be serialized into a JSON string and passed to the callback function. + /// + /// Exception is ignored because of the limitation on Windows. You can catch it yourself and return as string as a workaround. + pub fn eval_with_callback( + &self, + js: impl Into, + callback: impl Fn(String) + Send + 'static, + ) -> crate::Result<()> { + self.webview.eval_with_callback(js, callback) + } + /// Opens the developer tools window (Web Inspector). /// The devtools is only enabled on debug builds or with the `devtools` feature flag. /// @@ -2702,10 +2787,20 @@ impl ManagerBase for WebviewWindow { } fn runtime(&self) -> RuntimeOrDispatch<'_, R> { - self.webview.runtime() + self.window.runtime() } fn managed_app_handle(&self) -> &AppHandle { self.webview.managed_app_handle() } + + #[cfg(target_os = "android")] + fn activity_name(&self) -> Option> { + Some(self.window.activity_name()) + } + + #[cfg(target_os = "ios")] + fn scene_identifier(&self) -> Option> { + Some(self.window.scene_identifier()) + } } diff --git a/crates/tauri/src/window/mod.rs b/crates/tauri/src/window/mod.rs index 4d0f938feb6d..a329391422d2 100644 --- a/crates/tauri/src/window/mod.rs +++ b/crates/tauri/src/window/mod.rs @@ -20,10 +20,7 @@ use crate::{ CursorIcon, image::Image, menu::{ContextMenu, Menu, MenuId}, - runtime::{ - UserAttentionType, - dpi::{Position, Size}, - }, + runtime::UserAttentionType, }; use crate::{ Emitter, EventLoopMessage, EventName, Listener, Manager, ResourceTable, Runtime, Theme, Webview, @@ -34,6 +31,7 @@ use crate::{ manager::{AppManager, EmitPayload}, runtime::{ RuntimeHandle, WindowDispatch, + dpi::{Position, Size}, monitor::Monitor as RuntimeMonitor, window::{DetachedWindow, PendingWindow, WindowBuilder as _}, }, @@ -129,6 +127,10 @@ unstable_struct!( #[cfg(desktop)] on_menu_event: Option>>, window_effects: Option, + #[cfg(target_os = "android")] + created_by_activity_name_set: bool, + #[cfg(target_os = "ios")] + requested_by_scene_identifier_set: bool, } ); @@ -215,6 +217,10 @@ async fn create_window(app: tauri::AppHandle) { #[cfg(desktop)] on_menu_event: None, window_effects: None, + #[cfg(target_os = "android")] + created_by_activity_name_set: false, + #[cfg(target_os = "ios")] + requested_by_scene_identifier_set: false, } } @@ -250,6 +256,10 @@ async fn reopen_window(app: tauri::AppHandle) { pub fn from_config(manager: &'a M, config: &WindowConfig) -> crate::Result { #[cfg_attr(not(windows), allow(unused_mut))] let mut builder = Self { + #[cfg(target_os = "android")] + created_by_activity_name_set: config.created_by_activity_name.is_some(), + #[cfg(target_os = "ios")] + requested_by_scene_identifier_set: config.requested_by_scene_identifier.is_some(), manager, label: config.label.clone(), window_effects: config.window_effects.clone(), @@ -345,12 +355,31 @@ tauri::Builder::::new() /// Creates a new window with an optional webview. fn build_internal( - self, + // mutable on Android + #[allow(unused_mut)] mut self, webview: Option>, ) -> crate::Result> { #[cfg(desktop)] let theme = self.window_builder.get_theme(); + #[cfg(target_os = "android")] + if !self.created_by_activity_name_set { + if let Some(manager_window_activity_name) = self.manager.activity_name() { + self.window_builder = self + .window_builder + .created_by_activity_name(manager_window_activity_name?); + } + } + + #[cfg(target_os = "ios")] + if !self.requested_by_scene_identifier_set { + if let Some(manager_window_scene_identifier) = self.manager.scene_identifier() { + self.window_builder = self + .window_builder + .requested_by_scene_identifier(manager_window_scene_identifier?); + } + } + let mut pending = PendingWindow::new(self.window_builder, self.label)?; if let Some(webview) = webview { pending.set_webview(webview); @@ -426,7 +455,7 @@ tauri::Builder::::new() } } -/// Desktop APIs. +/// Desktop APIs #[cfg(desktop)] #[cfg_attr(not(feature = "unstable"), allow(dead_code))] impl<'a, R: Runtime, M: Manager> WindowBuilder<'a, R, M> { @@ -444,44 +473,6 @@ impl<'a, R: Runtime, M: Manager> WindowBuilder<'a, R, M> { self } - /// The initial position of the window in logical pixels. - #[must_use] - pub fn position(mut self, x: f64, y: f64) -> Self { - self.window_builder = self.window_builder.position(x, y); - self - } - - /// Window size in logical pixels. - #[must_use] - pub fn inner_size(mut self, width: f64, height: f64) -> Self { - self.window_builder = self.window_builder.inner_size(width, height); - self - } - - /// Window min inner size in logical pixels. - #[must_use] - pub fn min_inner_size(mut self, min_width: f64, min_height: f64) -> Self { - self.window_builder = self.window_builder.min_inner_size(min_width, min_height); - self - } - - /// Window max inner size in logical pixels. - #[must_use] - pub fn max_inner_size(mut self, max_width: f64, max_height: f64) -> Self { - self.window_builder = self.window_builder.max_inner_size(max_width, max_height); - self - } - - /// Window inner size constraints. - #[must_use] - pub fn inner_size_constraints( - mut self, - constraints: tauri_runtime::window::WindowSizeConstraints, - ) -> Self { - self.window_builder = self.window_builder.inner_size_constraints(constraints); - self - } - /// Prevent the window from overflowing the working area (e.g. monitor size - taskbar size) /// on creation, which means the window size will be limited to `monitor size - taskbar size` /// @@ -512,14 +503,6 @@ impl<'a, R: Runtime, M: Manager> WindowBuilder<'a, R, M> { self } - /// Whether the window is resizable or not. - /// When resizable is set to false, native window's maximize button is automatically disabled. - #[must_use] - pub fn resizable(mut self, resizable: bool) -> Self { - self.window_builder = self.window_builder.resizable(resizable); - self - } - /// Whether the window's native maximize button is enabled or not. /// If resizable is set to false, this setting is ignored. /// @@ -557,13 +540,6 @@ impl<'a, R: Runtime, M: Manager> WindowBuilder<'a, R, M> { self } - /// The title of the window in the title bar. - #[must_use] - pub fn title>(mut self, title: S) -> Self { - self.window_builder = self.window_builder.title(title); - self - } - /// Whether to start the window in fullscreen or not. #[must_use] pub fn fullscreen(mut self, fullscreen: bool) -> Self { @@ -571,31 +547,6 @@ impl<'a, R: Runtime, M: Manager> WindowBuilder<'a, R, M> { self } - /// Sets the window to be initially focused. - #[must_use] - #[deprecated( - since = "1.2.0", - note = "The window is automatically focused by default. This function Will be removed in 3.0.0. Use `focused` instead." - )] - pub fn focus(mut self) -> Self { - self.window_builder = self.window_builder.focused(true); - self - } - - /// Whether the window will be initially focused or not. - #[must_use] - pub fn focused(mut self, focused: bool) -> Self { - self.window_builder = self.window_builder.focused(focused); - self - } - - /// Whether the window will be focusable or not. - #[must_use] - pub fn focusable(mut self, focusable: bool) -> Self { - self.window_builder = self.window_builder.focusable(focusable); - self - } - /// Whether the window should be maximized upon creation. #[must_use] pub fn maximized(mut self, maximized: bool) -> Self { @@ -603,37 +554,6 @@ impl<'a, R: Runtime, M: Manager> WindowBuilder<'a, R, M> { self } - /// Whether the window should be immediately visible upon creation. - #[must_use] - pub fn visible(mut self, visible: bool) -> Self { - self.window_builder = self.window_builder.visible(visible); - self - } - - /// Forces a theme or uses the system settings if None was provided. - /// - /// ## Platform-specific - /// - /// - **macOS**: Only supported on macOS 10.14+. - #[must_use] - pub fn theme(mut self, theme: Option) -> Self { - self.window_builder = self.window_builder.theme(theme); - self - } - - /// Whether the window should be transparent. If this is true, writing colors - /// with alpha values different than `1.0` will produce a transparent window. - #[cfg(any(not(target_os = "macos"), feature = "macos-private-api"))] - #[cfg_attr( - docsrs, - doc(cfg(any(not(target_os = "macos"), feature = "macos-private-api"))) - )] - #[must_use] - pub fn transparent(mut self, transparent: bool) -> Self { - self.window_builder = self.window_builder.transparent(transparent); - self - } - /// Whether the window should have borders and bars. #[must_use] pub fn decorations(mut self, decorations: bool) -> Self { @@ -668,13 +588,6 @@ impl<'a, R: Runtime, M: Manager> WindowBuilder<'a, R, M> { self } - /// Prevents the window contents from being captured by other apps. - #[must_use] - pub fn content_protected(mut self, protected: bool) -> Self { - self.window_builder = self.window_builder.content_protected(protected); - self - } - /// Sets the window icon. pub fn icon(mut self, icon: Image<'a>) -> crate::Result { self.window_builder = self.window_builder.icon(icon.into())?; @@ -901,106 +814,260 @@ impl<'a, R: Runtime, M: Manager> WindowBuilder<'a, R, M> { } } -impl> WindowBuilder<'_, R, M> { - /// Set the window and webview background color. - /// - /// ## Platform-specific: - /// - /// - **Windows**: alpha channel is ignored. +/// Window APIs. +#[cfg_attr(not(feature = "unstable"), allow(dead_code))] +impl<'a, R: Runtime, M: Manager> WindowBuilder<'a, R, M> { + /// The initial position of the window in logical pixels. #[must_use] - pub fn background_color(mut self, color: Color) -> Self { - self.window_builder = self.window_builder.background_color(color); + pub fn position(mut self, x: f64, y: f64) -> Self { + self.window_builder = self.window_builder.position(x, y); self } -} -/// A wrapper struct to hold the window menu state -/// and whether it is global per-app or specific to this window. -#[cfg(desktop)] -pub(crate) struct WindowMenu { - pub(crate) is_app_wide: bool, - pub(crate) menu: Menu, -} - -// TODO: expand these docs since this is a pretty important type -/// A window managed by Tauri. -/// -/// This type also implements [`Manager`] which allows you to manage other windows attached to -/// the same application. -#[default_runtime(crate::Wry, wry)] -pub struct Window { - /// The window created by the runtime. - pub(crate) window: DetachedWindow, - /// The manager to associate this window with. - pub(crate) manager: Arc>, - pub(crate) app_handle: AppHandle, - // The menu set for this window - #[cfg(desktop)] - pub(crate) menu: Arc>>>, - pub(crate) resources_table: Arc>, -} -impl std::fmt::Debug for Window { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Window") - .field("window", &self.window) - .field("manager", &self.manager) - .field("app_handle", &self.app_handle) - .finish() + /// Window size in logical pixels. + #[must_use] + pub fn inner_size(mut self, width: f64, height: f64) -> Self { + self.window_builder = self.window_builder.inner_size(width, height); + self } -} -impl raw_window_handle::HasWindowHandle for Window { - fn window_handle( - &self, - ) -> std::result::Result, raw_window_handle::HandleError> { - self.window.dispatcher.window_handle() + /// Window min inner size in logical pixels. + #[must_use] + pub fn min_inner_size(mut self, min_width: f64, min_height: f64) -> Self { + self.window_builder = self.window_builder.min_inner_size(min_width, min_height); + self } -} -impl raw_window_handle::HasDisplayHandle for Window { - fn display_handle( - &self, - ) -> std::result::Result, raw_window_handle::HandleError> { - self.app_handle.display_handle() + /// Window max inner size in logical pixels. + #[must_use] + pub fn max_inner_size(mut self, max_width: f64, max_height: f64) -> Self { + self.window_builder = self.window_builder.max_inner_size(max_width, max_height); + self } -} -impl Clone for Window { - fn clone(&self) -> Self { - Self { - window: self.window.clone(), - manager: self.manager.clone(), - app_handle: self.app_handle.clone(), - #[cfg(desktop)] - menu: self.menu.clone(), - resources_table: self.resources_table.clone(), - } + /// Window inner size constraints. + #[must_use] + pub fn inner_size_constraints( + mut self, + constraints: tauri_runtime::window::WindowSizeConstraints, + ) -> Self { + self.window_builder = self.window_builder.inner_size_constraints(constraints); + self } -} -impl Hash for Window { - /// Only use the [`Window`]'s label to represent its hash. - fn hash(&self, state: &mut H) { - self.window.label.hash(state) + /// Whether the window is resizable or not. + /// When resizable is set to false, native window's maximize button is automatically disabled. + #[must_use] + pub fn resizable(mut self, resizable: bool) -> Self { + self.window_builder = self.window_builder.resizable(resizable); + self } -} -impl Eq for Window {} -impl PartialEq for Window { - /// Only use the [`Window`]'s label to compare equality. - fn eq(&self, other: &Self) -> bool { - self.window.label.eq(&other.window.label) + /// The title of the window in the title bar. + #[must_use] + pub fn title>(mut self, title: S) -> Self { + self.window_builder = self.window_builder.title(title); + self } -} -impl Manager for Window { - fn resources_table(&self) -> MutexGuard<'_, ResourceTable> { + /// Sets the window to be initially focused. + #[must_use] + #[deprecated( + since = "1.2.0", + note = "The window is automatically focused by default. This function Will be removed in 3.0.0. Use `focused` instead." + )] + pub fn focus(mut self) -> Self { + self.window_builder = self.window_builder.focused(true); self - .resources_table - .lock() - .expect("poisoned window resources table") } -} + + /// Whether the window will be initially focused or not. + #[must_use] + pub fn focused(mut self, focused: bool) -> Self { + self.window_builder = self.window_builder.focused(focused); + self + } + + /// Whether the window will be focusable or not. + #[must_use] + pub fn focusable(mut self, focusable: bool) -> Self { + self.window_builder = self.window_builder.focusable(focusable); + self + } + + /// Whether the window should be immediately visible upon creation. + #[must_use] + pub fn visible(mut self, visible: bool) -> Self { + self.window_builder = self.window_builder.visible(visible); + self + } + + /// Forces a theme or uses the system settings if None was provided. + /// + /// ## Platform-specific + /// + /// - **macOS**: Only supported on macOS 10.14+. + #[must_use] + pub fn theme(mut self, theme: Option) -> Self { + self.window_builder = self.window_builder.theme(theme); + self + } + + /// Whether the window should be transparent. If this is true, writing colors + /// with alpha values different than `1.0` will produce a transparent window. + #[cfg(any(not(target_os = "macos"), feature = "macos-private-api"))] + #[cfg_attr( + docsrs, + doc(cfg(any(not(target_os = "macos"), feature = "macos-private-api"))) + )] + #[must_use] + pub fn transparent(mut self, transparent: bool) -> Self { + self.window_builder = self.window_builder.transparent(transparent); + self + } + + /// Prevents the window contents from being captured by other apps. + #[must_use] + pub fn content_protected(mut self, protected: bool) -> Self { + self.window_builder = self.window_builder.content_protected(protected); + self + } + + /// Set the window and webview background color. + /// + /// ## Platform-specific: + /// + /// - **Windows**: alpha channel is ignored. + #[must_use] + pub fn background_color(mut self, color: Color) -> Self { + self.window_builder = self.window_builder.background_color(color); + self + } +} + +#[cfg(target_os = "android")] +impl> WindowBuilder<'_, R, M> { + /// The name of the activity to create for this webview window. + pub fn activity_name>(mut self, class_name: S) -> Self { + self.window_builder = self.window_builder.activity_name(class_name); + self + } + + /// Sets the name of the activity that is creating this webview window. + /// + /// This is important to determine which stack the activity will belong to. + pub fn created_by_activity_name>(mut self, class_name: S) -> Self { + self.created_by_activity_name_set = true; + self.window_builder = self.window_builder.created_by_activity_name(class_name); + self + } +} + +/// iOS specific APIs +#[cfg(target_os = "ios")] +impl> WindowBuilder<'_, R, M> { + /// Sets the identifier of the scene that is requesting the new scene, + /// establishing a relationship between the two scenes. + /// + /// By default the system uses the foreground scene. + #[cfg(target_os = "ios")] + pub fn requested_by_scene_identifier(mut self, identifier: String) -> Self { + self.requested_by_scene_identifier_set = true; + self.window_builder = self + .window_builder + .requested_by_scene_identifier(identifier); + self + } +} + +/// A wrapper struct to hold the window menu state +/// and whether it is global per-app or specific to this window. +#[cfg(desktop)] +pub(crate) struct WindowMenu { + pub(crate) is_app_wide: bool, + pub(crate) menu: Menu, +} + +// TODO: expand these docs since this is a pretty important type +/// A window managed by Tauri. +/// +/// This type also implements [`Manager`] which allows you to manage other windows attached to +/// the same application. +#[default_runtime(crate::Wry, wry)] +pub struct Window { + /// The window created by the runtime. + pub(crate) window: DetachedWindow, + /// The manager to associate this window with. + pub(crate) manager: Arc>, + pub(crate) app_handle: AppHandle, + // The menu set for this window + #[cfg(desktop)] + pub(crate) menu: Arc>>>, + pub(crate) resources_table: Arc>, +} + +impl std::fmt::Debug for Window { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Window") + .field("window", &self.window) + .field("manager", &self.manager) + .field("app_handle", &self.app_handle) + .finish() + } +} + +impl raw_window_handle::HasWindowHandle for Window { + fn window_handle( + &self, + ) -> std::result::Result, raw_window_handle::HandleError> { + self.window.dispatcher.window_handle() + } +} + +impl raw_window_handle::HasDisplayHandle for Window { + fn display_handle( + &self, + ) -> std::result::Result, raw_window_handle::HandleError> { + self.app_handle.display_handle() + } +} + +impl Clone for Window { + fn clone(&self) -> Self { + Self { + window: self.window.clone(), + manager: self.manager.clone(), + app_handle: self.app_handle.clone(), + #[cfg(desktop)] + menu: self.menu.clone(), + resources_table: self.resources_table.clone(), + } + } +} + +impl Hash for Window { + /// Only use the [`Window`]'s label to represent its hash. + fn hash(&self, state: &mut H) { + self.window.label.hash(state) + } +} + +impl Eq for Window {} +impl PartialEq for Window { + /// Only use the [`Window`]'s label to compare equality. + fn eq(&self, other: &Self) -> bool { + self.window.label.eq(&other.window.label) + } +} + +impl Manager for Window { + fn resources_table(&self) -> MutexGuard<'_, ResourceTable> { + self + .resources_table + .lock() + .expect("poisoned window resources table") + } +} impl ManagerBase for Window { fn manager(&self) -> &AppManager { @@ -1018,6 +1085,16 @@ impl ManagerBase for Window { fn managed_app_handle(&self) -> &AppHandle { &self.app_handle } + + #[cfg(target_os = "android")] + fn activity_name(&self) -> Option> { + Some(self.activity_name()) + } + + #[cfg(target_os = "ios")] + fn scene_identifier(&self) -> Option> { + Some(self.scene_identifier()) + } } impl<'de, R: Runtime> CommandArg<'de, R> for Window { @@ -1628,6 +1705,22 @@ impl Window { self.window.dispatcher.default_vbox().map_err(Into::into) } + /// Returns the name of the Android activity associated with this window. + #[cfg(target_os = "android")] + pub fn activity_name(&self) -> crate::Result { + self.window.dispatcher.activity_name().map_err(Into::into) + } + + /// Returns the identifier of the UIScene tied to this window. + #[cfg(target_os = "ios")] + pub fn scene_identifier(&self) -> crate::Result { + self + .window + .dispatcher + .scene_identifier() + .map_err(Into::into) + } + /// Returns the current window theme. /// /// ## Platform-specific @@ -1654,36 +1747,8 @@ impl Window { } } -/// Desktop window setters and actions. -#[cfg(desktop)] +/// Window setters and actions. impl Window { - /// Centers the window. - pub fn center(&self) -> crate::Result<()> { - self.window.dispatcher.center().map_err(Into::into) - } - - /// Requests user attention to the window, this has no effect if the application - /// is already focused. How requesting for user attention manifests is platform dependent, - /// see `UserAttentionType` for details. - /// - /// Providing `None` will unset the request for user attention. Unsetting the request for - /// user attention might not be done automatically by the WM when the window receives input. - /// - /// ## Platform-specific - /// - /// - **macOS:** `None` has no effect. - /// - **Linux:** Urgency levels have the same effect. - pub fn request_user_attention( - &self, - request_type: Option, - ) -> crate::Result<()> { - self - .window - .dispatcher - .request_user_attention(request_type) - .map_err(Into::into) - } - /// Determines if this window should be resizable. /// When resizable is set to false, native window's maximize button is automatically disabled. pub fn set_resizable(&self, resizable: bool) -> crate::Result<()> { @@ -1694,49 +1759,6 @@ impl Window { .map_err(Into::into) } - /// Determines if this window's native maximize button should be enabled. - /// If resizable is set to false, this setting is ignored. - /// - /// ## Platform-specific - /// - /// - **macOS:** Disables the "zoom" button in the window titlebar, which is also used to enter fullscreen mode. - /// - **Linux / iOS / Android:** Unsupported. - pub fn set_maximizable(&self, maximizable: bool) -> crate::Result<()> { - self - .window - .dispatcher - .set_maximizable(maximizable) - .map_err(Into::into) - } - - /// Determines if this window's native minimize button should be enabled. - /// - /// ## Platform-specific - /// - /// - **Linux / iOS / Android:** Unsupported. - pub fn set_minimizable(&self, minimizable: bool) -> crate::Result<()> { - self - .window - .dispatcher - .set_minimizable(minimizable) - .map_err(Into::into) - } - - /// Determines if this window's native close button should be enabled. - /// - /// ## Platform-specific - /// - /// - **Linux:** "GTK+ will do its best to convince the window manager not to show a close button. - /// Depending on the system, this function may not have any effect when called on a window that is already visible" - /// - **iOS / Android:** Unsupported. - pub fn set_closable(&self, closable: bool) -> crate::Result<()> { - self - .window - .dispatcher - .set_closable(closable) - .map_err(Into::into) - } - /// Set this window's title. pub fn set_title(&self, title: &str) -> crate::Result<()> { self @@ -1755,26 +1777,6 @@ impl Window { .map_err(Into::into) } - /// Maximizes this window. - pub fn maximize(&self) -> crate::Result<()> { - self.window.dispatcher.maximize().map_err(Into::into) - } - - /// Un-maximizes this window. - pub fn unmaximize(&self) -> crate::Result<()> { - self.window.dispatcher.unmaximize().map_err(Into::into) - } - - /// Minimizes this window. - pub fn minimize(&self) -> crate::Result<()> { - self.window.dispatcher.minimize().map_err(Into::into) - } - - /// Un-minimizes this window. - pub fn unminimize(&self) -> crate::Result<()> { - self.window.dispatcher.unminimize().map_err(Into::into) - } - /// Show this window. pub fn show(&self) -> crate::Result<()> { self.window.dispatcher.show().map_err(Into::into) @@ -1785,7 +1787,7 @@ impl Window { self.window.dispatcher.hide().map_err(Into::into) } - /// Closes this window. It emits [`crate::RunEvent::CloseRequested`] first like a user-initiated close request so you can intercept it. + /// Closes this window. It emits [`crate::WindowEvent::CloseRequested`] first like a user-initiated close request so you can intercept it. pub fn close(&self) -> crate::Result<()> { self.window.dispatcher.close().map_err(Into::into) } @@ -1795,6 +1797,219 @@ impl Window { self.window.dispatcher.destroy().map_err(Into::into) } + /// Sets the window background color. + /// + /// ## Platform-specific: + /// + /// - **Windows:** alpha channel is ignored. + /// - **iOS / Android:** Unsupported. + pub fn set_background_color(&self, color: Option) -> crate::Result<()> { + self + .window + .dispatcher + .set_background_color(color) + .map_err(Into::into) + } + + /// Prevents the window contents from being captured by other apps. + pub fn set_content_protected(&self, protected: bool) -> crate::Result<()> { + self + .window + .dispatcher + .set_content_protected(protected) + .map_err(Into::into) + } + + /// Resizes this window. + pub fn set_size>(&self, size: S) -> crate::Result<()> { + self + .window + .dispatcher + .set_size(size.into()) + .map_err(Into::into) + } + + /// Sets this window's minimum inner size. + pub fn set_min_size>(&self, size: Option) -> crate::Result<()> { + self + .window + .dispatcher + .set_min_size(size.map(|s| s.into())) + .map_err(Into::into) + } + + /// Sets this window's maximum inner size. + pub fn set_max_size>(&self, size: Option) -> crate::Result<()> { + self + .window + .dispatcher + .set_max_size(size.map(|s| s.into())) + .map_err(Into::into) + } + + /// Sets this window's minimum inner width. + pub fn set_size_constraints( + &self, + constraints: tauri_runtime::window::WindowSizeConstraints, + ) -> crate::Result<()> { + self + .window + .dispatcher + .set_size_constraints(constraints) + .map_err(Into::into) + } + + /// Sets this window's position. + pub fn set_position>(&self, position: Pos) -> crate::Result<()> { + self + .window + .dispatcher + .set_position(position.into()) + .map_err(Into::into) + } + + /// Bring the window to front and focus. + pub fn set_focus(&self) -> crate::Result<()> { + self.window.dispatcher.set_focus().map_err(Into::into) + } + + /// Sets whether the window can be focused. + /// + /// ## Platform-specific + /// + /// - **macOS**: If the window is already focused, it is not possible to unfocus it after calling `set_focusable(false)`. + /// In this case, you might consider calling [`Window::set_focus`] but it will move the window to the back i.e. at the bottom in terms of z-order. + pub fn set_focusable(&self, focusable: bool) -> crate::Result<()> { + self + .window + .dispatcher + .set_focusable(focusable) + .map_err(Into::into) + } + + /// Sets the theme for this window. + /// + /// ## Platform-specific + /// + /// - **Linux / macOS**: Theme is app-wide and not specific to this window. + /// - **iOS / Android:** Unsupported. + pub fn set_theme(&self, theme: Option) -> crate::Result<()> { + self + .window + .dispatcher + .set_theme(theme) + .map_err(Into::::into)?; + #[cfg(windows)] + if let (Some(menu), Ok(hwnd)) = (self.menu(), self.hwnd()) { + let raw_hwnd = hwnd.0 as isize; + self.run_on_main_thread(move || { + let _ = unsafe { + menu.inner().set_theme_for_hwnd( + raw_hwnd, + theme + .map(crate::menu::map_to_menu_theme) + .unwrap_or(muda::MenuTheme::Auto), + ) + }; + })?; + }; + Ok(()) + } +} + +/// Desktop window setters and actions. +#[cfg(desktop)] +impl Window { + /// Centers the window. + pub fn center(&self) -> crate::Result<()> { + self.window.dispatcher.center().map_err(Into::into) + } + + /// Requests user attention to the window, this has no effect if the application + /// is already focused. How requesting for user attention manifests is platform dependent, + /// see `UserAttentionType` for details. + /// + /// Providing `None` will unset the request for user attention. Unsetting the request for + /// user attention might not be done automatically by the WM when the window receives input. + /// + /// ## Platform-specific + /// + /// - **macOS:** `None` has no effect. + /// - **Linux:** Urgency levels have the same effect. + pub fn request_user_attention( + &self, + request_type: Option, + ) -> crate::Result<()> { + self + .window + .dispatcher + .request_user_attention(request_type) + .map_err(Into::into) + } + + /// Determines if this window's native maximize button should be enabled. + /// If resizable is set to false, this setting is ignored. + /// + /// ## Platform-specific + /// + /// - **macOS:** Disables the "zoom" button in the window titlebar, which is also used to enter fullscreen mode. + /// - **Linux / iOS / Android:** Unsupported. + pub fn set_maximizable(&self, maximizable: bool) -> crate::Result<()> { + self + .window + .dispatcher + .set_maximizable(maximizable) + .map_err(Into::into) + } + + /// Determines if this window's native minimize button should be enabled. + /// + /// ## Platform-specific + /// + /// - **Linux / iOS / Android:** Unsupported. + pub fn set_minimizable(&self, minimizable: bool) -> crate::Result<()> { + self + .window + .dispatcher + .set_minimizable(minimizable) + .map_err(Into::into) + } + + /// Determines if this window's native close button should be enabled. + /// + /// ## Platform-specific + /// + /// - **Linux:** "GTK+ will do its best to convince the window manager not to show a close button. + /// Depending on the system, this function may not have any effect when called on a window that is already visible" + /// - **iOS / Android:** Unsupported. + pub fn set_closable(&self, closable: bool) -> crate::Result<()> { + self + .window + .dispatcher + .set_closable(closable) + .map_err(Into::into) + } + + /// Maximizes this window. + pub fn maximize(&self) -> crate::Result<()> { + self.window.dispatcher.maximize().map_err(Into::into) + } + + /// Un-maximizes this window. + pub fn unmaximize(&self) -> crate::Result<()> { + self.window.dispatcher.unmaximize().map_err(Into::into) + } + + /// Minimizes this window. + pub fn minimize(&self) -> crate::Result<()> { + self.window.dispatcher.minimize().map_err(Into::into) + } + + /// Un-minimizes this window. + pub fn unminimize(&self) -> crate::Result<()> { + self.window.dispatcher.unminimize().map_err(Into::into) + } + /// Determines if this window should be [decorated]. /// /// [decorated]: https://en.wikipedia.org/wiki/Window_(computing)#Window_decoration @@ -1897,77 +2112,6 @@ tauri::Builder::::new() .map_err(Into::into) } - /// Sets the window background color. - /// - /// ## Platform-specific: - /// - /// - **Windows:** alpha channel is ignored. - /// - **iOS / Android:** Unsupported. - pub fn set_background_color(&self, color: Option) -> crate::Result<()> { - self - .window - .dispatcher - .set_background_color(color) - .map_err(Into::into) - } - - /// Prevents the window contents from being captured by other apps. - pub fn set_content_protected(&self, protected: bool) -> crate::Result<()> { - self - .window - .dispatcher - .set_content_protected(protected) - .map_err(Into::into) - } - - /// Resizes this window. - pub fn set_size>(&self, size: S) -> crate::Result<()> { - self - .window - .dispatcher - .set_size(size.into()) - .map_err(Into::into) - } - - /// Sets this window's minimum inner size. - pub fn set_min_size>(&self, size: Option) -> crate::Result<()> { - self - .window - .dispatcher - .set_min_size(size.map(|s| s.into())) - .map_err(Into::into) - } - - /// Sets this window's maximum inner size. - pub fn set_max_size>(&self, size: Option) -> crate::Result<()> { - self - .window - .dispatcher - .set_max_size(size.map(|s| s.into())) - .map_err(Into::into) - } - - /// Sets this window's minimum inner width. - pub fn set_size_constraints( - &self, - constraints: tauri_runtime::window::WindowSizeConstraints, - ) -> crate::Result<()> { - self - .window - .dispatcher - .set_size_constraints(constraints) - .map_err(Into::into) - } - - /// Sets this window's position. - pub fn set_position>(&self, position: Pos) -> crate::Result<()> { - self - .window - .dispatcher - .set_position(position.into()) - .map_err(Into::into) - } - /// Determines if this window should be fullscreen. pub fn set_fullscreen(&self, fullscreen: bool) -> crate::Result<()> { self @@ -2000,25 +2144,6 @@ tauri::Builder::::new() self.set_fullscreen(enable) } - /// Bring the window to front and focus. - pub fn set_focus(&self) -> crate::Result<()> { - self.window.dispatcher.set_focus().map_err(Into::into) - } - - /// Sets whether the window can be focused. - /// - /// ## Platform-specific - /// - /// - **macOS**: If the window is already focused, it is not possible to unfocus it after calling `set_focusable(false)`. - /// In this case, you might consider calling [`Window::set_focus`] but it will move the window to the back i.e. at the bottom in terms of z-order. - pub fn set_focusable(&self, focusable: bool) -> crate::Result<()> { - self - .window - .dispatcher - .set_focusable(focusable) - .map_err(Into::into) - } - /// Sets this window' icon. pub fn set_icon(&self, icon: Image<'_>) -> crate::Result<()> { self @@ -2197,35 +2322,6 @@ tauri::Builder::::new() .set_traffic_light_position(position) .map_err(Into::into) } - - /// Sets the theme for this window. - /// - /// ## Platform-specific - /// - /// - **Linux / macOS**: Theme is app-wide and not specific to this window. - /// - **iOS / Android:** Unsupported. - pub fn set_theme(&self, theme: Option) -> crate::Result<()> { - self - .window - .dispatcher - .set_theme(theme) - .map_err(Into::::into)?; - #[cfg(windows)] - if let (Some(menu), Ok(hwnd)) = (self.menu(), self.hwnd()) { - let raw_hwnd = hwnd.0 as isize; - self.run_on_main_thread(move || { - let _ = unsafe { - menu.inner().set_theme_for_hwnd( - raw_hwnd, - theme - .map(crate::menu::map_to_menu_theme) - .unwrap_or(muda::MenuTheme::Auto), - ) - }; - })?; - }; - Ok(()) - } } /// Progress bar state. diff --git a/crates/tauri/src/window/plugin.rs b/crates/tauri/src/window/plugin.rs index 698d5f160f4b..0bea76cb59ce 100644 --- a/crates/tauri/src/window/plugin.rs +++ b/crates/tauri/src/window/plugin.rs @@ -5,23 +5,56 @@ //! The tauri plugin to create and manipulate windows from JS. use crate::{ - Runtime, + Runtime, Window, plugin::{Builder, TauriPlugin}, + sealed::ManagerBase, }; -#[cfg(desktop)] -mod desktop_commands { - use tauri_runtime::{ResizeDirection, window::WindowSizeConstraints}; - use tauri_utils::TitleBarStyle; +fn get_window(window: Window, label: Option) -> crate::Result> { + match label { + Some(l) if !l.is_empty() => window + .manager() + .get_window(&l) + .ok_or(crate::Error::WindowNotFound), + _ => Ok(window), + } +} +macro_rules! getter { + ($cmd: ident, $ret: ty) => { + #[command(root = "crate")] + pub async fn $cmd(window: Window, label: Option) -> crate::Result<$ret> { + get_window(window, label)?.$cmd().map_err(Into::into) + } + }; +} + +macro_rules! setter { + ($cmd: ident) => { + #[command(root = "crate")] + pub async fn $cmd(window: Window, label: Option) -> crate::Result<()> { + get_window(window, label)?.$cmd().map_err(Into::into) + } + }; + + ($cmd: ident, $input: ty) => { + #[command(root = "crate")] + pub async fn $cmd( + window: Window, + label: Option, + value: $input, + ) -> crate::Result<()> { + get_window(window, label)?.$cmd(value).map_err(Into::into) + } + }; +} + +mod commands { + use tauri_runtime::window::WindowSizeConstraints; use super::*; use crate::{ - AppHandle, CursorIcon, Manager, Monitor, PhysicalPosition, PhysicalSize, Position, Size, Theme, - UserAttentionType, Webview, Window, command, - sealed::ManagerBase, - utils::config::{WindowConfig, WindowEffectsConfig}, - window::Color, - window::{ProgressBarState, WindowBuilder}, + AppHandle, Monitor, PhysicalPosition, PhysicalSize, Position, Size, Theme, Window, command, + sealed::ManagerBase, utils::config::WindowConfig, window::Color, window::WindowBuilder, }; #[command(root = "crate")] @@ -30,110 +63,101 @@ mod desktop_commands { } #[command(root = "crate")] - pub async fn create(app: AppHandle, options: WindowConfig) -> crate::Result<()> { - WindowBuilder::from_config(&app, &options)?.build()?; + pub async fn create(window: Window, options: WindowConfig) -> crate::Result<()> { + WindowBuilder::from_config(&window, &options)?.build()?; Ok(()) } - fn get_window(window: Window, label: Option) -> crate::Result> { - match label { - Some(l) if !l.is_empty() => window - .manager() - .get_window(&l) - .ok_or(crate::Error::WindowNotFound), - _ => Ok(window), - } - } - - macro_rules! getter { - ($cmd: ident, $ret: ty) => { - #[command(root = "crate")] - pub async fn $cmd( - window: Window, - label: Option, - ) -> crate::Result<$ret> { - get_window(window, label)?.$cmd().map_err(Into::into) - } - }; - } - - macro_rules! setter { - ($cmd: ident) => { - #[command(root = "crate")] - pub async fn $cmd(window: Window, label: Option) -> crate::Result<()> { - get_window(window, label)?.$cmd().map_err(Into::into) - } - }; - - ($cmd: ident, $input: ty) => { - #[command(root = "crate")] - pub async fn $cmd( - window: Window, - label: Option, - value: $input, - ) -> crate::Result<()> { - get_window(window, label)?.$cmd(value).map_err(Into::into) - } - }; - } - getter!(scale_factor, f64); getter!(inner_position, PhysicalPosition); getter!(outer_position, PhysicalPosition); getter!(inner_size, PhysicalSize); getter!(outer_size, PhysicalSize); - getter!(is_fullscreen, bool); - getter!(is_minimized, bool); - getter!(is_maximized, bool); getter!(is_focused, bool); - getter!(is_decorated, bool); getter!(is_resizable, bool); - getter!(is_maximizable, bool); - getter!(is_minimizable, bool); - getter!(is_closable, bool); getter!(is_visible, bool); getter!(is_enabled, bool); getter!(title, String); + getter!(theme, Theme); + #[cfg(target_os = "android")] + getter!(activity_name, String); + #[cfg(target_os = "ios")] + getter!(scene_identifier, String); + + setter!(set_resizable, bool); + setter!(set_title, &str); + setter!(show); + setter!(hide); + setter!(close); + setter!(destroy); + setter!(set_content_protected, bool); + setter!(set_size, Size); + setter!(set_min_size, Option); + setter!(set_max_size, Option); + setter!(set_position, Position); + setter!(set_focus); + setter!(set_focusable, bool); + setter!(set_background_color, Option); + setter!(set_size_constraints, WindowSizeConstraints); + setter!(set_theme, Option); + setter!(set_enabled, bool); + getter!(current_monitor, Option); getter!(primary_monitor, Option); getter!(available_monitors, Vec); + + #[command(root = "crate")] + pub async fn monitor_from_point( + window: Window, + label: Option, + x: f64, + y: f64, + ) -> crate::Result> { + let window = get_window(window, label)?; + window.monitor_from_point(x, y) + } +} + +#[cfg(desktop)] +mod desktop_commands { + use tauri_runtime::ResizeDirection; + use tauri_utils::TitleBarStyle; + + use super::*; + use crate::{ + CursorIcon, Manager, PhysicalPosition, Position, UserAttentionType, Webview, command, + utils::config::WindowEffectsConfig, window::ProgressBarState, + }; + + getter!(is_fullscreen, bool); + getter!(is_minimized, bool); + getter!(is_maximized, bool); + getter!(is_decorated, bool); + getter!(is_maximizable, bool); + getter!(is_minimizable, bool); + getter!(is_closable, bool); getter!(cursor_position, PhysicalPosition); - getter!(theme, Theme); getter!(is_always_on_top, bool); setter!(center); setter!(request_user_attention, Option); - setter!(set_resizable, bool); setter!(set_maximizable, bool); setter!(set_minimizable, bool); setter!(set_closable, bool); - setter!(set_title, &str); setter!(maximize); setter!(unmaximize); setter!(minimize); setter!(unminimize); - setter!(show); - setter!(hide); - setter!(close); - setter!(destroy); setter!(set_decorations, bool); setter!(set_shadow, bool); setter!(set_effects, Option); setter!(set_always_on_top, bool); setter!(set_always_on_bottom, bool); - setter!(set_content_protected, bool); - setter!(set_size, Size); - setter!(set_min_size, Option); - setter!(set_max_size, Option); - setter!(set_position, Position); setter!(set_fullscreen, bool); setter!(set_simple_fullscreen, bool); - setter!(set_focus); - setter!(set_focusable, bool); setter!(set_skip_taskbar, bool); setter!(set_cursor_grab, bool); setter!(set_cursor_visible, bool); - setter!(set_background_color, Option); setter!(set_cursor_icon, CursorIcon); setter!(set_cursor_position, Position); setter!(set_ignore_cursor_events, bool); @@ -145,9 +169,6 @@ mod desktop_commands { setter!(set_badge_label, Option); setter!(set_visible_on_all_workspaces, bool); setter!(set_title_bar_style, TitleBarStyle); - setter!(set_size_constraints, WindowSizeConstraints); - setter!(set_theme, Option); - setter!(set_enabled, bool); #[command(root = "crate")] #[cfg(target_os = "windows")] @@ -207,17 +228,6 @@ mod desktop_commands { } Ok(()) } - - #[command(root = "crate")] - pub async fn monitor_from_point( - window: Window, - label: Option, - x: f64, - y: f64, - ) -> crate::Result> { - let window = get_window(window, label)?; - window.monitor_from_point(x, y) - } } /// Initializes the plugin. @@ -243,96 +253,95 @@ pub fn init() -> TauriPlugin { Builder::new("window") .js_init_script(init_script) - .invoke_handler( - #[cfg(desktop)] - crate::generate_handler![ - #![plugin(window)] - desktop_commands::create, - // getters - desktop_commands::get_all_windows, - desktop_commands::scale_factor, - desktop_commands::inner_position, - desktop_commands::outer_position, - desktop_commands::inner_size, - desktop_commands::outer_size, - desktop_commands::is_fullscreen, - desktop_commands::is_minimized, - desktop_commands::is_maximized, - desktop_commands::is_focused, - desktop_commands::is_decorated, - desktop_commands::is_resizable, - desktop_commands::is_maximizable, - desktop_commands::is_minimizable, - desktop_commands::is_closable, - desktop_commands::is_visible, - desktop_commands::is_enabled, - desktop_commands::title, - desktop_commands::current_monitor, - desktop_commands::primary_monitor, - desktop_commands::monitor_from_point, - desktop_commands::available_monitors, - desktop_commands::cursor_position, - desktop_commands::theme, - desktop_commands::is_always_on_top, - // setters - desktop_commands::center, - desktop_commands::request_user_attention, - desktop_commands::set_resizable, - desktop_commands::set_maximizable, - desktop_commands::set_minimizable, - desktop_commands::set_closable, - desktop_commands::set_title, - desktop_commands::maximize, - desktop_commands::unmaximize, - desktop_commands::minimize, - desktop_commands::unminimize, - desktop_commands::show, - desktop_commands::hide, - desktop_commands::close, - desktop_commands::destroy, - desktop_commands::set_decorations, - desktop_commands::set_shadow, - desktop_commands::set_effects, - desktop_commands::set_always_on_top, - desktop_commands::set_always_on_bottom, - desktop_commands::set_content_protected, - desktop_commands::set_size, - desktop_commands::set_min_size, - desktop_commands::set_max_size, - desktop_commands::set_size_constraints, - desktop_commands::set_position, - desktop_commands::set_fullscreen, - desktop_commands::set_simple_fullscreen, - desktop_commands::set_focus, - desktop_commands::set_focusable, - desktop_commands::set_enabled, - desktop_commands::set_skip_taskbar, - desktop_commands::set_cursor_grab, - desktop_commands::set_cursor_visible, - desktop_commands::set_cursor_icon, - desktop_commands::set_cursor_position, - desktop_commands::set_ignore_cursor_events, - desktop_commands::start_dragging, - desktop_commands::start_resize_dragging, - desktop_commands::set_badge_count, - #[cfg(target_os = "macos")] - desktop_commands::set_badge_label, - desktop_commands::set_progress_bar, - #[cfg(target_os = "windows")] - desktop_commands::set_overlay_icon, - desktop_commands::set_icon, - desktop_commands::set_visible_on_all_workspaces, - desktop_commands::set_background_color, - desktop_commands::set_title_bar_style, - desktop_commands::set_theme, - desktop_commands::toggle_maximize, - desktop_commands::internal_toggle_maximize, - ], - #[cfg(mobile)] - |invoke| { - invoke.resolver.reject("Window API not available on mobile"); - true - }, - ) + .invoke_handler(crate::generate_handler![ + #![plugin(window)] + commands::create, + // getters + commands::get_all_windows, + commands::scale_factor, + commands::inner_position, + commands::outer_position, + commands::inner_size, + commands::outer_size, + commands::is_focused, + commands::is_resizable, + commands::is_visible, + commands::is_enabled, + commands::title, + commands::theme, + #[cfg(target_os = "android")] + commands::activity_name, + #[cfg(target_os = "ios")] + commands::scene_identifier, + + commands::set_resizable, + commands::set_title, + commands::show, + commands::hide, + commands::close, + commands::destroy, + commands::set_content_protected, + commands::set_size, + commands::set_min_size, + commands::set_max_size, + commands::set_position, + commands::set_size_constraints, + commands::set_focus, + commands::set_focusable, + commands::set_enabled, + commands::set_background_color, + commands::set_theme, + commands::current_monitor, + commands::primary_monitor, + commands::monitor_from_point, + commands::available_monitors, + + #[cfg(desktop)] desktop_commands::is_fullscreen, + #[cfg(desktop)] desktop_commands::is_minimized, + #[cfg(desktop)] desktop_commands::is_maximized, + #[cfg(desktop)] desktop_commands::is_decorated, + #[cfg(desktop)] desktop_commands::is_maximizable, + #[cfg(desktop)] desktop_commands::is_minimizable, + #[cfg(desktop)] desktop_commands::is_closable, + + #[cfg(desktop)] desktop_commands::cursor_position, + #[cfg(desktop)] desktop_commands::is_always_on_top, + // setters + #[cfg(desktop)] desktop_commands::center, + #[cfg(desktop)] desktop_commands::request_user_attention, + #[cfg(desktop)] desktop_commands::set_maximizable, + #[cfg(desktop)] desktop_commands::set_minimizable, + #[cfg(desktop)] desktop_commands::set_closable, + #[cfg(desktop)] desktop_commands::maximize, + #[cfg(desktop)] desktop_commands::unmaximize, + #[cfg(desktop)] desktop_commands::minimize, + #[cfg(desktop)] desktop_commands::unminimize, + #[cfg(desktop)] desktop_commands::set_decorations, + #[cfg(desktop)] desktop_commands::set_shadow, + #[cfg(desktop)] desktop_commands::set_effects, + #[cfg(desktop)] desktop_commands::set_always_on_top, + #[cfg(desktop)] desktop_commands::set_always_on_bottom, + #[cfg(desktop)] desktop_commands::set_fullscreen, + #[cfg(desktop)] desktop_commands::set_simple_fullscreen, + #[cfg(desktop)] desktop_commands::set_skip_taskbar, + #[cfg(desktop)] desktop_commands::set_cursor_grab, + #[cfg(desktop)] desktop_commands::set_cursor_visible, + #[cfg(desktop)] desktop_commands::set_cursor_icon, + #[cfg(desktop)] desktop_commands::set_cursor_position, + #[cfg(desktop)] desktop_commands::set_ignore_cursor_events, + #[cfg(desktop)] desktop_commands::start_dragging, + #[cfg(desktop)] desktop_commands::start_resize_dragging, + #[cfg(desktop)] desktop_commands::set_badge_count, + #[cfg(target_os = "macos")] + #[cfg(desktop)] desktop_commands::set_badge_label, + #[cfg(desktop)] desktop_commands::set_progress_bar, + #[cfg(target_os = "windows")] + #[cfg(desktop)] desktop_commands::set_overlay_icon, + #[cfg(desktop)] desktop_commands::set_icon, + #[cfg(desktop)] desktop_commands::set_visible_on_all_workspaces, + #[cfg(desktop)] desktop_commands::set_title_bar_style, + #[cfg(desktop)] desktop_commands::toggle_maximize, + #[cfg(desktop)] desktop_commands::internal_toggle_maximize, + ]) .build() } diff --git a/crates/tests/acl/Cargo.toml b/crates/tests/acl/Cargo.toml index 0fe970b9990d..9e4f45b903b7 100644 --- a/crates/tests/acl/Cargo.toml +++ b/crates/tests/acl/Cargo.toml @@ -11,6 +11,6 @@ rust-version.workspace = true publish = false [dev-dependencies] -tauri-utils = { path = "../../tauri-utils/", features = ["build"] } +tauri-utils = { path = "../../tauri-utils/", features = ["build-2"] } serde_json = "1" insta = "1" diff --git a/examples/api/package.json b/examples/api/package.json index 84db2ff41f82..53780ea95741 100644 --- a/examples/api/package.json +++ b/examples/api/package.json @@ -13,12 +13,12 @@ "@tauri-apps/api": "../../packages/api/dist" }, "devDependencies": { - "@iconify-json/codicon": "^1.2.47", + "@iconify-json/codicon": "^1.2.49", "@iconify-json/ph": "^1.2.2", - "@sveltejs/vite-plugin-svelte": "^6.2.4", - "@unocss/extractor-svelte": "^66.6.5", - "svelte": "^5.53.7", - "unocss": "^66.6.5", - "vite": "^7.3.1" + "@sveltejs/vite-plugin-svelte": "^7.0.0", + "@unocss/extractor-svelte": "^66.6.6", + "svelte": "^5.55.7", + "unocss": "^66.6.6", + "vite": "^8.0.5" } } diff --git a/examples/api/src-tauri/Cargo.toml b/examples/api/src-tauri/Cargo.toml index a8545e229384..067287ca3194 100644 --- a/examples/api/src-tauri/Cargo.toml +++ b/examples/api/src-tauri/Cargo.toml @@ -19,7 +19,6 @@ tauri-build = { path = "../../../crates/tauri-build", features = [ [dependencies] serde_json = "1" serde = { version = "1", features = ["derive"] } -tiny_http = "0.11" log = "0.4.21" tauri-plugin-sample = { path = "./tauri-plugin-sample/" } tauri-plugin-log = "2" diff --git a/examples/api/src-tauri/capabilities/run-app.json b/examples/api/src-tauri/capabilities/run-app.json index 99a2c118ca37..e40e0ed408a0 100644 --- a/examples/api/src-tauri/capabilities/run-app.json +++ b/examples/api/src-tauri/capabilities/run-app.json @@ -6,6 +6,7 @@ "permissions": [ "core:window:allow-is-enabled", "core:window:allow-set-enabled", + "allow-*", { "identifier": "allow-log-operation", "allow": [ @@ -14,9 +15,6 @@ } ] }, - "allow-perform-request", - "allow-echo", - "allow-spam", "app-menu:default", "sample:allow-ping-scoped", "sample:global-scope", diff --git a/examples/api/src-tauri/permissions/autogenerated/commands.toml b/examples/api/src-tauri/permissions/autogenerated/commands.toml new file mode 100644 index 000000000000..fefdeaee6989 --- /dev/null +++ b/examples/api/src-tauri/permissions/autogenerated/commands.toml @@ -0,0 +1,3 @@ +# Automatically generated - DO NOT EDIT! + +commands = ["log_operation", "perform_request", "echo", "spam"] diff --git a/examples/api/src-tauri/permissions/autogenerated/echo.toml b/examples/api/src-tauri/permissions/autogenerated/echo.toml deleted file mode 100644 index d8c458ee874c..000000000000 --- a/examples/api/src-tauri/permissions/autogenerated/echo.toml +++ /dev/null @@ -1,11 +0,0 @@ -# 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"] diff --git a/examples/api/src-tauri/permissions/autogenerated/log_operation.toml b/examples/api/src-tauri/permissions/autogenerated/log_operation.toml deleted file mode 100644 index a1e88b5958ea..000000000000 --- a/examples/api/src-tauri/permissions/autogenerated/log_operation.toml +++ /dev/null @@ -1,11 +0,0 @@ -# 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"] diff --git a/examples/api/src-tauri/permissions/autogenerated/perform_request.toml b/examples/api/src-tauri/permissions/autogenerated/perform_request.toml deleted file mode 100644 index 0d12b9d1c6bb..000000000000 --- a/examples/api/src-tauri/permissions/autogenerated/perform_request.toml +++ /dev/null @@ -1,11 +0,0 @@ -# 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"] diff --git a/examples/api/src-tauri/permissions/autogenerated/spam.toml b/examples/api/src-tauri/permissions/autogenerated/spam.toml deleted file mode 100644 index dfa659eb147f..000000000000 --- a/examples/api/src-tauri/permissions/autogenerated/spam.toml +++ /dev/null @@ -1,11 +0,0 @@ -# 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"] diff --git a/examples/api/src-tauri/src/lib.rs b/examples/api/src-tauri/src/lib.rs index 74020a4179c3..ee2936046ba1 100644 --- a/examples/api/src-tauri/src/lib.rs +++ b/examples/api/src-tauri/src/lib.rs @@ -18,9 +18,11 @@ use tauri::{ use tauri::{Manager, RunEvent}; use tauri_plugin_sample::{PingRequest, SampleExt}; -#[cfg(feature = "cef")] +#[cfg(test)] +type TauriRuntime = tauri::test::MockRuntime; +#[cfg(all(not(test), feature = "cef"))] type TauriRuntime = tauri::Cef; -#[cfg(not(feature = "cef"))] +#[cfg(all(not(test), not(feature = "cef")))] type TauriRuntime = tauri::Wry; #[derive(Clone, Serialize)] @@ -37,7 +39,7 @@ pub struct PopupMenu(#[allow(dead_code)] tauri::menu::Menu); #[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(feature = "cef", tauri::cef_entry_point)] pub fn run() { - run_app(tauri::Builder::::default(), |_app| {}); + run_app(tauri::Builder::::new(), |_app| {}); } pub fn run_app) + Send + 'static>( @@ -95,7 +97,7 @@ pub fn run_app) + Send + 'static>( let number = created_window_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed); - let builder = tauri::WebviewWindowBuilder::new( + let builder = WebviewWindowBuilder::new( &app_, format!("new-{number}"), tauri::WebviewUrl::External(if cfg!(feature = "cef") { @@ -170,31 +172,6 @@ pub fn run_app) + Send + 'static>( assert_eq!(res.value, value); } - #[cfg(desktop)] - std::thread::spawn(|| { - let server = match tiny_http::Server::http("localhost:3003") { - Ok(s) => s, - Err(e) => { - eprintln!("{e}"); - std::process::exit(1); - } - }; - loop { - if let Ok(mut request) = server.recv() { - let mut body = Vec::new(); - let _ = request.as_reader().read_to_end(&mut body); - let response = tiny_http::Response::new( - tiny_http::StatusCode(200), - request.headers().to_vec(), - std::io::Cursor::new(body), - request.body_length(), - None, - ); - let _ = request.respond(response); - } - } - }); - setup(app); Ok(()) @@ -229,18 +206,19 @@ pub fn run_app) + Send + 'static>( #[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| { - #[cfg(all(desktop, not(test)))] + #[cfg(not(test))] match &_event { #[cfg(not(feature = "cef"))] - RunEvent::ExitRequested { api, code, .. } => { + RunEvent::ExitRequested { api, code, .. } if code.is_none() => { // 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 - if code.is_none() { - api.prevent_exit(); - } + api.prevent_exit(); } + #[cfg(desktop)] RunEvent::WindowEvent { event: tauri::WindowEvent::CloseRequested { api, .. }, label, @@ -256,6 +234,20 @@ pub fn run_app) + Send + 'static>( .destroy() .unwrap(); } + #[cfg(target_os = "ios")] + RunEvent::SceneRequested { .. } => { + counter += 1; + WebviewWindowBuilder::new( + _app_handle, + format!("main-from-scene-{counter}"), + WebviewUrl::default(), + ) + .build() + .unwrap(); + } + RunEvent::Opened { urls } => { + println!("opened urls: {:?}", urls); + } _ => (), } }) diff --git a/examples/api/src-tauri/tauri-plugin-sample/permissions/autogenerated/commands/commands.toml b/examples/api/src-tauri/tauri-plugin-sample/permissions/autogenerated/commands/commands.toml new file mode 100644 index 000000000000..1fc2a0752ddb --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/permissions/autogenerated/commands/commands.toml @@ -0,0 +1,5 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +commands = ["ping"] diff --git a/examples/api/src-tauri/tauri-plugin-sample/permissions/autogenerated/commands/ping.toml b/examples/api/src-tauri/tauri-plugin-sample/permissions/autogenerated/commands/ping.toml deleted file mode 100644 index 1d1358807e5b..000000000000 --- a/examples/api/src-tauri/tauri-plugin-sample/permissions/autogenerated/commands/ping.toml +++ /dev/null @@ -1,13 +0,0 @@ -# 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"] diff --git a/examples/api/src-tauri/tauri-plugin-sample/permissions/schemas/schema.json b/examples/api/src-tauri/tauri-plugin-sample/permissions/schemas/schema.json index 7cc6cab1d6da..df3dad39e61e 100644 --- a/examples/api/src-tauri/tauri-plugin-sample/permissions/schemas/schema.json +++ b/examples/api/src-tauri/tauri-plugin-sample/permissions/schemas/schema.json @@ -29,6 +29,13 @@ "$ref": "#/definitions/Permission" }, "default": [] + }, + "commands": { + "description": "A list of command names that get `allow-$command` and `deny-$command` permissions\nautogenerated on demand instead of being stored as explicit permissions.\n\nSee [`Manifest::command_permission`] and the ACL resolver for how these are expanded.", + "type": "array", + "items": { + "type": "string" + } } }, "definitions": { diff --git a/examples/api/src-tauri/tauri.conf.json b/examples/api/src-tauri/tauri.conf.json index b40404c820ad..b99e62599e81 100644 --- a/examples/api/src-tauri/tauri.conf.json +++ b/examples/api/src-tauri/tauri.conf.json @@ -75,6 +75,37 @@ }, "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", diff --git a/examples/api/src/components/MenuItemBuilder.svelte b/examples/api/src/components/MenuItemBuilder.svelte index d907e0951a7b..0f93436978a4 100644 --- a/examples/api/src/components/MenuItemBuilder.svelte +++ b/examples/api/src/components/MenuItemBuilder.svelte @@ -32,7 +32,8 @@ 'ShowAll', 'CloseWindow', 'Quit', - 'Services' + 'Services', + 'BringAllToFront' ] function onKindChange(event) { diff --git a/examples/commands/commands.rs b/examples/commands/commands.rs index 022b1241d012..4f2a814aa0e1 100644 --- a/examples/commands/commands.rs +++ b/examples/commands/commands.rs @@ -25,3 +25,8 @@ pub fn simple_command(the_argument: String) { pub fn stateful_command(the_argument: Option, state: State<'_, super::MyState>) { println!("{:?} {:?}", the_argument, state.inner()); } + +#[command(rename = "renamed_command_in_mod_new")] +pub fn renamed_command_in_mod() { + println!("renamed command in mod called"); +} diff --git a/examples/commands/index.html b/examples/commands/index.html index aed95c21ca7d..ef95405fd89a 100644 --- a/examples/commands/index.html +++ b/examples/commands/index.html @@ -63,7 +63,11 @@

Tauri Commands

{ name: 'command_arguments_tuple_struct', args: { inlinePerson: ['ferris', 6] } - } + }, + { name: 'renamed_command' }, + { name: 'renamed_command_new' }, + { name: 'renamed_command_in_mod' }, + { name: 'renamed_command_in_mod_new' } ] for (const command of commands) { diff --git a/examples/commands/main.rs b/examples/commands/main.rs index 474b07216f78..a6b49b949ded 100644 --- a/examples/commands/main.rs +++ b/examples/commands/main.rs @@ -6,7 +6,7 @@ // we move some basic commands to a separate module just to show it works mod commands; -use commands::{cmd, invoke, message, resolver}; +use commands::{cmd, invoke, message, renamed_command_in_mod, resolver}; use serde::Deserialize; use tauri::{ @@ -187,6 +187,11 @@ fn command_arguments_wild(_: Window) { println!("we saw the wildcard!") } +#[command(rename = "renamed_command_new")] +fn renamed_command() { + println!("renamed command called") +} + #[derive(Deserialize)] struct Person<'a> { name: &'a str, @@ -251,6 +256,8 @@ fn main() { future_simple_command, async_stateful_command, command_arguments_wild, + renamed_command, + renamed_command_in_mod, command_arguments_struct, simple_command_with_result, async_simple_command_snake, diff --git a/packages/api/CHANGELOG.md b/packages/api/CHANGELOG.md index 5af686715ac4..078cb72cd8d3 100644 --- a/packages/api/CHANGELOG.md +++ b/packages/api/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## \[2.11.0] + +### New Features + +- [`074299c08`](https://www.github.com/tauri-apps/tauri/commit/074299c08dd99d2e1c57796f55ab24bc1d3976cc) ([#14307](https://www.github.com/tauri-apps/tauri/pull/14307)) Add Bring All to Front predefined menu item type +- [`a12142a48`](https://www.github.com/tauri-apps/tauri/commit/a12142a481f7a19b69e88ee36a438b1db71b36f5) ([#14357](https://www.github.com/tauri-apps/tauri/pull/14357)) Add macos support for setting the icon and icon template state in the same step of the main thread, to prevent flickering. +- [`001c8fe3d`](https://www.github.com/tauri-apps/tauri/commit/001c8fe3d288802de9a8c29cfd2f46f9220d97c5) ([#14722](https://www.github.com/tauri-apps/tauri/pull/14722)) Add a WebView option to control browser-level general autofill behavior. This option does not disable password or credit card autofill. On Windows (WebView2), setting it to true disables the general autofill "Suggestions" UI, which may appear even when `autocomplete="off"` is specified on input elements. On Linux, macOS, iOS, and Android, this option is currently unsupported and performs no operation. +- [`eb0312ea9`](https://www.github.com/tauri-apps/tauri/commit/eb0312ea9e493954298ac0b3fdaae7eafb52750e) ([#15199](https://www.github.com/tauri-apps/tauri/pull/15199)) Propagates the `Event::Suspended` and `Event::Resumed` events from `tao` when they are emitted on mobile targets. + ## \[2.10.1] ### Bug Fixes diff --git a/packages/api/package.json b/packages/api/package.json index db3744fc2446..367f80a6ad4d 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@tauri-apps/api", - "version": "2.10.1", + "version": "2.11.0", "description": "Tauri API definitions", "funding": { "type": "opencollective", @@ -55,9 +55,9 @@ "eslint-plugin-security": "4.0.0", "fast-glob": "3.3.3", "globals": "^17.4.0", - "rollup": "4.59.0", + "rollup": "4.60.3", "tslib": "^2.8.1", - "typescript": "^5.9.3", - "typescript-eslint": "^8.56.1" + "typescript": "^6.0.0", + "typescript-eslint": "^8.58.2" } } diff --git a/packages/api/src/app.ts b/packages/api/src/app.ts index 55b28043631f..82cc51859224 100644 --- a/packages/api/src/app.ts +++ b/packages/api/src/app.ts @@ -274,6 +274,10 @@ async function onBackButtonPress( ) } +async function supportsMultipleWindows(): Promise { + return invoke('plugin:app|supports_multiple_windows') +} + export { getName, getVersion, @@ -288,5 +292,6 @@ export { setDockVisibility, getBundleType, type OnBackButtonPressPayload, - onBackButtonPress + onBackButtonPress, + supportsMultipleWindows } diff --git a/packages/api/src/event.ts b/packages/api/src/event.ts index 1cf0f542a32d..7d8da18c83fe 100644 --- a/packages/api/src/event.ts +++ b/packages/api/src/event.ts @@ -65,6 +65,8 @@ enum TauriEvent { WINDOW_SCALE_FACTOR_CHANGED = 'tauri://scale-change', WINDOW_THEME_CHANGED = 'tauri://theme-changed', WINDOW_CREATED = 'tauri://window-created', + WINDOW_SUSPENDED = 'tauri://suspended', + WINDOW_RESUMED = 'tauri://resumed', WEBVIEW_CREATED = 'tauri://webview-created', DRAG_ENTER = 'tauri://drag-enter', DRAG_OVER = 'tauri://drag-over', diff --git a/packages/api/src/menu/predefinedMenuItem.ts b/packages/api/src/menu/predefinedMenuItem.ts index fed9a543facc..1517a35fa970 100644 --- a/packages/api/src/menu/predefinedMenuItem.ts +++ b/packages/api/src/menu/predefinedMenuItem.ts @@ -102,6 +102,7 @@ export interface PredefinedMenuItemOptions { | 'CloseWindow' | 'Quit' | 'Services' + | 'BringAllToFront' | { About: AboutMetadata | null } diff --git a/packages/api/src/tray.ts b/packages/api/src/tray.ts index fabe3e3567bf..c7bce20abb65 100644 --- a/packages/api/src/tray.ts +++ b/packages/api/src/tray.ts @@ -296,6 +296,31 @@ export class TrayIcon extends Resource { }) } + /** + * Sets a new tray icon and template status atomically. **macOS only**. + * + * Note that you may need the `image-ico` or `image-png` Cargo features to use this API. + * To enable it, change your Cargo.toml file: + * ```toml + * [dependencies] + * tauri = { version = "...", features = ["...", "image-png"] } + * ``` + */ + async setIconWithAsTemplate( + icon: string | Image | Uint8Array | ArrayBuffer | number[] | null, + asTemplate: boolean + ): Promise { + let trayIcon = null + if (icon) { + trayIcon = transformImage(icon) + } + return invoke('plugin:tray|set_icon_with_as_template', { + rid: this.rid, + icon: trayIcon, + asTemplate + }) + } + /** * Disable or enable showing the tray menu on left click. * diff --git a/packages/api/src/webview.ts b/packages/api/src/webview.ts index 47a497fb6c84..968983c02eda 100644 --- a/packages/api/src/webview.ts +++ b/packages/api/src/webview.ts @@ -897,6 +897,25 @@ interface WebviewOptions { * - **Linux / Android / iOS / macOS**: Unsupported. Only supports `Default` and performs no operation. */ scrollBarStyle?: ScrollBarStyle + /** + * Controls the WebView's browser-level general autofill behavior. + * + * **This option does not disable password or credit card autofill.** + * + * When set to `false`, the WebView will not automatically populate general form + * fields using previously stored data such as addresses or contact information. + * + * If not specified, this is `true` by default. + * + * ## Platform-specific + * + * - **Windows**: Supported. WebView2's autofill feature (called "Suggestions") + * may not honor `autocomplete="off"` on input elements in some cases. + * - **Linux / Android / iOS / macOS**: Unsupported and performs no operation. + * + * @since 2.11.0 + */ + generalAutofillEnabled?: boolean } export { Webview, getCurrentWebview, getAllWebviews } diff --git a/packages/api/src/window.ts b/packages/api/src/window.ts index 8a11d2cc3cb6..a6b157ad84d3 100644 --- a/packages/api/src/window.ts +++ b/packages/api/src/window.ts @@ -820,6 +820,18 @@ class Window { }) } + async activityName(): Promise { + return invoke('plugin:window|activity_name', { + label: this.label + }) + } + + async sceneIdentifier(): Promise { + return invoke('plugin:window|scene_identifier', { + label: this.label + }) + } + // Setters /** @@ -2511,6 +2523,23 @@ interface WindowOptions { * - **Linux / Android / iOS / macOS**: Unsupported. Only supports `Default` and performs no operation. */ scrollBarStyle?: ScrollBarStyle + /** + * The name of the Android activity to create for this window. + */ + activityName?: string + /** + * The name of the Android activity that is creating this webview window. + * + * This is important to determine which stack the activity will belong to. + */ + createdByActivityName?: string + /** + * Sets the identifier of the UIScene that is requesting the creation of this new scene, + * establishing a relationship between the two scenes. + * + * By default the system uses the foreground scene. + */ + requestedBySceneIdentifier?: string } function mapMonitor(m: Monitor | null): Monitor | null { diff --git a/packages/cli/.cef-cli-version b/packages/cli/.cef-cli-version new file mode 100644 index 000000000000..5b8e82cbc2a7 --- /dev/null +++ b/packages/cli/.cef-cli-version @@ -0,0 +1 @@ +3.0.0-alpha.6 diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 236ecc1d3e69..6c89dbbd5c1b 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,53 @@ # Changelog +## \[2.11.2] + +### Dependencies + +- Upgraded to `tauri-cli@2.11.2` + +## \[2.11.1] + +### Dependencies + +- Upgraded to `tauri-cli@2.11.1` + +## \[2.11.0] + +### New Features + +- [`926a57bb0`](https://www.github.com/tauri-apps/tauri/commit/926a57bb0851e45d47ad1ee68fc96a9c25754c7c) ([#15201](https://www.github.com/tauri-apps/tauri/pull/15201)) Added uninstaller icon and uninstaller header image support for NSIS installer. + + Notes: + + - For `tauri-bundler` lib users, the `NsisSettings` now has 2 new fields `uninstaller_icon` and `uninstaller_header_image` which can be a breaking change + - When bundling with NSIS, users can add `uninstallerIcon` and `uninstallerHeaderImage` under `bundle > windows > nsis` to configure them. +- [`764b9139a`](https://www.github.com/tauri-apps/tauri/commit/764b9139a32de149d8a914a6b5ec6cd1937c64eb) ([#14313](https://www.github.com/tauri-apps/tauri/pull/14313)) Prompt to restart the Android emulator if it is not connected to adb. +- [`5dc2cee60`](https://www.github.com/tauri-apps/tauri/commit/5dc2cee60370665af88c185684432e425b1c987d) ([#14793](https://www.github.com/tauri-apps/tauri/pull/14793)) Added support for `minimumWebview2Version` option support for the MSI (Wix) installer, the old `bundle > windows > nsis > minimumWebview2Version` is now deprecated in favor of `bundle > windows > minimumWebview2Version` + + Notes: + + - For anyone relying on the `WVRTINSTALLED` `Property` tag in `main.wxs`, it is now renamed to `INSTALLED_WEBVIEW2_VERSION` + - For `tauri-bundler` lib users, the `WindowsSettings` now has a new field `minimum_webview2_version` which can be a breaking change + +### Enhancements + +- [`be0e4bd2d`](https://www.github.com/tauri-apps/tauri/commit/be0e4bd2da02eb6cc75a8dc7c81663277e64c590) ([#15218](https://www.github.com/tauri-apps/tauri/pull/15218)) Added Vietnamese translations for the NSIS installer +- [`8718d0816`](https://www.github.com/tauri-apps/tauri/commit/8718d08163f074dfc53387ebd1d823f9c28280ee) ([#15033](https://www.github.com/tauri-apps/tauri/pull/15033)) Show the context before prompting for updater signing key password + +### Bug Fixes + +- [`fcb702ec4`](https://www.github.com/tauri-apps/tauri/commit/fcb702ec4d924e81943efaeebea8d3edb7289c33) ([#14954](https://www.github.com/tauri-apps/tauri/pull/14954)) Fix `build --bundles` to allow `nsis` arg in linux+macOS +- [`80c1425af`](https://www.github.com/tauri-apps/tauri/commit/80c1425af86058b1fc9489a30f778b6288d79b6b) ([#14921](https://www.github.com/tauri-apps/tauri/pull/14921)) Fix iOS build failure when `Metal Toolchain` is installed by using explicit `$(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain` path instead of `$(TOOLCHAIN_DIR)` for Swift library search paths. + +### What's Changed + +- [`9979cde1c`](https://www.github.com/tauri-apps/tauri/commit/9979cde1c5534dafb1a07cc4dc2bc280d15d2f66) ([#15175](https://www.github.com/tauri-apps/tauri/pull/15175)) Update NSIS installer Italian translations + +### Dependencies + +- Upgraded to `tauri-cli@2.11.0` + ## \[2.10.1] ### Bug Fixes diff --git a/packages/cli/Cargo.toml b/packages/cli/Cargo.toml index f4c989f3ec8d..3aab1abc6253 100644 --- a/packages/cli/Cargo.toml +++ b/packages/cli/Cargo.toml @@ -7,13 +7,15 @@ version = "0.0.0" crate-type = ["cdylib"] [dependencies] -napi = "3" -napi-derive = "3" +napi = "=3.4" +napi-sys = "=3.0" +napi-derive-backend = "=3.0.0" +napi-derive = "=3.3.0" tauri-cli = { path = "../../crates/tauri-cli", default-features = false } log = "0.4.21" [build-dependencies] -napi-build = "2.2" +napi-build = "=2.2" [features] default = ["tauri-cli/default"] diff --git a/packages/cli/index.js b/packages/cli/index.js index c9e399696117..9034ed8cd24e 100644 --- a/packages/cli/index.js +++ b/packages/cli/index.js @@ -81,8 +81,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-android-arm64') const bindingPackageVersion = require('@tauri-apps/cli-android-arm64/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -97,8 +97,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-android-arm-eabi') const bindingPackageVersion = require('@tauri-apps/cli-android-arm-eabi/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -118,8 +118,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-win32-x64-gnu') const bindingPackageVersion = require('@tauri-apps/cli-win32-x64-gnu/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -134,8 +134,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-win32-x64-msvc') const bindingPackageVersion = require('@tauri-apps/cli-win32-x64-msvc/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -151,8 +151,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-win32-ia32-msvc') const bindingPackageVersion = require('@tauri-apps/cli-win32-ia32-msvc/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -167,8 +167,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-win32-arm64-msvc') const bindingPackageVersion = require('@tauri-apps/cli-win32-arm64-msvc/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -186,8 +186,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-darwin-universal') const bindingPackageVersion = require('@tauri-apps/cli-darwin-universal/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -202,8 +202,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-darwin-x64') const bindingPackageVersion = require('@tauri-apps/cli-darwin-x64/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -218,8 +218,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-darwin-arm64') const bindingPackageVersion = require('@tauri-apps/cli-darwin-arm64/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -238,8 +238,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-freebsd-x64') const bindingPackageVersion = require('@tauri-apps/cli-freebsd-x64/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -254,8 +254,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-freebsd-arm64') const bindingPackageVersion = require('@tauri-apps/cli-freebsd-arm64/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -275,8 +275,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-linux-x64-musl') const bindingPackageVersion = require('@tauri-apps/cli-linux-x64-musl/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -291,8 +291,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-linux-x64-gnu') const bindingPackageVersion = require('@tauri-apps/cli-linux-x64-gnu/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -309,8 +309,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-linux-arm64-musl') const bindingPackageVersion = require('@tauri-apps/cli-linux-arm64-musl/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -325,8 +325,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-linux-arm64-gnu') const bindingPackageVersion = require('@tauri-apps/cli-linux-arm64-gnu/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -343,8 +343,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-linux-arm-musleabihf') const bindingPackageVersion = require('@tauri-apps/cli-linux-arm-musleabihf/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -359,8 +359,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-linux-arm-gnueabihf') const bindingPackageVersion = require('@tauri-apps/cli-linux-arm-gnueabihf/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -377,8 +377,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-linux-loong64-musl') const bindingPackageVersion = require('@tauri-apps/cli-linux-loong64-musl/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -393,8 +393,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-linux-loong64-gnu') const bindingPackageVersion = require('@tauri-apps/cli-linux-loong64-gnu/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -411,8 +411,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-linux-riscv64-musl') const bindingPackageVersion = require('@tauri-apps/cli-linux-riscv64-musl/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -427,8 +427,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-linux-riscv64-gnu') const bindingPackageVersion = require('@tauri-apps/cli-linux-riscv64-gnu/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -444,8 +444,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-linux-ppc64-gnu') const bindingPackageVersion = require('@tauri-apps/cli-linux-ppc64-gnu/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -460,8 +460,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-linux-s390x-gnu') const bindingPackageVersion = require('@tauri-apps/cli-linux-s390x-gnu/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -480,8 +480,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-openharmony-arm64') const bindingPackageVersion = require('@tauri-apps/cli-openharmony-arm64/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -496,8 +496,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-openharmony-x64') const bindingPackageVersion = require('@tauri-apps/cli-openharmony-x64/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -512,8 +512,8 @@ function requireNative() { try { const binding = require('@tauri-apps/cli-openharmony-arm') const bindingPackageVersion = require('@tauri-apps/cli-openharmony-arm/package.json').version - if (bindingPackageVersion !== '2.10.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.10.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.10.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.10.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -540,13 +540,17 @@ if (!nativeBinding || process.env.NAPI_RS_FORCE_WASI) { wasiBindingError = err } } - if (!nativeBinding) { + if (!nativeBinding || process.env.NAPI_RS_FORCE_WASI) { try { wasiBinding = require('@tauri-apps/cli-wasm32-wasi') nativeBinding = wasiBinding } catch (err) { if (process.env.NAPI_RS_FORCE_WASI) { - wasiBindingError.cause = err + if (!wasiBindingError) { + wasiBindingError = err + } else { + wasiBindingError.cause = err + } loadErrors.push(err) } } diff --git a/packages/cli/package.json b/packages/cli/package.json index d289be61f669..aea4940435d7 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@tauri-apps/cli", - "version": "2.10.1", + "version": "2.11.2", "description": "Command line interface for building Tauri apps", "type": "commonjs", "funding": { @@ -41,7 +41,7 @@ ] }, "devDependencies": { - "@napi-rs/cli": "^3.5.1", + "@napi-rs/cli": "3.4.1", "@types/node": "^24.11.0", "cross-env": "10.1.0", "vitest": "^4.0.18" @@ -58,8 +58,9 @@ "postbuild": "node append-headers.js", "build:debug": "cross-env TARGET=node napi build --platform", "postbuild:debug": "node append-headers.js", - "prepublishOnly": "napi prepublish -t npm --gh-release-id $RELEASE_ID", + "prepublishOnly": "napi prepublish -t npm --gh-release-id $RELEASE_ID --skip-optional-publish", "prepack": "cp ../../crates/tauri-schema-generator/schemas/config.schema.json .", + "postpublish": "node ./postpublish.js", "version": "napi version", "test": "vitest run", "tauri": "node ./tauri.js" diff --git a/packages/cli/postpublish.js b/packages/cli/postpublish.js new file mode 100644 index 000000000000..25b978dcbfd5 --- /dev/null +++ b/packages/cli/postpublish.js @@ -0,0 +1,32 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +const { execFileSync } = require('node:child_process') +const { readdirSync } = require('node:fs') +const { join } = require('node:path') + +function run(command, args, cwd = process.cwd()) { + execFileSync(command, args, { + cwd, + stdio: 'inherit', + env: process.env + }) +} + +const cliDir = process.cwd() +const npmDir = join(cliDir, 'npm') +const publishTag = process.env.npm_config_tag || 'latest' + +console.log( + `Publishing platform npm packages from postpublish hook using tag "${publishTag}"...` +) + +for (const entry of readdirSync(npmDir, { withFileTypes: true })) { + if (!entry.isDirectory()) { + continue + } + + const pkgDir = join(npmDir, entry.name) + run('npm', ['publish', '--tag', publishTag, '--ignore-scripts'], pkgDir) +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 34c0a30cfa71..b922a7d93b39 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,9 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + ws@>=8.0.0 <8.20.1: '>=8.20.1' + importers: .: @@ -15,8 +18,8 @@ importers: crates/tauri-schema-worker: devDependencies: wrangler: - specifier: ^4.70.0 - version: 4.70.0 + specifier: ^4.75.0 + version: 4.75.0 examples/api: dependencies: @@ -25,26 +28,26 @@ importers: version: link:../../packages/api/dist devDependencies: '@iconify-json/codicon': - specifier: ^1.2.47 - version: 1.2.47 + specifier: ^1.2.49 + version: 1.2.49 '@iconify-json/ph': specifier: ^1.2.2 version: 1.2.2 '@sveltejs/vite-plugin-svelte': - specifier: ^6.2.4 - version: 6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(terser@5.46.0)) + specifier: ^7.0.0 + version: 7.0.0(svelte@5.55.7(@typescript-eslint/types@8.58.2))(vite@8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@24.11.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0)) '@unocss/extractor-svelte': - specifier: ^66.6.5 - version: 66.6.5 + specifier: ^66.6.6 + version: 66.6.6 svelte: - specifier: ^5.53.7 - version: 5.53.7 + specifier: ^5.55.7 + version: 5.55.7(@typescript-eslint/types@8.58.2) unocss: - specifier: ^66.6.5 - version: 66.6.5(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(terser@5.46.0)) + specifier: ^66.6.6 + version: 66.6.6(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(vite@8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@24.11.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0)) vite: - specifier: ^7.3.1 - version: 7.3.1(@types/node@24.11.0)(jiti@2.6.1)(terser@5.46.0) + specifier: ^8.0.5 + version: 8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@24.11.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0) examples/file-associations: {} @@ -57,10 +60,10 @@ importers: version: 10.0.1(eslint@10.0.2(jiti@2.6.1)) '@rollup/plugin-terser': specifier: 1.0.0 - version: 1.0.0(rollup@4.59.0) + version: 1.0.0(rollup@4.60.3) '@rollup/plugin-typescript': specifier: 12.3.0 - version: 12.3.0(rollup@4.59.0)(tslib@2.8.1)(typescript@5.9.3) + version: 12.3.0(rollup@4.60.3)(tslib@2.8.1)(typescript@6.0.2) '@types/eslint': specifier: ^9.6.1 version: 9.6.1 @@ -83,23 +86,23 @@ importers: specifier: ^17.4.0 version: 17.4.0 rollup: - specifier: 4.59.0 - version: 4.59.0 + specifier: 4.60.3 + version: 4.60.3 tslib: specifier: ^2.8.1 version: 2.8.1 typescript: - specifier: ^5.9.3 - version: 5.9.3 + specifier: ^6.0.0 + version: 6.0.2 typescript-eslint: - specifier: ^8.56.1 - version: 8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) + specifier: ^8.58.2 + version: 8.58.2(eslint@10.0.2(jiti@2.6.1))(typescript@6.0.2) packages/cli: devDependencies: '@napi-rs/cli': - specifier: ^3.5.1 - version: 3.5.1(@emnapi/runtime@1.8.1)(@types/node@24.11.0) + specifier: 3.4.1 + version: 3.4.1(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@24.11.0) '@types/node': specifier: ^24.11.0 version: 24.11.0 @@ -108,7 +111,7 @@ importers: version: 10.1.0 vitest: specifier: ^4.0.18 - version: 4.0.18(@types/node@24.11.0)(jiti@2.6.1)(terser@5.46.0) + version: 4.1.0(@types/node@24.11.0)(vite@8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@24.11.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0)) packages: @@ -119,41 +122,41 @@ packages: resolution: {integrity: sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==} engines: {node: '>=18.0.0'} - '@cloudflare/unenv-preset@2.14.0': - resolution: {integrity: sha512-XKAkWhi1nBdNsSEoNG9nkcbyvfUrSjSf+VYVPfOto3gLTZVc3F4g6RASCMh6IixBKCG2yDgZKQIHGKtjcnLnKg==} + '@cloudflare/unenv-preset@2.15.0': + resolution: {integrity: sha512-EGYmJaGZKWl+X8tXxcnx4v2bOZSjQeNI5dWFeXivgX9+YCT69AkzHHwlNbVpqtEUTbew8eQurpyOpeN8fg00nw==} peerDependencies: unenv: 2.0.0-rc.24 - workerd: ^1.20260218.0 + workerd: 1.20260301.1 || ~1.20260302.1 || ~1.20260303.1 || ~1.20260304.1 || >1.20260305.0 <2.0.0-0 peerDependenciesMeta: workerd: optional: true - '@cloudflare/workerd-darwin-64@1.20260301.1': - resolution: {integrity: sha512-+kJvwociLrvy1JV9BAvoSVsMEIYD982CpFmo/yMEvBwxDIjltYsLTE8DLi0mCkGsQ8Ygidv2fD9wavzXeiY7OQ==} + '@cloudflare/workerd-darwin-64@1.20260317.1': + resolution: {integrity: sha512-8hjh3sPMwY8M/zedq3/sXoA2Q4BedlGufn3KOOleIG+5a4ReQKLlUah140D7J6zlKmYZAFMJ4tWC7hCuI/s79g==} engines: {node: '>=16'} cpu: [x64] os: [darwin] - '@cloudflare/workerd-darwin-arm64@1.20260301.1': - resolution: {integrity: sha512-PPIetY3e67YBr9O4UhILK8nbm5TqUDl14qx4rwFNrRSBOvlzuczzbd4BqgpAtbGVFxKp1PWpjAnBvGU/OI/tLQ==} + '@cloudflare/workerd-darwin-arm64@1.20260317.1': + resolution: {integrity: sha512-M/MnNyvO5HMgoIdr3QHjdCj2T1ki9gt0vIUnxYxBu9ISXS/jgtMl6chUVPJ7zHYBn9MyYr8ByeN6frjYxj0MGg==} engines: {node: '>=16'} cpu: [arm64] os: [darwin] - '@cloudflare/workerd-linux-64@1.20260301.1': - resolution: {integrity: sha512-Gu5vaVTZuYl3cHa+u5CDzSVDBvSkfNyuAHi6Mdfut7TTUdcb3V5CIcR/mXRSyMXzEy9YxEWIfdKMxOMBjupvYQ==} + '@cloudflare/workerd-linux-64@1.20260317.1': + resolution: {integrity: sha512-1ltuEjkRcS3fsVF7CxsKlWiRmzq2ZqMfqDN0qUOgbUwkpXsLVJsXmoblaLf5OP00ELlcgF0QsN0p2xPEua4Uug==} engines: {node: '>=16'} cpu: [x64] os: [linux] - '@cloudflare/workerd-linux-arm64@1.20260301.1': - resolution: {integrity: sha512-igL1pkyCXW6GiGpjdOAvqMi87UW0LMc/+yIQe/CSzuZJm5GzXoAMrwVTkCFnikk6JVGELrM5x0tGYlxa0sk5Iw==} + '@cloudflare/workerd-linux-arm64@1.20260317.1': + resolution: {integrity: sha512-3QrNnPF1xlaNwkHpasvRvAMidOvQs2NhXQmALJrEfpIJ/IDL2la8g499yXp3eqhG3hVMCB07XVY149GTs42Xtw==} engines: {node: '>=16'} cpu: [arm64] os: [linux] - '@cloudflare/workerd-windows-64@1.20260301.1': - resolution: {integrity: sha512-Q0wMJ4kcujXILwQKQFc1jaYamVsNvjuECzvRrTI8OxGFMx2yq9aOsswViE4X1gaS2YQQ5u0JGwuGi5WdT1Lt7A==} + '@cloudflare/workerd-windows-64@1.20260317.1': + resolution: {integrity: sha512-MfZTz+7LfuIpMGTa3RLXHX8Z/pnycZLItn94WRdHr8LPVet+C5/1Nzei399w/jr3+kzT4pDKk26JF/tlI5elpQ==} engines: {node: '>=16'} cpu: [x64] os: [win32] @@ -385,8 +388,8 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} - '@iconify-json/codicon@1.2.47': - resolution: {integrity: sha512-9z6Of5d3w8aJAv07IT2un0SgAmFZfXSCtG4LDqsf8b/yL4vBgQ6jaCVH6Y0mDegNZzLkJ+7ieIDfbZZlIXH7CA==} + '@iconify-json/codicon@1.2.49': + resolution: {integrity: sha512-Ljl9BWw7e8xYm0l5Npnj5/aN/lEFEQDfhl7UHspPYIlZgw3ZvPOvo0gAr78aorb6X1+LZJqI0BNFAgO5r2sjIQ==} '@iconify-json/ph@1.2.2': resolution: {integrity: sha512-PgkEZNtqa8hBGjHXQa4pMwZa93hmfu8FUSjs/nv4oUU6yLsgv+gh9nu28Kqi8Fz9CCVu4hj1MZs9/60J57IzFw==} @@ -550,134 +553,134 @@ packages: cpu: [x64] os: [win32] - '@inquirer/ansi@2.0.3': - resolution: {integrity: sha512-g44zhR3NIKVs0zUesa4iMzExmZpLUdTLRMCStqX3GE5NT6VkPcxQGJ+uC8tDgBUC/vB1rUhUd55cOf++4NZcmw==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + '@inquirer/ansi@1.0.2': + resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} + engines: {node: '>=18'} - '@inquirer/checkbox@5.1.0': - resolution: {integrity: sha512-/HjF1LN0a1h4/OFsbGKHNDtWICFU/dqXCdym719HFTyJo9IG7Otr+ziGWc9S0iQuohRZllh+WprSgd5UW5Fw0g==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + '@inquirer/checkbox@4.3.2': + resolution: {integrity: sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==} + engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/confirm@6.0.8': - resolution: {integrity: sha512-Di6dgmiZ9xCSUxWUReWTqDtbhXCuG2MQm2xmgSAIruzQzBqNf49b8E07/vbCYY506kDe8BiwJbegXweG8M1klw==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + '@inquirer/confirm@5.1.21': + resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} + engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/core@11.1.5': - resolution: {integrity: sha512-QQPAX+lka8GyLcZ7u7Nb1h6q72iZ/oy0blilC3IB2nSt1Qqxp7akt94Jqhi/DzARuN3Eo9QwJRvtl4tmVe4T5A==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + '@inquirer/core@10.3.2': + resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} + engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/editor@5.0.8': - resolution: {integrity: sha512-sLcpbb9B3XqUEGrj1N66KwhDhEckzZ4nI/W6SvLXyBX8Wic3LDLENlWRvkOGpCPoserabe+MxQkpiMoI8irvyA==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + '@inquirer/editor@4.2.23': + resolution: {integrity: sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==} + engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/expand@5.0.8': - resolution: {integrity: sha512-QieW3F1prNw3j+hxO7/NKkG1pk3oz7pOB6+5Upwu3OIwADfPX0oZVppsqlL+Vl/uBHHDSOBY0BirLctLnXwGGg==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + '@inquirer/expand@4.0.23': + resolution: {integrity: sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==} + engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/external-editor@2.0.3': - resolution: {integrity: sha512-LgyI7Agbda74/cL5MvA88iDpvdXI2KuMBCGRkbCl2Dg1vzHeOgs+s0SDcXV7b+WZJrv2+ERpWSM65Fpi9VfY3w==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + '@inquirer/external-editor@1.0.3': + resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} + engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/figures@2.0.3': - resolution: {integrity: sha512-y09iGt3JKoOCBQ3w4YrSJdokcD8ciSlMIWsD+auPu+OZpfxLuyz+gICAQ6GCBOmJJt4KEQGHuZSVff2jiNOy7g==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + '@inquirer/figures@1.0.15': + resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} + engines: {node: '>=18'} - '@inquirer/input@5.0.8': - resolution: {integrity: sha512-p0IJslw0AmedLEkOU+yrEX3Aj2RTpQq7ZOf8nc1DIhjzaxRWrrgeuE5Kyh39fVRgtcACaMXx/9WNo8+GjgBOfw==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + '@inquirer/input@4.3.1': + resolution: {integrity: sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==} + engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/number@4.0.8': - resolution: {integrity: sha512-uGLiQah9A0F9UIvJBX52m0CnqtLaym0WpT9V4YZrjZ+YRDKZdwwoEPz06N6w8ChE2lrnsdyhY9sL+Y690Kh9gQ==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + '@inquirer/number@3.0.23': + resolution: {integrity: sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==} + engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/password@5.0.8': - resolution: {integrity: sha512-zt1sF4lYLdvPqvmvHdmjOzuUUjuCQ897pdUCO8RbXMUDKXJTTyOQgtn23le+jwcb+MpHl3VAFvzIdxRAf6aPlA==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + '@inquirer/password@4.0.23': + resolution: {integrity: sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==} + engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/prompts@8.3.0': - resolution: {integrity: sha512-JAj66kjdH/F1+B7LCigjARbwstt3SNUOSzMdjpsvwJmzunK88gJeXmcm95L9nw1KynvFVuY4SzXh/3Y0lvtgSg==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + '@inquirer/prompts@7.10.1': + resolution: {integrity: sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==} + engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/rawlist@5.2.4': - resolution: {integrity: sha512-fTuJ5Cq9W286isLxwj6GGyfTjx1Zdk4qppVEPexFuA6yioCCXS4V1zfKroQqw7QdbDPN73xs2DiIAlo55+kBqg==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + '@inquirer/rawlist@4.1.11': + resolution: {integrity: sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==} + engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/search@4.1.4': - resolution: {integrity: sha512-9yPTxq7LPmYjrGn3DRuaPuPbmC6u3fiWcsE9ggfLcdgO/ICHYgxq7mEy1yJ39brVvgXhtOtvDVjDh9slJxE4LQ==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + '@inquirer/search@3.2.2': + resolution: {integrity: sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==} + engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/select@5.1.0': - resolution: {integrity: sha512-OyYbKnchS1u+zRe14LpYrN8S0wH1vD0p2yKISvSsJdH2TpI87fh4eZdWnpdbrGauCRWDph3NwxRmM4Pcm/hx1Q==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + '@inquirer/select@4.4.2': + resolution: {integrity: sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==} + engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/type@4.0.3': - resolution: {integrity: sha512-cKZN7qcXOpj1h+1eTTcGDVLaBIHNMT1Rz9JqJP5MnEJ0JhgVWllx7H/tahUp5YEK1qaByH2Itb8wLG/iScD5kw==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + '@inquirer/type@3.0.10': + resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} + engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: @@ -706,12 +709,12 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - '@napi-rs/cli@3.5.1': - resolution: {integrity: sha512-XBfLQRDcB3qhu6bazdMJsecWW55kR85l5/k0af9BIBELXQSsCFU0fzug7PX8eQp6vVdm7W/U3z6uP5WmITB2Gw==} + '@napi-rs/cli@3.4.1': + resolution: {integrity: sha512-ayhm+NfrP5Hmh7vy5pfyYm/ktYtLh2PrgdLuqHTAubO7RoO2JkUE4F991AtgYxNewwXI8+guZLxU8itV7QnDrQ==} engines: {node: '>= 16'} hasBin: true peerDependencies: - '@emnapi/runtime': ^1.7.1 + '@emnapi/runtime': ^1.5.0 peerDependenciesMeta: '@emnapi/runtime': optional: true @@ -968,8 +971,11 @@ packages: resolution: {integrity: sha512-7cmzIu+Vbupriudo7UudoMRH2OA3cTw67vva8MxeoAe5S7vPFI7z0vp0pMXiA25S8IUJefImQ90FeJjl8fjEaQ==} engines: {node: '>= 10'} - '@napi-rs/wasm-runtime@1.1.1': - resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} + '@napi-rs/wasm-runtime@1.1.2': + resolution: {integrity: sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 '@napi-rs/wasm-tools-android-arm-eabi@1.0.1': resolution: {integrity: sha512-lr07E/l571Gft5v4aA1dI8koJEmF1F0UigBbsqg9OWNzg80H3lDPO+auv85y3T/NHE3GirDk7x/D3sLO57vayw==} @@ -1250,6 +1256,9 @@ packages: '@oxc-project/types@0.115.0': resolution: {integrity: sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==} + '@oxc-project/types@0.122.0': + resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==} + '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} @@ -1265,6 +1274,104 @@ packages: '@quansync/fs@1.0.0': resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} + '@rolldown/binding-android-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-rc.12': + resolution: {integrity: sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-rc.12': + resolution: {integrity: sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': + resolution: {integrity: sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': + resolution: {integrity: sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': + resolution: {integrity: sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.12': + resolution: {integrity: sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': + resolution: {integrity: sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': + resolution: {integrity: sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-rc.12': + resolution: {integrity: sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==} + '@rollup/plugin-terser@1.0.0': resolution: {integrity: sha512-FnCxhTBx6bMOYQrar6C8h3scPt8/JwIzw3+AJ2K++6guogH5fYaIFia+zZuhqv0eo1RN7W1Pz630SyvLbDjhtQ==} engines: {node: '>=20.0.0'} @@ -1296,141 +1403,141 @@ packages: rollup: optional: true - '@rollup/rollup-android-arm-eabi@4.59.0': - resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} + '@rollup/rollup-android-arm-eabi@4.60.3': + resolution: {integrity: sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.59.0': - resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} + '@rollup/rollup-android-arm64@4.60.3': + resolution: {integrity: sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.59.0': - resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} + '@rollup/rollup-darwin-arm64@4.60.3': + resolution: {integrity: sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.59.0': - resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} + '@rollup/rollup-darwin-x64@4.60.3': + resolution: {integrity: sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.59.0': - resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} + '@rollup/rollup-freebsd-arm64@4.60.3': + resolution: {integrity: sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.59.0': - resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} + '@rollup/rollup-freebsd-x64@4.60.3': + resolution: {integrity: sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.59.0': - resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} + '@rollup/rollup-linux-arm-gnueabihf@4.60.3': + resolution: {integrity: sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==} cpu: [arm] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm-musleabihf@4.59.0': - resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} + '@rollup/rollup-linux-arm-musleabihf@4.60.3': + resolution: {integrity: sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==} cpu: [arm] os: [linux] libc: [musl] - '@rollup/rollup-linux-arm64-gnu@4.59.0': - resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} + '@rollup/rollup-linux-arm64-gnu@4.60.3': + resolution: {integrity: sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==} cpu: [arm64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm64-musl@4.59.0': - resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} + '@rollup/rollup-linux-arm64-musl@4.60.3': + resolution: {integrity: sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==} cpu: [arm64] os: [linux] libc: [musl] - '@rollup/rollup-linux-loong64-gnu@4.59.0': - resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} + '@rollup/rollup-linux-loong64-gnu@4.60.3': + resolution: {integrity: sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==} cpu: [loong64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-loong64-musl@4.59.0': - resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} + '@rollup/rollup-linux-loong64-musl@4.60.3': + resolution: {integrity: sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==} cpu: [loong64] os: [linux] libc: [musl] - '@rollup/rollup-linux-ppc64-gnu@4.59.0': - resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} + '@rollup/rollup-linux-ppc64-gnu@4.60.3': + resolution: {integrity: sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==} cpu: [ppc64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-ppc64-musl@4.59.0': - resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} + '@rollup/rollup-linux-ppc64-musl@4.60.3': + resolution: {integrity: sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==} cpu: [ppc64] os: [linux] libc: [musl] - '@rollup/rollup-linux-riscv64-gnu@4.59.0': - resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} + '@rollup/rollup-linux-riscv64-gnu@4.60.3': + resolution: {integrity: sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==} cpu: [riscv64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-riscv64-musl@4.59.0': - resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} + '@rollup/rollup-linux-riscv64-musl@4.60.3': + resolution: {integrity: sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==} cpu: [riscv64] os: [linux] libc: [musl] - '@rollup/rollup-linux-s390x-gnu@4.59.0': - resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} + '@rollup/rollup-linux-s390x-gnu@4.60.3': + resolution: {integrity: sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==} cpu: [s390x] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-gnu@4.59.0': - resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} + '@rollup/rollup-linux-x64-gnu@4.60.3': + resolution: {integrity: sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==} cpu: [x64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-musl@4.59.0': - resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} + '@rollup/rollup-linux-x64-musl@4.60.3': + resolution: {integrity: sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==} cpu: [x64] os: [linux] libc: [musl] - '@rollup/rollup-openbsd-x64@4.59.0': - resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} + '@rollup/rollup-openbsd-x64@4.60.3': + resolution: {integrity: sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==} cpu: [x64] os: [openbsd] - '@rollup/rollup-openharmony-arm64@4.59.0': - resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} + '@rollup/rollup-openharmony-arm64@4.60.3': + resolution: {integrity: sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.59.0': - resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} + '@rollup/rollup-win32-arm64-msvc@4.60.3': + resolution: {integrity: sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.59.0': - resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} + '@rollup/rollup-win32-ia32-msvc@4.60.3': + resolution: {integrity: sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.59.0': - resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} + '@rollup/rollup-win32-x64-gnu@4.60.3': + resolution: {integrity: sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.59.0': - resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} + '@rollup/rollup-win32-x64-msvc@4.60.3': + resolution: {integrity: sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==} cpu: [x64] os: [win32] @@ -1449,20 +1556,12 @@ packages: peerDependencies: acorn: ^8.9.0 - '@sveltejs/vite-plugin-svelte-inspector@5.0.2': - resolution: {integrity: sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig==} + '@sveltejs/vite-plugin-svelte@7.0.0': + resolution: {integrity: sha512-ILXmxC7HAsnkK2eslgPetrqqW1BKSL7LktsFgqzNj83MaivMGZzluWq32m25j2mDOjmSKX7GGWahePhuEs7P/g==} engines: {node: ^20.19 || ^22.12 || >=24} peerDependencies: - '@sveltejs/vite-plugin-svelte': ^6.0.0-next.0 - svelte: ^5.0.0 - vite: ^6.3.0 || ^7.0.0 - - '@sveltejs/vite-plugin-svelte@6.2.4': - resolution: {integrity: sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==} - engines: {node: ^20.19 || ^22.12 || >=24} - peerDependencies: - svelte: ^5.0.0 - vite: ^6.3.0 || ^7.0.0 + svelte: ^5.46.4 + vite: ^8.0.0-beta.7 || ^8.0.0 '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} @@ -1482,6 +1581,9 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -1491,168 +1593,165 @@ packages: '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} - '@typescript-eslint/eslint-plugin@8.56.1': - resolution: {integrity: sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==} + '@typescript-eslint/eslint-plugin@8.58.2': + resolution: {integrity: sha512-aC2qc5thQahutKjP+cl8cgN9DWe3ZUqVko30CMSZHnFEHyhOYoZSzkGtAI2mcwZ38xeImDucI4dnqsHiOYuuCw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.56.1 + '@typescript-eslint/parser': ^8.58.2 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.56.1': - resolution: {integrity: sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==} + '@typescript-eslint/parser@8.58.2': + resolution: {integrity: sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.56.1': - resolution: {integrity: sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==} + '@typescript-eslint/project-service@8.58.2': + resolution: {integrity: sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@8.56.1': - resolution: {integrity: sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==} + '@typescript-eslint/scope-manager@8.58.2': + resolution: {integrity: sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.56.1': - resolution: {integrity: sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==} + '@typescript-eslint/tsconfig-utils@8.58.2': + resolution: {integrity: sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.56.1': - resolution: {integrity: sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==} + '@typescript-eslint/type-utils@8.58.2': + resolution: {integrity: sha512-Z7EloNR/B389FvabdGeTo2XMs4W9TjtPiO9DAsmT0yom0bwlPyRjkJ1uCdW1DvrrrYP50AJZ9Xc3sByZA9+dcg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/types@8.56.1': - resolution: {integrity: sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==} + '@typescript-eslint/types@8.58.2': + resolution: {integrity: sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.56.1': - resolution: {integrity: sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==} + '@typescript-eslint/typescript-estree@8.58.2': + resolution: {integrity: sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.56.1': - resolution: {integrity: sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==} + '@typescript-eslint/utils@8.58.2': + resolution: {integrity: sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@8.56.1': - resolution: {integrity: sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==} + '@typescript-eslint/visitor-keys@8.58.2': + resolution: {integrity: sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@unocss/cli@66.6.5': - resolution: {integrity: sha512-UlETATpAZ+A5gOfj+z+BMXuIUcXCMjvlQteQE0VR2Yf0VIxz4sVO4z0VCXwXsxLTMfQiIMDpKVrGeczcYicvTA==} + '@unocss/cli@66.6.6': + resolution: {integrity: sha512-78SY8j4hAVelK+vP/adsDGaSjEITasYLFECJLHWxUJSzK+G9UIc5wtL/u4jA+zKvwVkHcDvbkcO5K6wwwpAixg==} engines: {node: '>=14'} hasBin: true - '@unocss/config@66.6.4': - resolution: {integrity: sha512-iwHl5FG81cOAMalqigjw21Z2tMa0xjN0doQxnGOLx8KP+BllruXSjBj8CRk3m6Ny9fDxfpFY0ruYbIBA5AGwDQ==} + '@unocss/config@66.6.6': + resolution: {integrity: sha512-menlnkqAFX/4wR2aandY8hSqrt01JE+rOzvtQxWaBt8kf1du62b0sS72FE5Z40n6HlEsEbF91N9FCfhnzG6i6g==} engines: {node: '>=14'} - '@unocss/core@66.6.4': - resolution: {integrity: sha512-Fii3lhVJVFrKUz6hMGAkq3sXBfNnXB2G8bldNHuBHJpDAoP1F0oO/SU/oSqSjCYvtcD5RtOn8qwzcHuuN3B/mg==} - - '@unocss/core@66.6.5': - resolution: {integrity: sha512-hzjo+0EF+pNbf+tb0OjRNZRF9BJoKECcZZgtufxRPpWJdlv+aYmNkH1p9fldlHHzYcn3ZqVnnHnmk7HwaolJbg==} + '@unocss/core@66.6.6': + resolution: {integrity: sha512-Sbbx0ZQqmV8K2lg8E+z9MJzWb1MgRtJnvqzxDIrNuBjXasKhbcFt5wEMBtEZJOr63Z4ck0xThhZK53HmYT2jmg==} - '@unocss/extractor-arbitrary-variants@66.6.5': - resolution: {integrity: sha512-wqzRtbyy3I595WCwwb8VBmznJTHWcTdylzVT+WBgacJDjRlT1sXaq2fRlOsHvtTRj1qG70t3PwKc6XgU0hutNg==} + '@unocss/extractor-arbitrary-variants@66.6.6': + resolution: {integrity: sha512-uMzekF2miZRUwSZGvy3yYQiBAcSAs9LiXK8e3NjldxEw8xcRDWgTErxgStRoBeAD6UyzDcg/Cvwtf2guMbtR+g==} - '@unocss/extractor-svelte@66.6.5': - resolution: {integrity: sha512-Zg58V3L5jxSPzT2BEZY87+WH0gVMI4VKZuwNwFS3ht83RsCD32eGjJxrk6w5CuIYDV6MeGSkosyPIwLY+OFD3w==} + '@unocss/extractor-svelte@66.6.6': + resolution: {integrity: sha512-5+Et3jiSFlMqxkoyVLsoT2/Rd8x/Jd65i5KzIyXMtQccDmqN2wSXuyvB2h5sLauHn4bBe/qOWO3PfGjbXBGWOA==} - '@unocss/inspector@66.6.5': - resolution: {integrity: sha512-rrXPlSeRfYajEL65FL1Ok9Hfhjy9zvuZZwqXh9P0qCJlou2r2IqDFO/Gf9j5yO89tnKIfJ8ff6jEyqUmzbKSMQ==} + '@unocss/inspector@66.6.6': + resolution: {integrity: sha512-CpXIsqHwxCXJtUjUz6S29diHCIA+EJ1u5WML/6m2YPI4ObgWAVKrExy09inSg2icS52lFkWWdWQSeqc9kl5W6Q==} - '@unocss/preset-attributify@66.6.5': - resolution: {integrity: sha512-fx+pKMZ0WgT+dfinVaLkNXlx6oZFwtMbZj5O/1SQia0UcfhnyS+G35HYpbgoc9GEAl3DclxxotzZjveZm++9fA==} + '@unocss/preset-attributify@66.6.6': + resolution: {integrity: sha512-3H12UI1rBt60PQy+S4IEeFYWu1/WQFuc2yhJ5mu/RCvX5/qwlIGanBpuh+xzTPXU1fWBlZN68yyO9uWOQgTqZQ==} - '@unocss/preset-icons@66.6.5': - resolution: {integrity: sha512-03ppAcTWD77w1WZhORT8c9beTHBtWu3cx+c4qfShOfY6LQmZgx5i7DhCij5Wcj/U1zYA4Vrh13CDEmpsdZO3Cw==} + '@unocss/preset-icons@66.6.6': + resolution: {integrity: sha512-HfIEEqf3jyKexOB2Sux556n0NkPoUftb2H4+Cf7prJvKHopMkZ/OUkXjwvUlxt1e5UpAEaIa0A2Ir7+ApxXoGA==} - '@unocss/preset-mini@66.6.5': - resolution: {integrity: sha512-Ber3k2jlE8JP0y507hw/lvdDvcxfY0t4zaGA7hVZdEqlH6Eus/TqIVZ9tdMH4u0VDWYeAs98YV+auUJmMqGXpg==} + '@unocss/preset-mini@66.6.6': + resolution: {integrity: sha512-k+/95PKMPOK57cJcSmz34VkIFem8BlujRRx6/L0Yusw7vLJMh98k0rPhC5s+NomZ/d9ZPgbNylskLhItJlak3w==} - '@unocss/preset-tagify@66.6.5': - resolution: {integrity: sha512-YYk/eg1OWX4Nx7rK1YZLMHXXntzNRDHp6BIInJteQmlXw0sFgrtdMKj7fnxrORsBDHwxWMp4sWEucPvfCtTlVQ==} + '@unocss/preset-tagify@66.6.6': + resolution: {integrity: sha512-KgBXYPYS0g4TVC3NLiIB78YIqUlvDLanz1EHIDo34rOTUfMgY8Uf5VuDJAzMu4Sc0LiwwBJbk6nIG9/Zm7ufWg==} - '@unocss/preset-typography@66.6.5': - resolution: {integrity: sha512-Cb63tdC0P2rgj/4t4DrSCl6RHebNpjUp9FQArg0KCnFnW75nWtKlsKpHuEXpi7KwrgOIx+rjlkwC1bDcsdNLHw==} + '@unocss/preset-typography@66.6.6': + resolution: {integrity: sha512-SM1km5nqt15z4sTabfOobSC633I5Ol5nnme6JFTra4wiyCUNs+Cg31nJ6jnopWDUT4SEAXqfUH7jKSSoCnI6ZA==} - '@unocss/preset-uno@66.6.5': - resolution: {integrity: sha512-feZfGyzt3dH4h6yP2kjsx5MuoI1gU7vY/VL5O+ObosaB7HzzOFCsu2WzlvWn/FTRBi+scvdq436hsfflVyHYfQ==} + '@unocss/preset-uno@66.6.6': + resolution: {integrity: sha512-40PcBDtlhW7QP7e/WOxC684IhN5T1dXvj1dgx9ZzK+8lEDGjcX7bN2noW4aSenzSrHymeSsMrL/0ltL4ED/5Zw==} - '@unocss/preset-web-fonts@66.6.5': - resolution: {integrity: sha512-u5jEHYTMeseykqinXd2VY2n7q9yFQlZotREpfSAft8ENNJdV7Yg/6It3lL68zT/k1AV/A8gk94KEuDh0fnoSxQ==} + '@unocss/preset-web-fonts@66.6.6': + resolution: {integrity: sha512-5ikwgrJB8VPzKd0bqgGNgYUGix90KFnVtKJPjWTP5qsv3+ZtZnea1rRbAFl8i2t52hg35msNBsQo+40IC3xB6A==} - '@unocss/preset-wind3@66.6.5': - resolution: {integrity: sha512-0ccQoJmHq4tTnn5C0UKhP598B/gG65AjqlfgfRpwt059yAWYqizGy6MRUGdLklyEK4H06E6qbMBqIjla2rOexQ==} + '@unocss/preset-wind3@66.6.6': + resolution: {integrity: sha512-rk6gPPIQ7z2DVucOqp7XZ4vGpKAuzBV1vtUDvDh5WscxzO/QlqaeTfTALk5YgGpmLaF4+ns6FrTgLjV+wHgHuQ==} - '@unocss/preset-wind4@66.6.5': - resolution: {integrity: sha512-JT57CU60PY3/PHBvxY+UG53I9K+awin/TodZTn4lqQNnF2v6fjkeBKiys9cxeoP4wbHuQWorrW4GqRLNDWIMcw==} + '@unocss/preset-wind4@66.6.6': + resolution: {integrity: sha512-caTDM9rZSlp4tyPWWAnwMvQr2PXq53LsEYwd3N8zj0ou2hcsqptJvF+mFvyhvGF66x26wWJr/FwuUEhh7qycaw==} - '@unocss/preset-wind@66.6.5': - resolution: {integrity: sha512-GLu7LzVF0LHqdZoHFZ8dbsCv8TD5ZH/r10CQbrL5qwmp4a/uyfDEmsre4Nsqim7JktRyXn3HK2XQmTB8AmXpgQ==} + '@unocss/preset-wind@66.6.6': + resolution: {integrity: sha512-TMy3lZ35FP/4QqDHOLWZmV+RoOGWUDqnDEOTjOKI1CQARGta0ppUmq+IZMuI1ZJLuOa4OZ9V6SfnwMXwRLgXmw==} - '@unocss/rule-utils@66.6.5': - resolution: {integrity: sha512-eDGXoMebb5aeEAFa2y4gnGLC+CHZPx93JYCt6uvEyf9xOoetwDcZaYC8brWdjaSKn+WVgsfxiZreC7F0rJywOQ==} + '@unocss/rule-utils@66.6.6': + resolution: {integrity: sha512-krWtQKGshOaqQMuxeGq1NOA8NL35VdpYlmQEWOe39BY6TACT51bgQFu40MRfsAIMZZtoGS2YYTrnHojgR92omw==} engines: {node: '>=14'} - '@unocss/transformer-attributify-jsx@66.6.5': - resolution: {integrity: sha512-/dVaRR7V/2Alskb2rUPmP/lhyb/YCxYyYNxp30kxxW0ew6mZWXQRzsxOJJVmGp23Uw7HxUW63t8zXzUdoI0b+g==} + '@unocss/transformer-attributify-jsx@66.6.6': + resolution: {integrity: sha512-NnDchmN2EeFLy4lfVqDgNe9j1+w2RLL2L9zKECXs5g6rDVfeeEK6FNgxSq3XnPcKltjNCy1pF4MaDOROG7r8yA==} - '@unocss/transformer-compile-class@66.6.5': - resolution: {integrity: sha512-U/ukk5lyZOFNyz9hVzZBkxciayjgimyfPuQBa5PHSC4W3nDmnFd1zgXzUVaM6KduPmiTExzpJSDgELb2OTbpqg==} + '@unocss/transformer-compile-class@66.6.6': + resolution: {integrity: sha512-KKssJxU8fZ9x84yznIirbtta2sB0LN/3lm0bp+Wl1298HITaNiVeG2n26iStQ3N7r240xRN2RarxncSVCMFwWw==} - '@unocss/transformer-directives@66.6.5': - resolution: {integrity: sha512-QgofDdDedNK6dQ246+RXhM6gTzRz7NuetQQ8UnNgArm4PBHngVrrkjCzG1ByDTtEtoE8WR70UMR4Vf5dXTcHPw==} + '@unocss/transformer-directives@66.6.6': + resolution: {integrity: sha512-CReFTcBfMtKkRvzIqxL20VptWt5C1Om27dwoKzyVFBXv0jzViWysbu0y0AQg3bsgD4cFqndFyAGyeL84j0nbKg==} - '@unocss/transformer-variant-group@66.6.5': - resolution: {integrity: sha512-k6vQgn/P7ObHBRYw6o1+xwdQIfwc6b9O5TFFe87UmBB6hJ2zaHWRVuPB6oky7F9Gz8bPfXC3WJuv7UyIwRmBQQ==} + '@unocss/transformer-variant-group@66.6.6': + resolution: {integrity: sha512-j4L/0Tw6AdMVB2dDnuBlDbevyL1/0CAk88a77VF/VjgEIBwB9VXsCCUsxz+2Dohcl7N2GMm7+kpaWA6qt2PSaA==} - '@unocss/vite@66.6.5': - resolution: {integrity: sha512-J/QZa6h94ordZlZytIKQkuYa+G2GiWiS3y9O1uoHAAN2tzFSkgCXNUif7lHu1h4eCrgC0AOHJSYWg1LIASNDkg==} + '@unocss/vite@66.6.6': + resolution: {integrity: sha512-DgG7KcUUMtoDhPOlFf2l4dR+66xZ23SdZvTYpikk5nZfLCzZd62vedutD7x0bTR6VpK2YRq39B+F+Z6TktNY/w==} peerDependencies: vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 || ^8.0.0-0 - '@vitest/expect@4.0.18': - resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==} + '@vitest/expect@4.1.0': + resolution: {integrity: sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==} - '@vitest/mocker@4.0.18': - resolution: {integrity: sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==} + '@vitest/mocker@4.1.0': + resolution: {integrity: sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==} peerDependencies: msw: ^2.4.9 - vite: ^6.0.0 || ^7.0.0-0 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0-0 peerDependenciesMeta: msw: optional: true vite: optional: true - '@vitest/pretty-format@4.0.18': - resolution: {integrity: sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==} + '@vitest/pretty-format@4.1.0': + resolution: {integrity: sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==} - '@vitest/runner@4.0.18': - resolution: {integrity: sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==} + '@vitest/runner@4.1.0': + resolution: {integrity: sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==} - '@vitest/snapshot@4.0.18': - resolution: {integrity: sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==} + '@vitest/snapshot@4.1.0': + resolution: {integrity: sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==} - '@vitest/spy@4.0.18': - resolution: {integrity: sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==} + '@vitest/spy@4.1.0': + resolution: {integrity: sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==} - '@vitest/utils@4.0.18': - resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==} + '@vitest/utils@4.1.0': + resolution: {integrity: sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==} acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} @@ -1667,6 +1766,14 @@ packages: ajv@6.14.0: resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -1692,8 +1799,8 @@ packages: blake3-wasm@2.1.5: resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} - brace-expansion@5.0.4: - resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} engines: {node: 18 || 20 || >=22} braces@3.0.3: @@ -1731,6 +1838,13 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} @@ -1744,6 +1858,9 @@ packages: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie@1.1.1: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} @@ -1777,8 +1894,8 @@ packages: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} - defu@6.1.4: - resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + defu@6.1.6: + resolution: {integrity: sha512-f8mefEW4WIVg4LckePx3mALjQSPQgFlg9U8yaPdlsbdYcHQyj9n2zL2LJEA52smeYxOvmd/nB7TpMtHGMTHcug==} destr@2.0.5: resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} @@ -1787,8 +1904,8 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - devalue@5.6.3: - resolution: {integrity: sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg==} + devalue@5.8.1: + resolution: {integrity: sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==} duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} @@ -1801,11 +1918,14 @@ packages: node-addon-api: optional: true + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + error-stack-parser-es@1.0.5: resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} - es-module-lexer@1.7.0: - resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-module-lexer@2.1.0: + resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} es-toolkit@1.45.1: resolution: {integrity: sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==} @@ -1862,8 +1982,13 @@ packages: resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} engines: {node: '>=0.10'} - esrap@2.2.3: - resolution: {integrity: sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ==} + esrap@2.2.8: + resolution: {integrity: sha512-MPweq2EvEGj8jwOI7Hgycw/QIHzqA1EbAM8lG7p+FBfZbZq/hQ6h3AMsqnu/djzisH1KVWNzbb7LSgIVtMlPSg==} + peerDependencies: + '@typescript-eslint/types': ^8.2.0 + peerDependenciesMeta: + '@typescript-eslint/types': + optional: true esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} @@ -1903,15 +2028,6 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - fast-string-truncated-width@3.0.3: - resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} - - fast-string-width@3.0.2: - resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} - - fast-wrap-ansi@0.2.0: - resolution: {integrity: sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==} - fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -1940,8 +2056,8 @@ packages: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} - flatted@3.3.4: - resolution: {integrity: sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==} + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} @@ -1995,6 +2111,10 @@ packages: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -2040,6 +2160,80 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + locate-character@3.0.0: resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} @@ -2064,13 +2258,13 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} - miniflare@4.20260301.1: - resolution: {integrity: sha512-fqkHx0QMKswRH9uqQQQOU/RoaS3Wjckxy3CUX3YGJr0ZIMu7ObvI+NovdYi6RIsSPthNtq+3TPmRNxjeRiasog==} + miniflare@4.20260317.0: + resolution: {integrity: sha512-xuwk5Kjv+shi5iUBAdCrRl9IaWSGnTU8WuTQzsUS2GlSDIMCJuu8DiF/d9ExjMXYiQG5ml+k9SVKnMj8cRkq0w==} engines: {node: '>=18.0.0'} hasBin: true - minimatch@10.2.4: - resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} mlly@1.8.0: @@ -2083,9 +2277,9 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - mute-stream@3.0.0: - resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==} - engines: {node: ^20.17.0 || >=22.9.0} + mute-stream@2.0.0: + resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} + engines: {node: ^18.17.0 || >=20.5.0} nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} @@ -2151,19 +2345,19 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} engines: {node: '>=8.6'} - picomatch@4.0.3: - resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} - postcss@8.5.8: - resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + postcss@8.5.12: + resolution: {integrity: sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==} engines: {node: ^10 || ^12 || >=14} prelude-ls@1.2.1: @@ -2202,8 +2396,13 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rollup@4.59.0: - resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} + rolldown@1.0.0-rc.12: + resolution: {integrity: sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + rollup@4.60.3: + resolution: {integrity: sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -2221,8 +2420,8 @@ packages: engines: {node: '>=10'} hasBin: true - serialize-javascript@7.0.4: - resolution: {integrity: sha512-DuGdB+Po43Q5Jxwpzt1lhyFSYKryqoNjQSA9M92tyw0lyHIOur+XCalOUe0KTJpyqzT8+fQ5A0Jf7vCx/NKmIg==} + serialize-javascript@7.0.5: + resolution: {integrity: sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw==} engines: {node: '>=20.0.0'} sharp@0.34.5: @@ -2266,8 +2465,16 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - std-env@3.10.0: - resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} supports-color@10.2.2: resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} @@ -2277,8 +2484,8 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - svelte@5.53.7: - resolution: {integrity: sha512-uxck1KI7JWtlfP3H6HOWi/94soAl23jsGJkBzN2BAWcQng0+lTrRNhxActFqORgnO9BHVd1hKJhG+ljRuIUWfQ==} + svelte@5.55.7: + resolution: {integrity: sha512-ymI5ykLPwIHW839E053FQbI1G+jnRFJEw3Kv5Y4njixVWywQBx+NUFpkkKyk5LIb36Fg9DVXSYpqiGekLD0hyw==} engines: {node: '>=18'} terser@5.46.0: @@ -2289,16 +2496,16 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - tinyexec@1.0.2: - resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} + tinyexec@1.2.4: + resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} engines: {node: '>=18'} - tinyglobby@0.2.15: - resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} - tinyrainbow@3.0.3: - resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} to-regex-range@5.0.1: @@ -2309,8 +2516,8 @@ packages: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} - ts-api-utils@2.4.0: - resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} engines: {node: '>=18.12'} peerDependencies: typescript: '>=4.8.4' @@ -2328,15 +2535,15 @@ packages: type-level-regexp@0.1.17: resolution: {integrity: sha512-wTk4DH3cxwk196uGLK/E9pE45aLfeKJacKmcEgEOA/q5dnPGNxXt0cfYdFxb57L+sEpf1oJH4Dnx/pnRcku9jg==} - typescript-eslint@8.56.1: - resolution: {integrity: sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==} + typescript-eslint@8.58.2: + resolution: {integrity: sha512-V8iSng9mRbdZjl54VJ9NKr6ZB+dW0J3TzRXRGcSbLIej9jV86ZRtlYeTKDR/QLxXykocJ5icNzbsl2+5TzIvcQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + typescript@6.0.2: + resolution: {integrity: sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==} engines: {node: '>=14.17'} hasBin: true @@ -2352,8 +2559,8 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} - undici@7.18.2: - resolution: {integrity: sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==} + undici@7.24.4: + resolution: {integrity: sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==} engines: {node: '>=20.18.1'} unenv@2.0.0-rc.24: @@ -2362,13 +2569,13 @@ packages: universal-user-agent@7.0.3: resolution: {integrity: sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==} - unocss@66.6.5: - resolution: {integrity: sha512-WlpPlV7yAzEPREcwaKeacP+1jOm6ImhyKJRkK18tIW2b2BRZZDKln7X8P+NzJtAr0kziNY/ttUKZNZRnSmzP1A==} + unocss@66.6.6: + resolution: {integrity: sha512-PRKK945e2oZKHV664MA5Z9CDHbvY/V79IvTOUWKZ514jpl3UsJU3sS+skgxmKJSmwrWvXE5OVcmPthJrD/7vxg==} engines: {node: '>=14'} peerDependencies: - '@unocss/astro': 66.6.5 - '@unocss/postcss': 66.6.5 - '@unocss/webpack': 66.6.5 + '@unocss/astro': 66.6.6 + '@unocss/postcss': 66.6.6 + '@unocss/webpack': 66.6.6 peerDependenciesMeta: '@unocss/astro': optional: true @@ -2388,15 +2595,16 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - vite@7.3.1: - resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} + vite@8.0.5: + resolution: {integrity: sha512-nmu43Qvq9UopTRfMx2jOYW5l16pb3iDC1JH6yMuPkpVbzK0k+L7dfsEDH4jRgYFmsg0sTAqkojoZgzLMlwHsCQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.0 + esbuild: ^0.27.0 || ^0.28.0 jiti: '>=1.21.0' less: ^4.0.0 - lightningcss: ^1.21.0 sass: ^1.70.0 sass-embedded: ^1.70.0 stylus: '>=0.54.8' @@ -2407,12 +2615,14 @@ packages: peerDependenciesMeta: '@types/node': optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true jiti: optional: true less: optional: true - lightningcss: - optional: true sass: optional: true sass-embedded: @@ -2436,20 +2646,21 @@ packages: vite: optional: true - vitest@4.0.18: - resolution: {integrity: sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==} + vitest@4.1.0: + resolution: {integrity: sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.0.18 - '@vitest/browser-preview': 4.0.18 - '@vitest/browser-webdriverio': 4.0.18 - '@vitest/ui': 4.0.18 + '@vitest/browser-playwright': 4.1.0 + '@vitest/browser-preview': 4.1.0 + '@vitest/browser-webdriverio': 4.1.0 + '@vitest/ui': 4.1.0 happy-dom: '*' jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0-0 peerDependenciesMeta: '@edge-runtime/vm': optional: true @@ -2487,23 +2698,27 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} - workerd@1.20260301.1: - resolution: {integrity: sha512-oterQ1IFd3h7PjCfT4znSFOkJCvNQ6YMOyZ40YsnO3nrSpgB4TbJVYWFOnyJAw71/RQuupfVqZZWKvsy8GO3fw==} + workerd@1.20260317.1: + resolution: {integrity: sha512-ZuEq1OdrJBS+NV+L5HMYPCzVn49a2O60slQiiLpG44jqtlOo+S167fWC76kEXteXLLLydeuRrluRel7WdOUa4g==} engines: {node: '>=16'} hasBin: true - wrangler@4.70.0: - resolution: {integrity: sha512-PNDZ9o4e+B5x+1bUbz62Hmwz6G9lw+I9pnYe/AguLddJFjfIyt2cmFOUOb3eOZSoXsrhcEPUg2YidYIbVwUkfw==} + wrangler@4.75.0: + resolution: {integrity: sha512-Efk1tcnm4eduBYpH1sSjMYydXMnIFPns/qABI3+fsbDrUk5GksNYX8nYGVP4sFygvGPO7kJc36YJKB5ooA7JAg==} engines: {node: '>=20.0.0'} hasBin: true peerDependencies: - '@cloudflare/workers-types': ^4.20260226.1 + '@cloudflare/workers-types': ^4.20260317.1 peerDependenciesMeta: '@cloudflare/workers-types': optional: true - ws@8.18.0: - resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + ws@8.20.1: + resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -2518,6 +2733,10 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + yoctocolors-cjs@2.1.3: + resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} + engines: {node: '>=18'} + youch-core@0.3.3: resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} @@ -2532,29 +2751,29 @@ snapshots: '@antfu/install-pkg@1.1.0': dependencies: package-manager-detector: 1.6.0 - tinyexec: 1.0.2 + tinyexec: 1.2.4 '@cloudflare/kv-asset-handler@0.4.2': {} - '@cloudflare/unenv-preset@2.14.0(unenv@2.0.0-rc.24)(workerd@1.20260301.1)': + '@cloudflare/unenv-preset@2.15.0(unenv@2.0.0-rc.24)(workerd@1.20260317.1)': dependencies: unenv: 2.0.0-rc.24 optionalDependencies: - workerd: 1.20260301.1 + workerd: 1.20260317.1 - '@cloudflare/workerd-darwin-64@1.20260301.1': + '@cloudflare/workerd-darwin-64@1.20260317.1': optional: true - '@cloudflare/workerd-darwin-arm64@1.20260301.1': + '@cloudflare/workerd-darwin-arm64@1.20260317.1': optional: true - '@cloudflare/workerd-linux-64@1.20260301.1': + '@cloudflare/workerd-linux-64@1.20260317.1': optional: true - '@cloudflare/workerd-linux-arm64@1.20260301.1': + '@cloudflare/workerd-linux-arm64@1.20260317.1': optional: true - '@cloudflare/workerd-windows-64@1.20260301.1': + '@cloudflare/workerd-windows-64@1.20260317.1': optional: true '@cspotcode/source-map-support@0.8.1': @@ -2668,7 +2887,7 @@ snapshots: dependencies: '@eslint/object-schema': 3.0.2 debug: 4.4.3 - minimatch: 10.2.4 + minimatch: 10.2.5 transitivePeerDependencies: - supports-color @@ -2702,7 +2921,7 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} - '@iconify-json/codicon@1.2.47': + '@iconify-json/codicon@1.2.49': dependencies: '@iconify/types': 2.0.0 @@ -2814,122 +3033,128 @@ snapshots: '@img/sharp-win32-x64@0.34.5': optional: true - '@inquirer/ansi@2.0.3': {} + '@inquirer/ansi@1.0.2': {} - '@inquirer/checkbox@5.1.0(@types/node@24.11.0)': + '@inquirer/checkbox@4.3.2(@types/node@24.11.0)': dependencies: - '@inquirer/ansi': 2.0.3 - '@inquirer/core': 11.1.5(@types/node@24.11.0) - '@inquirer/figures': 2.0.3 - '@inquirer/type': 4.0.3(@types/node@24.11.0) + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@24.11.0) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@24.11.0) + yoctocolors-cjs: 2.1.3 optionalDependencies: '@types/node': 24.11.0 - '@inquirer/confirm@6.0.8(@types/node@24.11.0)': + '@inquirer/confirm@5.1.21(@types/node@24.11.0)': dependencies: - '@inquirer/core': 11.1.5(@types/node@24.11.0) - '@inquirer/type': 4.0.3(@types/node@24.11.0) + '@inquirer/core': 10.3.2(@types/node@24.11.0) + '@inquirer/type': 3.0.10(@types/node@24.11.0) optionalDependencies: '@types/node': 24.11.0 - '@inquirer/core@11.1.5(@types/node@24.11.0)': + '@inquirer/core@10.3.2(@types/node@24.11.0)': dependencies: - '@inquirer/ansi': 2.0.3 - '@inquirer/figures': 2.0.3 - '@inquirer/type': 4.0.3(@types/node@24.11.0) + '@inquirer/ansi': 1.0.2 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@24.11.0) cli-width: 4.1.0 - fast-wrap-ansi: 0.2.0 - mute-stream: 3.0.0 + mute-stream: 2.0.0 signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.3 optionalDependencies: '@types/node': 24.11.0 - '@inquirer/editor@5.0.8(@types/node@24.11.0)': + '@inquirer/editor@4.2.23(@types/node@24.11.0)': dependencies: - '@inquirer/core': 11.1.5(@types/node@24.11.0) - '@inquirer/external-editor': 2.0.3(@types/node@24.11.0) - '@inquirer/type': 4.0.3(@types/node@24.11.0) + '@inquirer/core': 10.3.2(@types/node@24.11.0) + '@inquirer/external-editor': 1.0.3(@types/node@24.11.0) + '@inquirer/type': 3.0.10(@types/node@24.11.0) optionalDependencies: '@types/node': 24.11.0 - '@inquirer/expand@5.0.8(@types/node@24.11.0)': + '@inquirer/expand@4.0.23(@types/node@24.11.0)': dependencies: - '@inquirer/core': 11.1.5(@types/node@24.11.0) - '@inquirer/type': 4.0.3(@types/node@24.11.0) + '@inquirer/core': 10.3.2(@types/node@24.11.0) + '@inquirer/type': 3.0.10(@types/node@24.11.0) + yoctocolors-cjs: 2.1.3 optionalDependencies: '@types/node': 24.11.0 - '@inquirer/external-editor@2.0.3(@types/node@24.11.0)': + '@inquirer/external-editor@1.0.3(@types/node@24.11.0)': dependencies: chardet: 2.1.1 iconv-lite: 0.7.2 optionalDependencies: '@types/node': 24.11.0 - '@inquirer/figures@2.0.3': {} + '@inquirer/figures@1.0.15': {} - '@inquirer/input@5.0.8(@types/node@24.11.0)': + '@inquirer/input@4.3.1(@types/node@24.11.0)': dependencies: - '@inquirer/core': 11.1.5(@types/node@24.11.0) - '@inquirer/type': 4.0.3(@types/node@24.11.0) + '@inquirer/core': 10.3.2(@types/node@24.11.0) + '@inquirer/type': 3.0.10(@types/node@24.11.0) optionalDependencies: '@types/node': 24.11.0 - '@inquirer/number@4.0.8(@types/node@24.11.0)': + '@inquirer/number@3.0.23(@types/node@24.11.0)': dependencies: - '@inquirer/core': 11.1.5(@types/node@24.11.0) - '@inquirer/type': 4.0.3(@types/node@24.11.0) + '@inquirer/core': 10.3.2(@types/node@24.11.0) + '@inquirer/type': 3.0.10(@types/node@24.11.0) optionalDependencies: '@types/node': 24.11.0 - '@inquirer/password@5.0.8(@types/node@24.11.0)': + '@inquirer/password@4.0.23(@types/node@24.11.0)': dependencies: - '@inquirer/ansi': 2.0.3 - '@inquirer/core': 11.1.5(@types/node@24.11.0) - '@inquirer/type': 4.0.3(@types/node@24.11.0) + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@24.11.0) + '@inquirer/type': 3.0.10(@types/node@24.11.0) optionalDependencies: '@types/node': 24.11.0 - '@inquirer/prompts@8.3.0(@types/node@24.11.0)': - dependencies: - '@inquirer/checkbox': 5.1.0(@types/node@24.11.0) - '@inquirer/confirm': 6.0.8(@types/node@24.11.0) - '@inquirer/editor': 5.0.8(@types/node@24.11.0) - '@inquirer/expand': 5.0.8(@types/node@24.11.0) - '@inquirer/input': 5.0.8(@types/node@24.11.0) - '@inquirer/number': 4.0.8(@types/node@24.11.0) - '@inquirer/password': 5.0.8(@types/node@24.11.0) - '@inquirer/rawlist': 5.2.4(@types/node@24.11.0) - '@inquirer/search': 4.1.4(@types/node@24.11.0) - '@inquirer/select': 5.1.0(@types/node@24.11.0) + '@inquirer/prompts@7.10.1(@types/node@24.11.0)': + dependencies: + '@inquirer/checkbox': 4.3.2(@types/node@24.11.0) + '@inquirer/confirm': 5.1.21(@types/node@24.11.0) + '@inquirer/editor': 4.2.23(@types/node@24.11.0) + '@inquirer/expand': 4.0.23(@types/node@24.11.0) + '@inquirer/input': 4.3.1(@types/node@24.11.0) + '@inquirer/number': 3.0.23(@types/node@24.11.0) + '@inquirer/password': 4.0.23(@types/node@24.11.0) + '@inquirer/rawlist': 4.1.11(@types/node@24.11.0) + '@inquirer/search': 3.2.2(@types/node@24.11.0) + '@inquirer/select': 4.4.2(@types/node@24.11.0) optionalDependencies: '@types/node': 24.11.0 - '@inquirer/rawlist@5.2.4(@types/node@24.11.0)': + '@inquirer/rawlist@4.1.11(@types/node@24.11.0)': dependencies: - '@inquirer/core': 11.1.5(@types/node@24.11.0) - '@inquirer/type': 4.0.3(@types/node@24.11.0) + '@inquirer/core': 10.3.2(@types/node@24.11.0) + '@inquirer/type': 3.0.10(@types/node@24.11.0) + yoctocolors-cjs: 2.1.3 optionalDependencies: '@types/node': 24.11.0 - '@inquirer/search@4.1.4(@types/node@24.11.0)': + '@inquirer/search@3.2.2(@types/node@24.11.0)': dependencies: - '@inquirer/core': 11.1.5(@types/node@24.11.0) - '@inquirer/figures': 2.0.3 - '@inquirer/type': 4.0.3(@types/node@24.11.0) + '@inquirer/core': 10.3.2(@types/node@24.11.0) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@24.11.0) + yoctocolors-cjs: 2.1.3 optionalDependencies: '@types/node': 24.11.0 - '@inquirer/select@5.1.0(@types/node@24.11.0)': + '@inquirer/select@4.4.2(@types/node@24.11.0)': dependencies: - '@inquirer/ansi': 2.0.3 - '@inquirer/core': 11.1.5(@types/node@24.11.0) - '@inquirer/figures': 2.0.3 - '@inquirer/type': 4.0.3(@types/node@24.11.0) + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@24.11.0) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@24.11.0) + yoctocolors-cjs: 2.1.3 optionalDependencies: '@types/node': 24.11.0 - '@inquirer/type@4.0.3(@types/node@24.11.0)': + '@inquirer/type@3.0.10(@types/node@24.11.0)': optionalDependencies: '@types/node': 24.11.0 @@ -2962,23 +3187,24 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@napi-rs/cli@3.5.1(@emnapi/runtime@1.8.1)(@types/node@24.11.0)': + '@napi-rs/cli@3.4.1(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@24.11.0)': dependencies: - '@inquirer/prompts': 8.3.0(@types/node@24.11.0) - '@napi-rs/cross-toolchain': 1.0.3 - '@napi-rs/wasm-tools': 1.0.1 + '@inquirer/prompts': 7.10.1(@types/node@24.11.0) + '@napi-rs/cross-toolchain': 1.0.3(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1) + '@napi-rs/wasm-tools': 1.0.1(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1) '@octokit/rest': 22.0.1 clipanion: 4.0.0-rc.4(typanion@3.14.0) colorette: 2.0.20 + debug: 4.4.3 emnapi: 1.8.1 es-toolkit: 1.45.1 js-yaml: 4.1.1 - obug: 2.1.1 semver: 7.7.4 typanion: 3.14.0 optionalDependencies: '@emnapi/runtime': 1.8.1 transitivePeerDependencies: + - '@emnapi/core' - '@napi-rs/cross-toolchain-arm64-target-aarch64' - '@napi-rs/cross-toolchain-arm64-target-armv7' - '@napi-rs/cross-toolchain-arm64-target-ppc64le' @@ -2993,12 +3219,14 @@ snapshots: - node-addon-api - supports-color - '@napi-rs/cross-toolchain@1.0.3': + '@napi-rs/cross-toolchain@1.0.3(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)': dependencies: - '@napi-rs/lzma': 1.4.5 - '@napi-rs/tar': 1.1.0 + '@napi-rs/lzma': 1.4.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1) + '@napi-rs/tar': 1.1.0(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1) debug: 4.4.3 transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' - supports-color '@napi-rs/lzma-android-arm-eabi@1.4.5': @@ -3040,9 +3268,12 @@ snapshots: '@napi-rs/lzma-linux-x64-musl@1.4.5': optional: true - '@napi-rs/lzma-wasm32-wasi@1.4.5': + '@napi-rs/lzma-wasm32-wasi@1.4.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)': dependencies: - '@napi-rs/wasm-runtime': 1.1.1 + '@napi-rs/wasm-runtime': 1.1.2(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' optional: true '@napi-rs/lzma-win32-arm64-msvc@1.4.5': @@ -3054,7 +3285,7 @@ snapshots: '@napi-rs/lzma-win32-x64-msvc@1.4.5': optional: true - '@napi-rs/lzma@1.4.5': + '@napi-rs/lzma@1.4.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)': optionalDependencies: '@napi-rs/lzma-android-arm-eabi': 1.4.5 '@napi-rs/lzma-android-arm64': 1.4.5 @@ -3069,10 +3300,13 @@ snapshots: '@napi-rs/lzma-linux-s390x-gnu': 1.4.5 '@napi-rs/lzma-linux-x64-gnu': 1.4.5 '@napi-rs/lzma-linux-x64-musl': 1.4.5 - '@napi-rs/lzma-wasm32-wasi': 1.4.5 + '@napi-rs/lzma-wasm32-wasi': 1.4.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1) '@napi-rs/lzma-win32-arm64-msvc': 1.4.5 '@napi-rs/lzma-win32-ia32-msvc': 1.4.5 '@napi-rs/lzma-win32-x64-msvc': 1.4.5 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' '@napi-rs/tar-android-arm-eabi@1.1.0': optional: true @@ -3110,9 +3344,12 @@ snapshots: '@napi-rs/tar-linux-x64-musl@1.1.0': optional: true - '@napi-rs/tar-wasm32-wasi@1.1.0': + '@napi-rs/tar-wasm32-wasi@1.1.0(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)': dependencies: - '@napi-rs/wasm-runtime': 1.1.1 + '@napi-rs/wasm-runtime': 1.1.2(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' optional: true '@napi-rs/tar-win32-arm64-msvc@1.1.0': @@ -3124,7 +3361,7 @@ snapshots: '@napi-rs/tar-win32-x64-msvc@1.1.0': optional: true - '@napi-rs/tar@1.1.0': + '@napi-rs/tar@1.1.0(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)': optionalDependencies: '@napi-rs/tar-android-arm-eabi': 1.1.0 '@napi-rs/tar-android-arm64': 1.1.0 @@ -3138,12 +3375,15 @@ snapshots: '@napi-rs/tar-linux-s390x-gnu': 1.1.0 '@napi-rs/tar-linux-x64-gnu': 1.1.0 '@napi-rs/tar-linux-x64-musl': 1.1.0 - '@napi-rs/tar-wasm32-wasi': 1.1.0 + '@napi-rs/tar-wasm32-wasi': 1.1.0(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1) '@napi-rs/tar-win32-arm64-msvc': 1.1.0 '@napi-rs/tar-win32-ia32-msvc': 1.1.0 '@napi-rs/tar-win32-x64-msvc': 1.1.0 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' - '@napi-rs/wasm-runtime@1.1.1': + '@napi-rs/wasm-runtime@1.1.2(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)': dependencies: '@emnapi/core': 1.8.1 '@emnapi/runtime': 1.8.1 @@ -3177,9 +3417,12 @@ snapshots: '@napi-rs/wasm-tools-linux-x64-musl@1.0.1': optional: true - '@napi-rs/wasm-tools-wasm32-wasi@1.0.1': + '@napi-rs/wasm-tools-wasm32-wasi@1.0.1(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)': dependencies: - '@napi-rs/wasm-runtime': 1.1.1 + '@napi-rs/wasm-runtime': 1.1.2(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' optional: true '@napi-rs/wasm-tools-win32-arm64-msvc@1.0.1': @@ -3191,7 +3434,7 @@ snapshots: '@napi-rs/wasm-tools-win32-x64-msvc@1.0.1': optional: true - '@napi-rs/wasm-tools@1.0.1': + '@napi-rs/wasm-tools@1.0.1(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)': optionalDependencies: '@napi-rs/wasm-tools-android-arm-eabi': 1.0.1 '@napi-rs/wasm-tools-android-arm64': 1.0.1 @@ -3202,10 +3445,13 @@ snapshots: '@napi-rs/wasm-tools-linux-arm64-musl': 1.0.1 '@napi-rs/wasm-tools-linux-x64-gnu': 1.0.1 '@napi-rs/wasm-tools-linux-x64-musl': 1.0.1 - '@napi-rs/wasm-tools-wasm32-wasi': 1.0.1 + '@napi-rs/wasm-tools-wasm32-wasi': 1.0.1(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1) '@napi-rs/wasm-tools-win32-arm64-msvc': 1.0.1 '@napi-rs/wasm-tools-win32-ia32-msvc': 1.0.1 '@napi-rs/wasm-tools-win32-x64-msvc': 1.0.1 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' '@nodelib/fs.scandir@2.1.5': dependencies: @@ -3330,9 +3576,12 @@ snapshots: '@oxc-parser/binding-openharmony-arm64@0.115.0': optional: true - '@oxc-parser/binding-wasm32-wasi@0.115.0': + '@oxc-parser/binding-wasm32-wasi@0.115.0(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)': dependencies: - '@napi-rs/wasm-runtime': 1.1.1 + '@napi-rs/wasm-runtime': 1.1.2(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' optional: true '@oxc-parser/binding-win32-arm64-msvc@0.115.0': @@ -3346,6 +3595,8 @@ snapshots: '@oxc-project/types@0.115.0': {} + '@oxc-project/types@0.122.0': {} + '@polka/url@1.0.0-next.29': {} '@poppinss/colors@4.1.6': @@ -3364,104 +3615,156 @@ snapshots: dependencies: quansync: 1.0.0 - '@rollup/plugin-terser@1.0.0(rollup@4.59.0)': + '@rolldown/binding-android-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.12(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)': dependencies: - serialize-javascript: 7.0.4 + '@napi-rs/wasm-runtime': 1.1.2(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': + optional: true + + '@rolldown/pluginutils@1.0.0-rc.12': {} + + '@rollup/plugin-terser@1.0.0(rollup@4.60.3)': + dependencies: + serialize-javascript: 7.0.5 smob: 1.6.1 terser: 5.46.0 optionalDependencies: - rollup: 4.59.0 + rollup: 4.60.3 - '@rollup/plugin-typescript@12.3.0(rollup@4.59.0)(tslib@2.8.1)(typescript@5.9.3)': + '@rollup/plugin-typescript@12.3.0(rollup@4.60.3)(tslib@2.8.1)(typescript@6.0.2)': dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.59.0) + '@rollup/pluginutils': 5.3.0(rollup@4.60.3) resolve: 1.22.11 - typescript: 5.9.3 + typescript: 6.0.2 optionalDependencies: - rollup: 4.59.0 + rollup: 4.60.3 tslib: 2.8.1 - '@rollup/pluginutils@5.3.0(rollup@4.59.0)': + '@rollup/pluginutils@5.3.0(rollup@4.60.3)': dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 estree-walker: 2.0.2 - picomatch: 4.0.3 + picomatch: 4.0.4 optionalDependencies: - rollup: 4.59.0 + rollup: 4.60.3 - '@rollup/rollup-android-arm-eabi@4.59.0': + '@rollup/rollup-android-arm-eabi@4.60.3': optional: true - '@rollup/rollup-android-arm64@4.59.0': + '@rollup/rollup-android-arm64@4.60.3': optional: true - '@rollup/rollup-darwin-arm64@4.59.0': + '@rollup/rollup-darwin-arm64@4.60.3': optional: true - '@rollup/rollup-darwin-x64@4.59.0': + '@rollup/rollup-darwin-x64@4.60.3': optional: true - '@rollup/rollup-freebsd-arm64@4.59.0': + '@rollup/rollup-freebsd-arm64@4.60.3': optional: true - '@rollup/rollup-freebsd-x64@4.59.0': + '@rollup/rollup-freebsd-x64@4.60.3': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + '@rollup/rollup-linux-arm-gnueabihf@4.60.3': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.59.0': + '@rollup/rollup-linux-arm-musleabihf@4.60.3': optional: true - '@rollup/rollup-linux-arm64-gnu@4.59.0': + '@rollup/rollup-linux-arm64-gnu@4.60.3': optional: true - '@rollup/rollup-linux-arm64-musl@4.59.0': + '@rollup/rollup-linux-arm64-musl@4.60.3': optional: true - '@rollup/rollup-linux-loong64-gnu@4.59.0': + '@rollup/rollup-linux-loong64-gnu@4.60.3': optional: true - '@rollup/rollup-linux-loong64-musl@4.59.0': + '@rollup/rollup-linux-loong64-musl@4.60.3': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.59.0': + '@rollup/rollup-linux-ppc64-gnu@4.60.3': optional: true - '@rollup/rollup-linux-ppc64-musl@4.59.0': + '@rollup/rollup-linux-ppc64-musl@4.60.3': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.59.0': + '@rollup/rollup-linux-riscv64-gnu@4.60.3': optional: true - '@rollup/rollup-linux-riscv64-musl@4.59.0': + '@rollup/rollup-linux-riscv64-musl@4.60.3': optional: true - '@rollup/rollup-linux-s390x-gnu@4.59.0': + '@rollup/rollup-linux-s390x-gnu@4.60.3': optional: true - '@rollup/rollup-linux-x64-gnu@4.59.0': + '@rollup/rollup-linux-x64-gnu@4.60.3': optional: true - '@rollup/rollup-linux-x64-musl@4.59.0': + '@rollup/rollup-linux-x64-musl@4.60.3': optional: true - '@rollup/rollup-openbsd-x64@4.59.0': + '@rollup/rollup-openbsd-x64@4.60.3': optional: true - '@rollup/rollup-openharmony-arm64@4.59.0': + '@rollup/rollup-openharmony-arm64@4.60.3': optional: true - '@rollup/rollup-win32-arm64-msvc@4.59.0': + '@rollup/rollup-win32-arm64-msvc@4.60.3': optional: true - '@rollup/rollup-win32-ia32-msvc@4.59.0': + '@rollup/rollup-win32-ia32-msvc@4.60.3': optional: true - '@rollup/rollup-win32-x64-gnu@4.59.0': + '@rollup/rollup-win32-x64-gnu@4.60.3': optional: true - '@rollup/rollup-win32-x64-msvc@4.59.0': + '@rollup/rollup-win32-x64-msvc@4.60.3': optional: true '@sindresorhus/is@7.2.0': {} @@ -3474,22 +3777,14 @@ snapshots: dependencies: acorn: 8.16.0 - '@sveltejs/vite-plugin-svelte-inspector@5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(terser@5.46.0)))(svelte@5.53.7)(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(terser@5.46.0))': + '@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.7(@typescript-eslint/types@8.58.2))(vite@8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@24.11.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0))': dependencies: - '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(terser@5.46.0)) - obug: 2.1.1 - svelte: 5.53.7 - vite: 7.3.1(@types/node@24.11.0)(jiti@2.6.1)(terser@5.46.0) - - '@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(terser@5.46.0))': - dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(terser@5.46.0)))(svelte@5.53.7)(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(terser@5.46.0)) deepmerge: 4.3.1 magic-string: 0.30.21 obug: 2.1.1 - svelte: 5.53.7 - vite: 7.3.1(@types/node@24.11.0)(jiti@2.6.1)(terser@5.46.0) - vitefu: 1.1.2(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(terser@5.46.0)) + svelte: 5.55.7(@typescript-eslint/types@8.58.2) + vite: 8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@24.11.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0) + vitefu: 1.1.2(vite@8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@24.11.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0)) '@tybys/wasm-util@0.10.1': dependencies: @@ -3505,13 +3800,15 @@ snapshots: '@types/eslint@9.6.1': dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 '@types/json-schema': 7.0.15 '@types/esrecurse@4.3.1': {} '@types/estree@1.0.8': {} + '@types/estree@1.0.9': {} + '@types/json-schema@7.0.15': {} '@types/node@24.11.0': @@ -3520,105 +3817,105 @@ snapshots: '@types/trusted-types@2.0.7': {} - '@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.0.2(jiti@2.6.1))(typescript@6.0.2))(eslint@10.0.2(jiti@2.6.1))(typescript@6.0.2)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.56.1 - '@typescript-eslint/type-utils': 8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.56.1 + '@typescript-eslint/parser': 8.58.2(eslint@10.0.2(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/scope-manager': 8.58.2 + '@typescript-eslint/type-utils': 8.58.2(eslint@10.0.2(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/utils': 8.58.2(eslint@10.0.2(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/visitor-keys': 8.58.2 eslint: 10.0.2(jiti@2.6.1) ignore: 7.0.5 natural-compare: 1.4.0 - ts-api-utils: 2.4.0(typescript@5.9.3) - typescript: 5.9.3 + ts-api-utils: 2.5.0(typescript@6.0.2) + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/parser@8.58.2(eslint@10.0.2(jiti@2.6.1))(typescript@6.0.2)': dependencies: - '@typescript-eslint/scope-manager': 8.56.1 - '@typescript-eslint/types': 8.56.1 - '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.56.1 + '@typescript-eslint/scope-manager': 8.58.2 + '@typescript-eslint/types': 8.58.2 + '@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.2) + '@typescript-eslint/visitor-keys': 8.58.2 debug: 4.4.3 eslint: 10.0.2(jiti@2.6.1) - typescript: 5.9.3 + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.56.1(typescript@5.9.3)': + '@typescript-eslint/project-service@8.58.2(typescript@6.0.2)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3) - '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/tsconfig-utils': 8.58.2(typescript@6.0.2) + '@typescript-eslint/types': 8.58.2 debug: 4.4.3 - typescript: 5.9.3 + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.56.1': + '@typescript-eslint/scope-manager@8.58.2': dependencies: - '@typescript-eslint/types': 8.56.1 - '@typescript-eslint/visitor-keys': 8.56.1 + '@typescript-eslint/types': 8.58.2 + '@typescript-eslint/visitor-keys': 8.58.2 - '@typescript-eslint/tsconfig-utils@8.56.1(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.58.2(typescript@6.0.2)': dependencies: - typescript: 5.9.3 + typescript: 6.0.2 - '@typescript-eslint/type-utils@8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.58.2(eslint@10.0.2(jiti@2.6.1))(typescript@6.0.2)': dependencies: - '@typescript-eslint/types': 8.56.1 - '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.58.2 + '@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.2) + '@typescript-eslint/utils': 8.58.2(eslint@10.0.2(jiti@2.6.1))(typescript@6.0.2) debug: 4.4.3 eslint: 10.0.2(jiti@2.6.1) - ts-api-utils: 2.4.0(typescript@5.9.3) - typescript: 5.9.3 + ts-api-utils: 2.5.0(typescript@6.0.2) + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.56.1': {} + '@typescript-eslint/types@8.58.2': {} - '@typescript-eslint/typescript-estree@8.56.1(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.58.2(typescript@6.0.2)': dependencies: - '@typescript-eslint/project-service': 8.56.1(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3) - '@typescript-eslint/types': 8.56.1 - '@typescript-eslint/visitor-keys': 8.56.1 + '@typescript-eslint/project-service': 8.58.2(typescript@6.0.2) + '@typescript-eslint/tsconfig-utils': 8.58.2(typescript@6.0.2) + '@typescript-eslint/types': 8.58.2 + '@typescript-eslint/visitor-keys': 8.58.2 debug: 4.4.3 - minimatch: 10.2.4 + minimatch: 10.2.5 semver: 7.7.4 - tinyglobby: 0.2.15 - ts-api-utils: 2.4.0(typescript@5.9.3) - typescript: 5.9.3 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@6.0.2) + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/utils@8.58.2(eslint@10.0.2(jiti@2.6.1))(typescript@6.0.2)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.2(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.56.1 - '@typescript-eslint/types': 8.56.1 - '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.58.2 + '@typescript-eslint/types': 8.58.2 + '@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.2) eslint: 10.0.2(jiti@2.6.1) - typescript: 5.9.3 + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.56.1': + '@typescript-eslint/visitor-keys@8.58.2': dependencies: - '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/types': 8.58.2 eslint-visitor-keys: 5.0.1 - '@unocss/cli@66.6.5': + '@unocss/cli@66.6.6': dependencies: '@jridgewell/remapping': 2.3.5 - '@unocss/config': 66.6.4 - '@unocss/core': 66.6.5 - '@unocss/preset-wind3': 66.6.5 - '@unocss/preset-wind4': 66.6.5 - '@unocss/transformer-directives': 66.6.5 + '@unocss/config': 66.6.6 + '@unocss/core': 66.6.6 + '@unocss/preset-wind3': 66.6.6 + '@unocss/preset-wind4': 66.6.6 + '@unocss/transformer-directives': 66.6.6 cac: 6.7.14 chokidar: 5.0.0 colorette: 2.0.20 @@ -3626,162 +3923,165 @@ snapshots: magic-string: 0.30.21 pathe: 2.0.3 perfect-debounce: 2.1.0 - tinyglobby: 0.2.15 + tinyglobby: 0.2.17 unplugin-utils: 0.3.1 - '@unocss/config@66.6.4': + '@unocss/config@66.6.6': dependencies: - '@unocss/core': 66.6.4 + '@unocss/core': 66.6.6 colorette: 2.0.20 consola: 3.4.2 unconfig: 7.5.0 - '@unocss/core@66.6.4': {} + '@unocss/core@66.6.6': {} - '@unocss/core@66.6.5': {} - - '@unocss/extractor-arbitrary-variants@66.6.5': + '@unocss/extractor-arbitrary-variants@66.6.6': dependencies: - '@unocss/core': 66.6.5 + '@unocss/core': 66.6.6 - '@unocss/extractor-svelte@66.6.5': {} + '@unocss/extractor-svelte@66.6.6': {} - '@unocss/inspector@66.6.5': + '@unocss/inspector@66.6.6': dependencies: - '@unocss/core': 66.6.5 - '@unocss/rule-utils': 66.6.5 + '@unocss/core': 66.6.6 + '@unocss/rule-utils': 66.6.6 colorette: 2.0.20 gzip-size: 6.0.0 sirv: 3.0.2 - '@unocss/preset-attributify@66.6.5': + '@unocss/preset-attributify@66.6.6': dependencies: - '@unocss/core': 66.6.5 + '@unocss/core': 66.6.6 - '@unocss/preset-icons@66.6.5': + '@unocss/preset-icons@66.6.6': dependencies: '@iconify/utils': 3.1.0 - '@unocss/core': 66.6.5 + '@unocss/core': 66.6.6 ofetch: 1.5.1 - '@unocss/preset-mini@66.6.5': + '@unocss/preset-mini@66.6.6': dependencies: - '@unocss/core': 66.6.5 - '@unocss/extractor-arbitrary-variants': 66.6.5 - '@unocss/rule-utils': 66.6.5 + '@unocss/core': 66.6.6 + '@unocss/extractor-arbitrary-variants': 66.6.6 + '@unocss/rule-utils': 66.6.6 - '@unocss/preset-tagify@66.6.5': + '@unocss/preset-tagify@66.6.6': dependencies: - '@unocss/core': 66.6.5 + '@unocss/core': 66.6.6 - '@unocss/preset-typography@66.6.5': + '@unocss/preset-typography@66.6.6': dependencies: - '@unocss/core': 66.6.5 - '@unocss/rule-utils': 66.6.5 + '@unocss/core': 66.6.6 + '@unocss/rule-utils': 66.6.6 - '@unocss/preset-uno@66.6.5': + '@unocss/preset-uno@66.6.6': dependencies: - '@unocss/core': 66.6.5 - '@unocss/preset-wind3': 66.6.5 + '@unocss/core': 66.6.6 + '@unocss/preset-wind3': 66.6.6 - '@unocss/preset-web-fonts@66.6.5': + '@unocss/preset-web-fonts@66.6.6': dependencies: - '@unocss/core': 66.6.5 + '@unocss/core': 66.6.6 ofetch: 1.5.1 - '@unocss/preset-wind3@66.6.5': + '@unocss/preset-wind3@66.6.6': dependencies: - '@unocss/core': 66.6.5 - '@unocss/preset-mini': 66.6.5 - '@unocss/rule-utils': 66.6.5 + '@unocss/core': 66.6.6 + '@unocss/preset-mini': 66.6.6 + '@unocss/rule-utils': 66.6.6 - '@unocss/preset-wind4@66.6.5': + '@unocss/preset-wind4@66.6.6': dependencies: - '@unocss/core': 66.6.5 - '@unocss/extractor-arbitrary-variants': 66.6.5 - '@unocss/rule-utils': 66.6.5 + '@unocss/core': 66.6.6 + '@unocss/extractor-arbitrary-variants': 66.6.6 + '@unocss/rule-utils': 66.6.6 - '@unocss/preset-wind@66.6.5': + '@unocss/preset-wind@66.6.6': dependencies: - '@unocss/core': 66.6.5 - '@unocss/preset-wind3': 66.6.5 + '@unocss/core': 66.6.6 + '@unocss/preset-wind3': 66.6.6 - '@unocss/rule-utils@66.6.5': + '@unocss/rule-utils@66.6.6': dependencies: - '@unocss/core': 66.6.5 + '@unocss/core': 66.6.6 magic-string: 0.30.21 - '@unocss/transformer-attributify-jsx@66.6.5': + '@unocss/transformer-attributify-jsx@66.6.6(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)': dependencies: - '@unocss/core': 66.6.5 - oxc-parser: 0.115.0 - oxc-walker: 0.7.0(oxc-parser@0.115.0) + '@unocss/core': 66.6.6 + oxc-parser: 0.115.0(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1) + oxc-walker: 0.7.0(oxc-parser@0.115.0(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' - '@unocss/transformer-compile-class@66.6.5': + '@unocss/transformer-compile-class@66.6.6': dependencies: - '@unocss/core': 66.6.5 + '@unocss/core': 66.6.6 - '@unocss/transformer-directives@66.6.5': + '@unocss/transformer-directives@66.6.6': dependencies: - '@unocss/core': 66.6.5 - '@unocss/rule-utils': 66.6.5 + '@unocss/core': 66.6.6 + '@unocss/rule-utils': 66.6.6 css-tree: 3.1.0 - '@unocss/transformer-variant-group@66.6.5': + '@unocss/transformer-variant-group@66.6.6': dependencies: - '@unocss/core': 66.6.5 + '@unocss/core': 66.6.6 - '@unocss/vite@66.6.5(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(terser@5.46.0))': + '@unocss/vite@66.6.6(vite@8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@24.11.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0))': dependencies: '@jridgewell/remapping': 2.3.5 - '@unocss/config': 66.6.4 - '@unocss/core': 66.6.5 - '@unocss/inspector': 66.6.5 + '@unocss/config': 66.6.6 + '@unocss/core': 66.6.6 + '@unocss/inspector': 66.6.6 chokidar: 5.0.0 magic-string: 0.30.21 pathe: 2.0.3 - tinyglobby: 0.2.15 + tinyglobby: 0.2.17 unplugin-utils: 0.3.1 - vite: 7.3.1(@types/node@24.11.0)(jiti@2.6.1)(terser@5.46.0) + vite: 8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@24.11.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0) - '@vitest/expect@4.0.18': + '@vitest/expect@4.1.0': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@vitest/spy': 4.0.18 - '@vitest/utils': 4.0.18 + '@vitest/spy': 4.1.0 + '@vitest/utils': 4.1.0 chai: 6.2.2 - tinyrainbow: 3.0.3 + tinyrainbow: 3.1.0 - '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(terser@5.46.0))': + '@vitest/mocker@4.1.0(vite@8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@24.11.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0))': dependencies: - '@vitest/spy': 4.0.18 + '@vitest/spy': 4.1.0 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(@types/node@24.11.0)(jiti@2.6.1)(terser@5.46.0) + vite: 8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@24.11.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0) - '@vitest/pretty-format@4.0.18': + '@vitest/pretty-format@4.1.0': dependencies: - tinyrainbow: 3.0.3 + tinyrainbow: 3.1.0 - '@vitest/runner@4.0.18': + '@vitest/runner@4.1.0': dependencies: - '@vitest/utils': 4.0.18 + '@vitest/utils': 4.1.0 pathe: 2.0.3 - '@vitest/snapshot@4.0.18': + '@vitest/snapshot@4.1.0': dependencies: - '@vitest/pretty-format': 4.0.18 + '@vitest/pretty-format': 4.1.0 + '@vitest/utils': 4.1.0 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@4.0.18': {} + '@vitest/spy@4.1.0': {} - '@vitest/utils@4.0.18': + '@vitest/utils@4.1.0': dependencies: - '@vitest/pretty-format': 4.0.18 - tinyrainbow: 3.0.3 + '@vitest/pretty-format': 4.1.0 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 acorn-jsx@5.3.2(acorn@8.16.0): dependencies: @@ -3796,6 +4096,12 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + argparse@2.0.1: {} aria-query@5.3.1: {} @@ -3810,7 +4116,7 @@ snapshots: blake3-wasm@2.1.5: {} - brace-expansion@5.0.4: + brace-expansion@5.0.6: dependencies: balanced-match: 4.0.4 @@ -3838,6 +4144,12 @@ snapshots: clsx@2.1.1: {} + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + colorette@2.0.20: {} commander@2.20.3: {} @@ -3846,6 +4158,8 @@ snapshots: consola@3.4.2: {} + convert-source-map@2.0.0: {} + cookie@1.1.1: {} cross-env@10.1.0: @@ -3872,21 +4186,23 @@ snapshots: deepmerge@4.3.1: {} - defu@6.1.4: {} + defu@6.1.6: {} destr@2.0.5: {} detect-libc@2.1.2: {} - devalue@5.6.3: {} + devalue@5.8.1: {} duplexer@0.1.2: {} emnapi@1.8.1: {} + emoji-regex@8.0.0: {} + error-stack-parser-es@1.0.5: {} - es-module-lexer@1.7.0: {} + es-module-lexer@2.1.0: {} es-toolkit@1.45.1: {} @@ -3932,7 +4248,7 @@ snapshots: eslint-scope@9.1.1: dependencies: '@types/esrecurse': 4.3.1 - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 esrecurse: 4.3.0 estraverse: 5.3.0 @@ -3951,7 +4267,7 @@ snapshots: '@humanfs/node': 0.16.7 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 ajv: 6.14.0 cross-spawn: 7.0.6 debug: 4.4.3 @@ -3969,7 +4285,7 @@ snapshots: imurmurhash: 0.1.4 is-glob: 4.0.3 json-stable-stringify-without-jsonify: 1.0.1 - minimatch: 10.2.4 + minimatch: 10.2.5 natural-compare: 1.4.0 optionator: 0.9.4 optionalDependencies: @@ -3989,9 +4305,11 @@ snapshots: dependencies: estraverse: 5.3.0 - esrap@2.2.3: + esrap@2.2.8(@typescript-eslint/types@8.58.2): dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + optionalDependencies: + '@typescript-eslint/types': 8.58.2 esrecurse@4.3.0: dependencies: @@ -4003,7 +4321,7 @@ snapshots: estree-walker@3.0.3: dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 esutils@2.0.3: {} @@ -4025,23 +4343,13 @@ snapshots: fast-levenshtein@2.0.6: {} - fast-string-truncated-width@3.0.3: {} - - fast-string-width@3.0.2: - dependencies: - fast-string-truncated-width: 3.0.3 - - fast-wrap-ansi@0.2.0: - dependencies: - fast-string-width: 3.0.2 - fastq@1.20.1: dependencies: reusify: 1.1.0 - fdir@6.5.0(picomatch@4.0.3): + fdir@6.5.0(picomatch@4.0.4): optionalDependencies: - picomatch: 4.0.3 + picomatch: 4.0.4 file-entry-cache@8.0.0: dependencies: @@ -4058,10 +4366,10 @@ snapshots: flat-cache@4.0.1: dependencies: - flatted: 3.3.4 + flatted: 3.4.2 keyv: 4.5.4 - flatted@3.3.4: {} + flatted@3.4.2: {} fsevents@2.3.3: optional: true @@ -4102,6 +4410,8 @@ snapshots: is-extglob@2.1.1: {} + is-fullwidth-code-point@3.0.0: {} + is-glob@4.0.3: dependencies: is-extglob: 2.1.1 @@ -4110,7 +4420,7 @@ snapshots: is-reference@3.0.3: dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 isexe@2.0.0: {} @@ -4139,6 +4449,55 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + locate-character@3.0.0: {} locate-path@6.0.0: @@ -4166,23 +4525,23 @@ snapshots: micromatch@4.0.8: dependencies: braces: 3.0.3 - picomatch: 2.3.1 + picomatch: 2.3.2 - miniflare@4.20260301.1: + miniflare@4.20260317.0: dependencies: '@cspotcode/source-map-support': 0.8.1 sharp: 0.34.5 - undici: 7.18.2 - workerd: 1.20260301.1 - ws: 8.18.0 + undici: 7.24.4 + workerd: 1.20260317.1 + ws: 8.20.1 youch: 4.1.0-beta.10 transitivePeerDependencies: - bufferutil - utf-8-validate - minimatch@10.2.4: + minimatch@10.2.5: dependencies: - brace-expansion: 5.0.4 + brace-expansion: 5.0.6 mlly@1.8.0: dependencies: @@ -4195,7 +4554,7 @@ snapshots: ms@2.1.3: {} - mute-stream@3.0.0: {} + mute-stream@2.0.0: {} nanoid@3.3.11: {} @@ -4220,7 +4579,7 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 - oxc-parser@0.115.0: + oxc-parser@0.115.0(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1): dependencies: '@oxc-project/types': 0.115.0 optionalDependencies: @@ -4240,15 +4599,18 @@ snapshots: '@oxc-parser/binding-linux-x64-gnu': 0.115.0 '@oxc-parser/binding-linux-x64-musl': 0.115.0 '@oxc-parser/binding-openharmony-arm64': 0.115.0 - '@oxc-parser/binding-wasm32-wasi': 0.115.0 + '@oxc-parser/binding-wasm32-wasi': 0.115.0(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1) '@oxc-parser/binding-win32-arm64-msvc': 0.115.0 '@oxc-parser/binding-win32-ia32-msvc': 0.115.0 '@oxc-parser/binding-win32-x64-msvc': 0.115.0 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' - oxc-walker@0.7.0(oxc-parser@0.115.0): + oxc-walker@0.7.0(oxc-parser@0.115.0(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)): dependencies: magic-regexp: 0.10.0 - oxc-parser: 0.115.0 + oxc-parser: 0.115.0(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1) p-limit@3.1.0: dependencies: @@ -4274,9 +4636,9 @@ snapshots: picocolors@1.1.1: {} - picomatch@2.3.1: {} + picomatch@2.3.2: {} - picomatch@4.0.3: {} + picomatch@4.0.4: {} pkg-types@1.3.1: dependencies: @@ -4284,7 +4646,7 @@ snapshots: mlly: 1.8.0 pathe: 2.0.3 - postcss@8.5.8: + postcss@8.5.12: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 @@ -4312,35 +4674,59 @@ snapshots: reusify@1.1.0: {} - rollup@4.59.0: + rolldown@1.0.0-rc.12(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1): + dependencies: + '@oxc-project/types': 0.122.0 + '@rolldown/pluginutils': 1.0.0-rc.12 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.12 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.12 + '@rolldown/binding-darwin-x64': 1.0.0-rc.12 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.12 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.12 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.12 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.12 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.12 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.12(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1) + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.12 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.12 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + + rollup@4.60.3: dependencies: '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.59.0 - '@rollup/rollup-android-arm64': 4.59.0 - '@rollup/rollup-darwin-arm64': 4.59.0 - '@rollup/rollup-darwin-x64': 4.59.0 - '@rollup/rollup-freebsd-arm64': 4.59.0 - '@rollup/rollup-freebsd-x64': 4.59.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 - '@rollup/rollup-linux-arm-musleabihf': 4.59.0 - '@rollup/rollup-linux-arm64-gnu': 4.59.0 - '@rollup/rollup-linux-arm64-musl': 4.59.0 - '@rollup/rollup-linux-loong64-gnu': 4.59.0 - '@rollup/rollup-linux-loong64-musl': 4.59.0 - '@rollup/rollup-linux-ppc64-gnu': 4.59.0 - '@rollup/rollup-linux-ppc64-musl': 4.59.0 - '@rollup/rollup-linux-riscv64-gnu': 4.59.0 - '@rollup/rollup-linux-riscv64-musl': 4.59.0 - '@rollup/rollup-linux-s390x-gnu': 4.59.0 - '@rollup/rollup-linux-x64-gnu': 4.59.0 - '@rollup/rollup-linux-x64-musl': 4.59.0 - '@rollup/rollup-openbsd-x64': 4.59.0 - '@rollup/rollup-openharmony-arm64': 4.59.0 - '@rollup/rollup-win32-arm64-msvc': 4.59.0 - '@rollup/rollup-win32-ia32-msvc': 4.59.0 - '@rollup/rollup-win32-x64-gnu': 4.59.0 - '@rollup/rollup-win32-x64-msvc': 4.59.0 + '@rollup/rollup-android-arm-eabi': 4.60.3 + '@rollup/rollup-android-arm64': 4.60.3 + '@rollup/rollup-darwin-arm64': 4.60.3 + '@rollup/rollup-darwin-x64': 4.60.3 + '@rollup/rollup-freebsd-arm64': 4.60.3 + '@rollup/rollup-freebsd-x64': 4.60.3 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.3 + '@rollup/rollup-linux-arm-musleabihf': 4.60.3 + '@rollup/rollup-linux-arm64-gnu': 4.60.3 + '@rollup/rollup-linux-arm64-musl': 4.60.3 + '@rollup/rollup-linux-loong64-gnu': 4.60.3 + '@rollup/rollup-linux-loong64-musl': 4.60.3 + '@rollup/rollup-linux-ppc64-gnu': 4.60.3 + '@rollup/rollup-linux-ppc64-musl': 4.60.3 + '@rollup/rollup-linux-riscv64-gnu': 4.60.3 + '@rollup/rollup-linux-riscv64-musl': 4.60.3 + '@rollup/rollup-linux-s390x-gnu': 4.60.3 + '@rollup/rollup-linux-x64-gnu': 4.60.3 + '@rollup/rollup-linux-x64-musl': 4.60.3 + '@rollup/rollup-openbsd-x64': 4.60.3 + '@rollup/rollup-openharmony-arm64': 4.60.3 + '@rollup/rollup-win32-arm64-msvc': 4.60.3 + '@rollup/rollup-win32-ia32-msvc': 4.60.3 + '@rollup/rollup-win32-x64-gnu': 4.60.3 + '@rollup/rollup-win32-x64-msvc': 4.60.3 fsevents: 2.3.3 run-parallel@1.2.0: @@ -4355,7 +4741,7 @@ snapshots: semver@7.7.4: {} - serialize-javascript@7.0.4: {} + serialize-javascript@7.0.5: {} sharp@0.34.5: dependencies: @@ -4417,30 +4803,42 @@ snapshots: stackback@0.0.2: {} - std-env@3.10.0: {} + std-env@4.1.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 supports-color@10.2.2: {} supports-preserve-symlinks-flag@1.0.0: {} - svelte@5.53.7: + svelte@5.55.7(@typescript-eslint/types@8.58.2): dependencies: '@jridgewell/remapping': 2.3.5 '@jridgewell/sourcemap-codec': 1.5.5 '@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.0) - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 '@types/trusted-types': 2.0.7 acorn: 8.16.0 aria-query: 5.3.1 axobject-query: 4.1.0 clsx: 2.1.1 - devalue: 5.6.3 + devalue: 5.8.1 esm-env: 1.2.2 - esrap: 2.2.3 + esrap: 2.2.8(@typescript-eslint/types@8.58.2) is-reference: 3.0.3 locate-character: 3.0.0 magic-string: 0.30.21 zimmerframe: 1.1.4 + transitivePeerDependencies: + - '@typescript-eslint/types' terser@5.46.0: dependencies: @@ -4451,14 +4849,14 @@ snapshots: tinybench@2.9.0: {} - tinyexec@1.0.2: {} + tinyexec@1.2.4: {} - tinyglobby@0.2.15: + tinyglobby@0.2.17: dependencies: - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 - tinyrainbow@3.0.3: {} + tinyrainbow@3.1.0: {} to-regex-range@5.0.1: dependencies: @@ -4466,9 +4864,9 @@ snapshots: totalist@3.0.1: {} - ts-api-utils@2.4.0(typescript@5.9.3): + ts-api-utils@2.5.0(typescript@6.0.2): dependencies: - typescript: 5.9.3 + typescript: 6.0.2 tslib@2.8.1: {} @@ -4480,18 +4878,18 @@ snapshots: type-level-regexp@0.1.17: {} - typescript-eslint@8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3): + typescript-eslint@8.58.2(eslint@10.0.2(jiti@2.6.1))(typescript@6.0.2): dependencies: - '@typescript-eslint/eslint-plugin': 8.56.1(@typescript-eslint/parser@8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': 8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.0.2(jiti@2.6.1))(typescript@6.0.2))(eslint@10.0.2(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/parser': 8.58.2(eslint@10.0.2(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.2) + '@typescript-eslint/utils': 8.58.2(eslint@10.0.2(jiti@2.6.1))(typescript@6.0.2) eslint: 10.0.2(jiti@2.6.1) - typescript: 5.9.3 + typescript: 6.0.2 transitivePeerDependencies: - supports-color - typescript@5.9.3: {} + typescript@6.0.2: {} ufo@1.6.3: {} @@ -4503,14 +4901,14 @@ snapshots: unconfig@7.5.0: dependencies: '@quansync/fs': 1.0.0 - defu: 6.1.4 + defu: 6.1.6 jiti: 2.6.1 quansync: 1.0.0 unconfig-core: 7.5.0 undici-types@7.16.0: {} - undici@7.18.2: {} + undici@7.24.4: {} unenv@2.0.0-rc.24: dependencies: @@ -4518,98 +4916,93 @@ snapshots: universal-user-agent@7.0.3: {} - unocss@66.6.5(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(terser@5.46.0)): - dependencies: - '@unocss/cli': 66.6.5 - '@unocss/core': 66.6.5 - '@unocss/preset-attributify': 66.6.5 - '@unocss/preset-icons': 66.6.5 - '@unocss/preset-mini': 66.6.5 - '@unocss/preset-tagify': 66.6.5 - '@unocss/preset-typography': 66.6.5 - '@unocss/preset-uno': 66.6.5 - '@unocss/preset-web-fonts': 66.6.5 - '@unocss/preset-wind': 66.6.5 - '@unocss/preset-wind3': 66.6.5 - '@unocss/preset-wind4': 66.6.5 - '@unocss/transformer-attributify-jsx': 66.6.5 - '@unocss/transformer-compile-class': 66.6.5 - '@unocss/transformer-directives': 66.6.5 - '@unocss/transformer-variant-group': 66.6.5 - '@unocss/vite': 66.6.5(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(terser@5.46.0)) + unocss@66.6.6(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(vite@8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@24.11.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0)): + dependencies: + '@unocss/cli': 66.6.6 + '@unocss/core': 66.6.6 + '@unocss/preset-attributify': 66.6.6 + '@unocss/preset-icons': 66.6.6 + '@unocss/preset-mini': 66.6.6 + '@unocss/preset-tagify': 66.6.6 + '@unocss/preset-typography': 66.6.6 + '@unocss/preset-uno': 66.6.6 + '@unocss/preset-web-fonts': 66.6.6 + '@unocss/preset-wind': 66.6.6 + '@unocss/preset-wind3': 66.6.6 + '@unocss/preset-wind4': 66.6.6 + '@unocss/transformer-attributify-jsx': 66.6.6(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1) + '@unocss/transformer-compile-class': 66.6.6 + '@unocss/transformer-directives': 66.6.6 + '@unocss/transformer-variant-group': 66.6.6 + '@unocss/vite': 66.6.6(vite@8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@24.11.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0)) transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' - vite unplugin-utils@0.3.1: dependencies: pathe: 2.0.3 - picomatch: 4.0.3 + picomatch: 4.0.4 unplugin@2.3.11: dependencies: '@jridgewell/remapping': 2.3.5 acorn: 8.16.0 - picomatch: 4.0.3 + picomatch: 4.0.4 webpack-virtual-modules: 0.6.2 uri-js@4.4.1: dependencies: punycode: 2.3.1 - vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(terser@5.46.0): + vite@8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@24.11.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0): dependencies: - esbuild: 0.27.3 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - postcss: 8.5.8 - rollup: 4.59.0 - tinyglobby: 0.2.15 + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.12 + rolldown: 1.0.0-rc.12(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1) + tinyglobby: 0.2.17 optionalDependencies: '@types/node': 24.11.0 + esbuild: 0.27.3 fsevents: 2.3.3 jiti: 2.6.1 terser: 5.46.0 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' - vitefu@1.1.2(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(terser@5.46.0)): + vitefu@1.1.2(vite@8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@24.11.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0)): optionalDependencies: - vite: 7.3.1(@types/node@24.11.0)(jiti@2.6.1)(terser@5.46.0) - - vitest@4.0.18(@types/node@24.11.0)(jiti@2.6.1)(terser@5.46.0): - dependencies: - '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(terser@5.46.0)) - '@vitest/pretty-format': 4.0.18 - '@vitest/runner': 4.0.18 - '@vitest/snapshot': 4.0.18 - '@vitest/spy': 4.0.18 - '@vitest/utils': 4.0.18 - es-module-lexer: 1.7.0 + vite: 8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@24.11.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0) + + vitest@4.1.0(@types/node@24.11.0)(vite@8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@24.11.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0)): + dependencies: + '@vitest/expect': 4.1.0 + '@vitest/mocker': 4.1.0(vite@8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@24.11.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0)) + '@vitest/pretty-format': 4.1.0 + '@vitest/runner': 4.1.0 + '@vitest/snapshot': 4.1.0 + '@vitest/spy': 4.1.0 + '@vitest/utils': 4.1.0 + es-module-lexer: 2.1.0 expect-type: 1.3.0 magic-string: 0.30.21 obug: 2.1.1 pathe: 2.0.3 - picomatch: 4.0.3 - std-env: 3.10.0 + picomatch: 4.0.4 + std-env: 4.1.0 tinybench: 2.9.0 - tinyexec: 1.0.2 - tinyglobby: 0.2.15 - tinyrainbow: 3.0.3 - vite: 7.3.1(@types/node@24.11.0)(jiti@2.6.1)(terser@5.46.0) + tinyexec: 1.2.4 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 + vite: 8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@24.11.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.11.0 transitivePeerDependencies: - - jiti - - less - - lightningcss - msw - - sass - - sass-embedded - - stylus - - sugarss - - terser - - tsx - - yaml webpack-virtual-modules@0.6.2: {} @@ -4624,34 +5017,42 @@ snapshots: word-wrap@1.2.5: {} - workerd@1.20260301.1: + workerd@1.20260317.1: optionalDependencies: - '@cloudflare/workerd-darwin-64': 1.20260301.1 - '@cloudflare/workerd-darwin-arm64': 1.20260301.1 - '@cloudflare/workerd-linux-64': 1.20260301.1 - '@cloudflare/workerd-linux-arm64': 1.20260301.1 - '@cloudflare/workerd-windows-64': 1.20260301.1 + '@cloudflare/workerd-darwin-64': 1.20260317.1 + '@cloudflare/workerd-darwin-arm64': 1.20260317.1 + '@cloudflare/workerd-linux-64': 1.20260317.1 + '@cloudflare/workerd-linux-arm64': 1.20260317.1 + '@cloudflare/workerd-windows-64': 1.20260317.1 - wrangler@4.70.0: + wrangler@4.75.0: dependencies: '@cloudflare/kv-asset-handler': 0.4.2 - '@cloudflare/unenv-preset': 2.14.0(unenv@2.0.0-rc.24)(workerd@1.20260301.1) + '@cloudflare/unenv-preset': 2.15.0(unenv@2.0.0-rc.24)(workerd@1.20260317.1) blake3-wasm: 2.1.5 esbuild: 0.27.3 - miniflare: 4.20260301.1 + miniflare: 4.20260317.0 path-to-regexp: 6.3.0 unenv: 2.0.0-rc.24 - workerd: 1.20260301.1 + workerd: 1.20260317.1 optionalDependencies: fsevents: 2.3.3 transitivePeerDependencies: - bufferutil - utf-8-validate - ws@8.18.0: {} + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + ws@8.20.1: {} yocto-queue@0.1.0: {} + yoctocolors-cjs@2.1.3: {} + youch-core@0.3.3: dependencies: '@poppinss/exception': 1.2.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index f0a696121145..04054138f8ce 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -9,3 +9,6 @@ packages: onlyBuiltDependencies: - esbuild - workerd + +overrides: + ws@>=8.0.0 <8.20.1: '>=8.20.1'