diff --git a/.claude/skills/ohos-build/scripts/build-ohos.sh b/.claude/skills/ohos-build/scripts/build-ohos.sh index 474f9d420b90..10c109639938 100644 --- a/.claude/skills/ohos-build/scripts/build-ohos.sh +++ b/.claude/skills/ohos-build/scripts/build-ohos.sh @@ -10,11 +10,14 @@ source "$SCRIPT_DIR/env.sh" API_DIR="$PROJECT_ROOT/examples/api" SRC_TAURI="$API_DIR/src-tauri" OHOS_PROJECT="$SRC_TAURI/gen/ohos" -# PR #59: entry module is now entry_desktop or entry_mobile based on OHOS_DEVICE_TYPE -ENTRY_MODULE="entry_${OHOS_DEVICE_TYPE:-desktop}" -SIGNED_HAP="$OHOS_PROJECT/${ENTRY_MODULE}/build/default/outputs/default/${ENTRY_MODULE}-default-signed.hap" +if [ "$OHOS_DEVICE_TYPE" = "desktop" ]; then + ENTRY_DIR="entry_desktop" +else + ENTRY_DIR="entry_mobile" +fi +SIGNED_HAP="$OHOS_PROJECT/$ENTRY_DIR/build/default/outputs/default/$ENTRY_DIR-default-signed.hap" SO_FILE="$PROJECT_ROOT/target/aarch64-unknown-linux-ohos/release/libapi_lib.so" -HVIGORFILE="$OHOS_PROJECT/${ENTRY_MODULE}/hvigorfile.ts" +HVIGORFILE="$OHOS_PROJECT/$ENTRY_DIR/hvigorfile.ts" echo "=== Tauri OpenHarmony Build ===" echo "DEVECO_HOME=$DEVECO_HOME" @@ -24,7 +27,7 @@ echo "" # ─── Step 0: Detect template changes and re-run `tauri ohos init` ─── TEMPLATE_DIR="$PROJECT_ROOT/crates/tauri-cli/templates/mobile/open-harmony" -ENTRY_ETS="$OHOS_PROJECT/${ENTRY_MODULE}/src/main/ets/entryability/EntryAbility.ets" +ENTRY_ETS="$OHOS_PROJECT/$ENTRY_DIR/src/main/ets/entryability/EntryAbility.ets" NEED_INIT=false if [ ! -f "$ENTRY_ETS" ]; then @@ -100,8 +103,8 @@ echo " Generated: $SO_FILE" # ─── Step 5: 拷贝 .so 到 ohos 项目 ─── echo "" echo ">>> Step 5: Copying .so to ohos project..." -mkdir -p "$OHOS_PROJECT/${ENTRY_MODULE}/libs/arm64-v8a" -cp "$SO_FILE" "$OHOS_PROJECT/${ENTRY_MODULE}/libs/arm64-v8a/libapi_lib.so" +mkdir -p "$OHOS_PROJECT/$ENTRY_DIR/libs/arm64-v8a" +cp "$SO_FILE" "$OHOS_PROJECT/$ENTRY_DIR/libs/arm64-v8a/libapi_lib.so" # ─── Step 6: hvigorw 打包(自动禁用/恢复 tauriPlugin)─── echo "" @@ -116,7 +119,7 @@ else fi rm -f "$SIGNED_HAP" -(cd "$OHOS_PROJECT" && hvigorw --no-daemon -p product=default -p module=${ENTRY_MODULE}@default assembleHap --analyze=normal --parallel --incremental) || HVIGOR_EXIT=$? +(cd "$OHOS_PROJECT" && hvigorw --no-daemon -p product=default -p module=$ENTRY_DIR@default assembleHap --analyze=normal --parallel --incremental) || HVIGOR_EXIT=$? HVIGOR_EXIT=${HVIGOR_EXIT:-0} # 恢复 tauriPlugin diff --git a/.claude/skills/ohos-build/scripts/sign-and-install.sh b/.claude/skills/ohos-build/scripts/sign-and-install.sh index 56af9ffab1cf..3e73f7a20cb4 100644 --- a/.claude/skills/ohos-build/scripts/sign-and-install.sh +++ b/.claude/skills/ohos-build/scripts/sign-and-install.sh @@ -9,7 +9,12 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/env.sh" OHOS_PROJECT="$PROJECT_ROOT/examples/api/src-tauri/gen/ohos" -SIGNED_HAP="$OHOS_PROJECT/entry/build/default/outputs/default/entry-default-signed.hap" +if [ "$OHOS_DEVICE_TYPE" = "desktop" ]; then + ENTRY_DIR="entry_desktop" +else + ENTRY_DIR="entry_mobile" +fi +SIGNED_HAP="$OHOS_PROJECT/$ENTRY_DIR/build/default/outputs/default/$ENTRY_DIR-default-signed.hap" # ─── 检查已签名 HAP ─── if [ ! -f "$SIGNED_HAP" ]; then diff --git a/.claude/skills/tauri-ohos-design/references/ohos-constraints.md b/.claude/skills/tauri-ohos-design/references/ohos-constraints.md index 58392b236570..09c36f467027 100644 --- a/.claude/skills/tauri-ohos-design/references/ohos-constraints.md +++ b/.claude/skills/tauri-ohos-design/references/ohos-constraints.md @@ -60,6 +60,7 @@ | TSFN 回调必须返回 **参数元组**, 不是 `Result<()>` | 返回 `()` = 空 JS 参数 (全部 `undefined`)。返回 `FnArgs { data: (arg1, arg2) }` | | **禁止** 使用 `callee_handled::()` | napi-ohos 在 `CalleeHandled=true` 时自动在首位插入 `null`, 导致参数偏移。必须用 `callee_handled::()` | | 裸 tuple 类型会序列化为 JS Array | 必须用 `FnArgs<>` 包装 tuple, 否则 JS 函数收到数组而非展开参数 | +| **`Function::call` 也有同样 bug** | `func.call((arg1, arg2))` 裸 tuple 走通用 impl 只传 1 个参数。必须 `Function<'_, FnArgs<(T1,T2)>, R>` + `func.call(FnArgs { data: (arg1, arg2) })`。p1-window-vibrancy 的 set_window_blur 因此从未工作过 | | TSFN 数据必须通过泛型参数携带, 不是全局 Mutex | 全局 `Mutex>` 中转模式在快速连续调用时产生数据竞态, 导致 freeze。每个 TSFN 调用独立 Box 入队, 天然隔离 | ### 2.3 NAPI 上下文限制 @@ -70,6 +71,7 @@ | `statusBarManager.on()` 必须在 `addToStatusBar` 之后 200ms 注册 | OHOS 内部 `ScbServerReceiver` 在 `addToStatusBar` 后异步初始化。提前注册的 handler 被静默丢弃 | | NAPI `Env` 只在获取它的线程有效 | `MAIN_THREAD_ENV` 存储在 `thread_local!` 中, 其他线程调用 `get_main_thread_env()` 返回 `None` | | `ObjectRef` (napi_ref) 不是 Send/Sync | 必须通过 `Mutex` + `ptr::read` 跨线程共享, `unsafe impl Send/Sync` | +| **hilog 在 NAPI 回调上下文抛 Argc mismatch** | 被 Rust NAPI `func.call` 调的 ArkTS 函数内部用 `hilog.info`/`hilog.error` 会抛 `"assertion (false) failed: Argc mismatch"`(疑 NAPI 重入限制)。异常被 catch 吞成 `failed: {}`。被 NAPI 调的函数内部禁用 hilog;纯 ArkTS 调用链(如 registerController)里 hilog 正常 | --- @@ -122,6 +124,7 @@ | 模块级 `@Builder function` 没有 `this` 上下文 | 全局 `@Builder` 无法访问组件实例属性和方法。只有 `@Component` 内的 `private @Builder` 方法才有 `this` | | 递归 `@Builder`(如子菜单渲染)必须在 `@Component` 内 | 模块级 `@Builder` 调用其他 `@Builder` 时, `this` 为 `undefined`, 导致 `TypeError`。这是 menu Phase 4→6→9 三次方案演进的根本原因 | | WebView 事件必须在 `@Builder` 内 pre-build 注册 | ArkUI 约束: 事件回调不能在 `@Builder` 外部动态绑定。所有 `onLoadIntercept`、`onPageBegin` 等必须在构建时注册 | +| **`BuilderNode.update` 不刷新组件属性** | `.backdropBlur(data.style.blurRadius)` 等属性在 update 时不重新求值。build 时通过 `addWebview` 注入值;**运行时刷新用 `AttributeUpdater`**:`modifier.attribute?.backdropBlur(radius)` 立即触发组件更新(不需 @State, 适合 @Builder/BuilderNode)。vibrancy BlurModifier 用此机制刷新 backdropBlur/backgroundColor | ### 4.2 语义反转 diff --git a/Cargo.toml b/Cargo.toml index 31ac49608720..577cc7a5d785 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,82 +1,83 @@ -[workspace] -members = [ - "crates/tauri", - "crates/tauri-runtime", - "crates/tauri-runtime-wry", - "crates/tauri-macros", - "crates/tauri-utils", - "crates/tauri-build", - "crates/tauri-codegen", - "crates/tauri-plugin", - "crates/tauri-schema-generator", - "crates/tauri-schema-worker", - "crates/tauri-cli", - "crates/tauri-bundler", - "crates/tauri-macos-sign", - "crates/tauri-driver", - - # @tauri-apps/cli rust project - "packages/cli", - - # integration tests - "crates/tests/restart", - "crates/tests/acl", - - # bench - "bench", - "bench/tests/cpu_intensive/src-tauri", - "bench/tests/files_transfer/src-tauri", - "bench/tests/helloworld/src-tauri", - - # examples - "examples/file-associations/src-tauri", - "examples/resources/src-tauri", - "examples/api/src-tauri", - "examples/api/src-tauri/tauri-plugin-sample", -] -resolver = "2" - -[workspace.package] -authors = ["Tauri Programme within The Commons Conservancy"] -homepage = "https://tauri.app/" -repository = "https://github.com/tauri-apps/tauri" -categories = ["gui", "web-programming"] -license = "Apache-2.0 OR MIT" -edition = "2021" -rust-version = "1.77.2" - -# default to small, optimized workspace release binaries -[profile.release] -panic = "abort" -codegen-units = 1 -lto = true -incremental = false -opt-level = "s" -strip = true - -# profiles for tauri-cli -[profile.dev.package.miniz_oxide] -opt-level = 3 - -[profile.release-size-optimized] -inherits = "release" -codegen-units = 1 -lto = true -incremental = false -opt-level = "s" - -# Temporary patch to schemars to preserve newlines in docstrings for our reference docs schemas -# See https://github.com/GREsau/schemars/issues/120 for reference -[patch.crates-io] -schemars_derive = { git = 'https://github.com/tauri-apps/schemars.git', branch = 'feat/preserve-description-newlines' } -tauri = { path = "./crates/tauri" } -tauri-plugin = { path = "./crates/tauri-plugin" } -tauri-utils = { path = "./crates/tauri-utils" } -cargo-mobile2 = { path = "../cargo-mobile2", default-features = false } -wry = { path = "../wry" } -#tao = { git = "https://github.com/richerfu/tao", branch = "feat-ohos-webview" } -tao = { path = "../tao" } -muda = { path = "../muda" } -tray-icon = { path = "../tray-icon" } -openharmony-ability = { path = "../openharmony-ability/crates/ability" } -openharmony-ability-derive = { path = "../openharmony-ability/crates/derive" } \ No newline at end of file +[workspace] +members = [ + "crates/tauri", + "crates/tauri-runtime", + "crates/tauri-runtime-wry", + "crates/tauri-macros", + "crates/tauri-utils", + "crates/tauri-build", + "crates/tauri-codegen", + "crates/tauri-plugin", + "crates/tauri-schema-generator", + "crates/tauri-schema-worker", + "crates/tauri-cli", + "crates/tauri-bundler", + "crates/tauri-macos-sign", + "crates/tauri-driver", + + # @tauri-apps/cli rust project + "packages/cli", + + # integration tests + "crates/tests/restart", + "crates/tests/acl", + + # bench + "bench", + "bench/tests/cpu_intensive/src-tauri", + "bench/tests/files_transfer/src-tauri", + "bench/tests/helloworld/src-tauri", + + # examples + "examples/file-associations/src-tauri", + "examples/resources/src-tauri", + "examples/api/src-tauri", + "examples/api/src-tauri/tauri-plugin-sample", +] +resolver = "2" + +[workspace.package] +authors = ["Tauri Programme within The Commons Conservancy"] +homepage = "https://tauri.app/" +repository = "https://github.com/tauri-apps/tauri" +categories = ["gui", "web-programming"] +license = "Apache-2.0 OR MIT" +edition = "2021" +rust-version = "1.77.2" + +# default to small, optimized workspace release binaries +[profile.release] +panic = "abort" +codegen-units = 1 +lto = true +incremental = false +opt-level = "s" +strip = true + +# profiles for tauri-cli +[profile.dev.package.miniz_oxide] +opt-level = 3 + +[profile.release-size-optimized] +inherits = "release" +codegen-units = 1 +lto = true +incremental = false +opt-level = "s" + +# Temporary patch to schemars to preserve newlines in docstrings for our reference docs schemas +# See https://github.com/GREsau/schemars/issues/120 for reference +[patch.crates-io] +schemars_derive = { git = 'https://github.com/tauri-apps/schemars.git', branch = 'feat/preserve-description-newlines' } +tauri = { path = "./crates/tauri" } +tauri-plugin = { path = "./crates/tauri-plugin" } +tauri-utils = { path = "./crates/tauri-utils" } +cargo-mobile2 = { path = "../cargo-mobile2", default-features = false } +wry = { path = "../wry" } +#tao = { git = "https://github.com/richerfu/tao", branch = "feat-ohos-webview" } +tao = { path = "../tao" } +muda = { path = "../muda" } +tray-icon = { path = "../tray-icon" } +openharmony-ability = { path = "../openharmony-ability/crates/ability" } +openharmony-ability-derive = { path = "../openharmony-ability/crates/derive" } +window-vibrancy = { path = "../window-vibrancy" } diff --git a/crates/tauri-runtime-wry/src/lib.rs b/crates/tauri-runtime-wry/src/lib.rs index fbe4f08a8925..8d3e85406ed3 100644 --- a/crates/tauri-runtime-wry/src/lib.rs +++ b/crates/tauri-runtime-wry/src/lib.rs @@ -1554,6 +1554,8 @@ pub enum WindowMessage { DragWindow, ResizeDragWindow(tauri_runtime::ResizeDirection), RequestRedraw, + #[cfg(target_env = "ohos")] + OhosWindowId(Sender>), } #[derive(Debug, Clone)] @@ -2705,6 +2707,11 @@ impl WindowDispatch for WryWindowDispatcher { Message::Window(self.window_id, WindowMessage::SetBackgroundColor(color)), ) } + + #[cfg(target_env = "ohos")] + fn ohos_window_id(&self) -> Result> { + window_getter!(self, WindowMessage::OhosWindowId) + } } #[derive(Clone)] @@ -3901,6 +3908,11 @@ fn handle_user_message( WindowMessage::SetBackgroundColor(color) => { window.set_background_color(color.map(Into::into)) } + #[cfg(target_env = "ohos")] + WindowMessage::OhosWindowId(tx) => { + use tao::platform::ohos::WindowExtOpenHarmony; + let _ = tx.send(window.window_id()); + } } } } diff --git a/crates/tauri-runtime/src/lib.rs b/crates/tauri-runtime/src/lib.rs index b535484cdad1..27ba03f2dba4 100644 --- a/crates/tauri-runtime/src/lib.rs +++ b/crates/tauri-runtime/src/lib.rs @@ -926,6 +926,10 @@ pub trait WindowDispatch: Debug + Clone + Send + Sync + Sized + 's /// Set the window background. fn set_background_color(&self, color: Option) -> Result<()>; + /// Returns the OHOS OS-level window ID (0 = main window, positive = sub-window). + #[cfg(target_env = "ohos")] + fn ohos_window_id(&self) -> Result>; + /// Prevents the window contents from being captured by other apps. fn set_content_protected(&self, protected: bool) -> Result<()>; diff --git a/crates/tauri/Cargo.toml b/crates/tauri/Cargo.toml index 8e62f583407c..d29108813add 100644 --- a/crates/tauri/Cargo.toml +++ b/crates/tauri/Cargo.toml @@ -1,287 +1,286 @@ -[package] -name = "tauri" -version = "2.10.3" -description = "Make tiny, secure apps for all desktop platforms with Tauri" -exclude = ["/test", "/.scripts", "CHANGELOG.md", "/target"] -readme = "README.md" -links = "Tauri" -authors.workspace = true -homepage.workspace = true -repository.workspace = true -categories.workspace = true -license.workspace = true -edition.workspace = true -rust-version.workspace = true - -[package.metadata.docs.rs] -no-default-features = true -features = [ - "wry", - "unstable", - "custom-protocol", - "tray-icon", - "devtools", - "image-png", - "protocol-asset", - "test", - "specta", - "dynamic-acl", -] -default-target = "x86_64-unknown-linux-gnu" -targets = [ - "x86_64-pc-windows-msvc", - "x86_64-unknown-linux-gnu", - "x86_64-apple-darwin", - "x86_64-linux-android", - "x86_64-apple-ios", -] - -[package.metadata.cargo-udeps.ignore] -normal = ["reqwest"] -build = ["tauri-build"] -development = ["quickcheck_macros"] - -[dependencies] -serde_json = { version = "1", features = ["raw_value"] } -serde = { version = "1", features = ["derive", "rc"] } -tokio = { version = "1", features = [ - "rt", - "rt-multi-thread", - "sync", - "fs", - "io-util", -] } -uuid = { version = "1", features = ["v4"], optional = true } -url = "2" -anyhow = "1" -thiserror = "2" -tauri-runtime = { version = "2.10.1", path = "../tauri-runtime" } -tauri-macros = { version = "2.5.5", path = "../tauri-macros" } -tauri-utils = { version = "2.8.3", features = [ - "resources", -], path = "../tauri-utils" } -tauri-runtime-wry = { version = "2.10.1", path = "../tauri-runtime-wry", default-features = false, optional = true } -getrandom = "0.3" -serde_repr = "0.1" -http = "1" -dirs = "6" -percent-encoding = "2" -raw-window-handle = { version = "0.6", features = ["std"] } -glob = "0.3" -mime = "0.3" -data-url = { version = "0.3", optional = true } -serialize-to-javascript = "0.1.2" -image = { version = "0.25", default-features = false, optional = true } -http-range = { version = "0.1", optional = true } -tracing = { version = "0.1", optional = true } -heck = "0.5" -log = "0.4.21" -dunce = "1" -specta = { version = "^2.0.0-rc.16", optional = true, default-features = false, features = [ - "function", - "derive", -] } -# WARNING: cookie::Cookie is re-exported so bumping this is a breaking change, documented to be done as a minor bump -cookie = "0.18" - -# desktop (exclude ohos) -[target.'cfg(all(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd", target_os = "windows", target_os = "macos"), not(target_env = "ohos")))'.dependencies] -muda = { path = "../../../muda", default-features = false, features = [ - "serde", - "gtk", -] } -tray-icon = { path = "../../../tray-icon", default-features = false, features = [ - "serde", -], optional = true } - -# linux -[target.'cfg(all(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"), not(target_env = "ohos")))'.dependencies] -gtk = { version = "0.18", features = ["v3_24"] } -webkit2gtk = { version = "=2.0", features = ["v2_40"], optional = true } - -# darwin -[target.'cfg(target_vendor = "apple")'.dependencies] -objc2 = "0.6" - -# macOS -[target.'cfg(target_os = "macos")'.dependencies] -embed_plist = "1.2" -plist = "1" -objc2-foundation = { version = "0.3", default-features = false, features = [ - "std", - "NSData", - "NSThread", -] } -objc2-app-kit = { version = "0.3", default-features = false, features = [ - "std", - "NSApplication", - "NSColor", - "NSResponder", - "NSView", - "NSWindow", - "NSImage", -] } -objc2-web-kit = { version = "0.3", default-features = false, features = [ - "objc2-app-kit", - "WKWebView", - "WKWebViewConfiguration", - "WKUserContentController", -] } -window-vibrancy = "0.6" - -# windows -[target."cfg(windows)".dependencies] -webview2-com = { version = "0.38", optional = true } -window-vibrancy = "0.6" -windows = { version = "0.61", features = [ - "Win32_Foundation", - "Win32_UI", - "Win32_UI_WindowsAndMessaging", -] } - -# mobile -[target.'cfg(any(target_os = "android", target_env = "ohos", all(target_vendor = "apple", not(target_os = "macos"))))'.dependencies] -bytes = { version = "1", features = ["serde"] } -reqwest = { version = "0.13", default-features = false, features = [ - "json", - "stream", -] } -rustls = { version = "0.23", default-features = false, features = [ - "ring", -], optional = true } - -[target.'cfg(target_env = "ohos")'.dependencies] -muda = { path = "../../../muda", default-features = false, features = [ - "serde", -] } -tray-icon = { path = "../../../tray-icon", default-features = false, features = [ - "serde", -], optional = true } -openharmony-ability = { path = "../../../openharmony-ability/crates/ability", features = ["webview", "menu"] } -openharmony-ability-derive = { path = "../../../openharmony-ability/crates/derive" } -napi-ohos = "1" -napi-derive-ohos = "1" - -# android -[target.'cfg(target_os = "android")'.dependencies] -jni = "0.21" - -# UIKit, i.e. iOS/tvOS/watchOS/visionOS -[target.'cfg(all(target_vendor = "apple", not(target_os = "macos")))'.dependencies] -libc = "0.2" -swift-rs = "1" -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-2", -] } - -[dev-dependencies] -proptest = "1.6.0" -quickcheck = "1.0.3" -quickcheck_macros = "1.0.0" -serde = { version = "1", features = ["derive"] } -serde_json = "1" -tauri = { path = ".", default-features = false, features = ["wry"] } -tokio = { version = "1", features = ["full"] } -cargo_toml = "0.22" -http-range = "0.1.5" - -[features] -default = [ - "wry", - "compression", - "common-controls-v6", - "dynamic-acl", - "x11", - "dbus", -] -unstable = ["tauri-runtime-wry?/unstable"] -x11 = ["tauri-runtime-wry?/x11"] -dbus = ["tauri-runtime-wry?/dbus"] -common-controls-v6 = [ - "tray-icon?/common-controls-v6", - "muda/common-controls-v6", - "tauri-runtime-wry?/common-controls-v6", -] -tray-icon = ["dep:tray-icon"] -tracing = ["dep:tracing", "tauri-macros/tracing", "tauri-runtime-wry?/tracing"] -test = [] -compression = ["tauri-macros/compression", "tauri-utils/compression"] -wry = ["webview2-com", "webkit2gtk", "tauri-runtime-wry"] -# TODO: Remove in v3 - wry does not have this feature anymore -objc-exception = [] -linux-libxdo = ["tray-icon/libxdo", "muda/libxdo"] -isolation = ["tauri-utils/isolation", "tauri-macros/isolation", "uuid"] -custom-protocol = ["tauri-macros/custom-protocol"] -# TODO: Remove these flags in v3 and/or enable them by default behind a mobile flag https://github.com/tauri-apps/tauri/issues/12384 -native-tls = ["reqwest/native-tls"] -native-tls-vendored = ["reqwest/native-tls-vendored"] -rustls-tls = ["reqwest/rustls-no-provider", "dep:rustls"] -devtools = ["tauri-runtime/devtools", "tauri-runtime-wry?/devtools"] -process-relaunch-dangerous-allow-symlink-macos = [ - "tauri-utils/process-relaunch-dangerous-allow-symlink-macos", -] -macos-private-api = [ - "tauri-runtime/macos-private-api", - "tauri-runtime-wry?/macos-private-api", -] -webview-data-url = ["data-url", "tauri-utils/html-manipulation-2"] -protocol-asset = ["http-range"] -config-json5 = ["tauri-macros/config-json5"] -config-toml = ["tauri-macros/config-toml"] -image-ico = ["image/ico"] -image-png = ["image/png"] -macos-proxy = ["tauri-runtime-wry?/macos-proxy"] -dynamic-acl = [] -specta = ["dep:specta"] - -[[example]] -name = "commands" -path = "../../examples/commands/main.rs" - -[[example]] -name = "helloworld" -path = "../../examples/helloworld/main.rs" - -[[example]] -name = "drag" -path = "../../examples/drag/main.rs" - -[[example]] -name = "multiwebview" -path = "../../examples/multiwebview/main.rs" -required-features = ["unstable"] - -[[example]] -name = "multiwindow" -path = "../../examples/multiwindow/main.rs" - -[[example]] -name = "run-return" -path = "../../examples/run-return/main.rs" - -[[example]] -name = "splashscreen" -path = "../../examples/splashscreen/main.rs" - -[[example]] -name = "state" -path = "../../examples/state/main.rs" - -[[example]] -name = "streaming" -path = "../../examples/streaming/main.rs" - -[[example]] -name = "isolation" -path = "../../examples/isolation/main.rs" -required-features = ["isolation"] +[package] +name = "tauri" +version = "2.10.3" +description = "Make tiny, secure apps for all desktop platforms with Tauri" +exclude = ["/test", "/.scripts", "CHANGELOG.md", "/target"] +readme = "README.md" +links = "Tauri" +authors.workspace = true +homepage.workspace = true +repository.workspace = true +categories.workspace = true +license.workspace = true +edition.workspace = true +rust-version.workspace = true + +[package.metadata.docs.rs] +no-default-features = true +features = [ + "wry", + "unstable", + "custom-protocol", + "tray-icon", + "devtools", + "image-png", + "protocol-asset", + "test", + "specta", + "dynamic-acl", +] +default-target = "x86_64-unknown-linux-gnu" +targets = [ + "x86_64-pc-windows-msvc", + "x86_64-unknown-linux-gnu", + "x86_64-apple-darwin", + "x86_64-linux-android", + "x86_64-apple-ios", +] + +[package.metadata.cargo-udeps.ignore] +normal = ["reqwest"] +build = ["tauri-build"] +development = ["quickcheck_macros"] + +[dependencies] +serde_json = { version = "1", features = ["raw_value"] } +serde = { version = "1", features = ["derive", "rc"] } +tokio = { version = "1", features = [ + "rt", + "rt-multi-thread", + "sync", + "fs", + "io-util", +] } +uuid = { version = "1", features = ["v4"], optional = true } +url = "2" +anyhow = "1" +thiserror = "2" +tauri-runtime = { version = "2.10.1", path = "../tauri-runtime" } +tauri-macros = { version = "2.5.5", path = "../tauri-macros" } +tauri-utils = { version = "2.8.3", features = [ + "resources", +], path = "../tauri-utils" } +tauri-runtime-wry = { version = "2.10.1", path = "../tauri-runtime-wry", default-features = false, optional = true } +window-vibrancy = "0.7" +getrandom = "0.3" +serde_repr = "0.1" +http = "1" +dirs = "6" +percent-encoding = "2" +raw-window-handle = { version = "0.6", features = ["std"] } +glob = "0.3" +mime = "0.3" +data-url = { version = "0.3", optional = true } +serialize-to-javascript = "0.1.2" +image = { version = "0.25", default-features = false, optional = true } +http-range = { version = "0.1", optional = true } +tracing = { version = "0.1", optional = true } +heck = "0.5" +log = "0.4.21" +dunce = "1" +specta = { version = "^2.0.0-rc.16", optional = true, default-features = false, features = [ + "function", + "derive", +] } +# WARNING: cookie::Cookie is re-exported so bumping this is a breaking change, documented to be done as a minor bump +cookie = "0.18" + +# desktop (exclude ohos) +[target.'cfg(all(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd", target_os = "windows", target_os = "macos"), not(target_env = "ohos")))'.dependencies] +muda = { path = "../../../muda", default-features = false, features = [ + "serde", + "gtk", +] } +tray-icon = { path = "../../../tray-icon", default-features = false, features = [ + "serde", +], optional = true } + +# linux +[target.'cfg(all(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"), not(target_env = "ohos")))'.dependencies] +gtk = { version = "0.18", features = ["v3_24"] } +webkit2gtk = { version = "=2.0", features = ["v2_40"], optional = true } + +# darwin +[target.'cfg(target_vendor = "apple")'.dependencies] +objc2 = "0.6" + +# macOS +[target.'cfg(target_os = "macos")'.dependencies] +embed_plist = "1.2" +plist = "1" +objc2-foundation = { version = "0.3", default-features = false, features = [ + "std", + "NSData", + "NSThread", +] } +objc2-app-kit = { version = "0.3", default-features = false, features = [ + "std", + "NSApplication", + "NSColor", + "NSResponder", + "NSView", + "NSWindow", + "NSImage", +] } +objc2-web-kit = { version = "0.3", default-features = false, features = [ + "objc2-app-kit", + "WKWebView", + "WKWebViewConfiguration", + "WKUserContentController", +] } + +# windows +[target."cfg(windows)".dependencies] +webview2-com = { version = "0.38", optional = true } +windows = { version = "0.61", features = [ + "Win32_Foundation", + "Win32_UI", + "Win32_UI_WindowsAndMessaging", +] } + +# mobile +[target.'cfg(any(target_os = "android", target_env = "ohos", all(target_vendor = "apple", not(target_os = "macos"))))'.dependencies] +bytes = { version = "1", features = ["serde"] } +reqwest = { version = "0.13", default-features = false, features = [ + "json", + "stream", +] } +rustls = { version = "0.23", default-features = false, features = [ + "ring", +], optional = true } + +[target.'cfg(target_env = "ohos")'.dependencies] +muda = { path = "../../../muda", default-features = false, features = [ + "serde", +] } +tray-icon = { path = "../../../tray-icon", default-features = false, features = [ + "serde", +], optional = true } +openharmony-ability = { path = "../../../openharmony-ability/crates/ability", features = ["webview", "menu"] } +openharmony-ability-derive = { path = "../../../openharmony-ability/crates/derive" } +napi-ohos = "1" +napi-derive-ohos = "1" + +# android +[target.'cfg(target_os = "android")'.dependencies] +jni = "0.21" + +# UIKit, i.e. iOS/tvOS/watchOS/visionOS +[target.'cfg(all(target_vendor = "apple", not(target_os = "macos")))'.dependencies] +libc = "0.2" +swift-rs = "1" +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-2", +] } + +[dev-dependencies] +proptest = "1.6.0" +quickcheck = "1.0.3" +quickcheck_macros = "1.0.0" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tauri = { path = ".", default-features = false, features = ["wry"] } +tokio = { version = "1", features = ["full"] } +cargo_toml = "0.22" +http-range = "0.1.5" + +[features] +default = [ + "wry", + "compression", + "common-controls-v6", + "dynamic-acl", + "x11", + "dbus", +] +unstable = ["tauri-runtime-wry?/unstable"] +x11 = ["tauri-runtime-wry?/x11"] +dbus = ["tauri-runtime-wry?/dbus"] +common-controls-v6 = [ + "tray-icon?/common-controls-v6", + "muda/common-controls-v6", + "tauri-runtime-wry?/common-controls-v6", +] +tray-icon = ["dep:tray-icon"] +tracing = ["dep:tracing", "tauri-macros/tracing", "tauri-runtime-wry?/tracing"] +test = [] +compression = ["tauri-macros/compression", "tauri-utils/compression"] +wry = ["webview2-com", "webkit2gtk", "tauri-runtime-wry"] +# TODO: Remove in v3 - wry does not have this feature anymore +objc-exception = [] +linux-libxdo = ["tray-icon/libxdo", "muda/libxdo"] +isolation = ["tauri-utils/isolation", "tauri-macros/isolation", "uuid"] +custom-protocol = ["tauri-macros/custom-protocol"] +# TODO: Remove these flags in v3 and/or enable them by default behind a mobile flag https://github.com/tauri-apps/tauri/issues/12384 +native-tls = ["reqwest/native-tls"] +native-tls-vendored = ["reqwest/native-tls-vendored"] +rustls-tls = ["reqwest/rustls-no-provider", "dep:rustls"] +devtools = ["tauri-runtime/devtools", "tauri-runtime-wry?/devtools"] +process-relaunch-dangerous-allow-symlink-macos = [ + "tauri-utils/process-relaunch-dangerous-allow-symlink-macos", +] +macos-private-api = [ + "tauri-runtime/macos-private-api", + "tauri-runtime-wry?/macos-private-api", +] +webview-data-url = ["data-url", "tauri-utils/html-manipulation-2"] +protocol-asset = ["http-range"] +config-json5 = ["tauri-macros/config-json5"] +config-toml = ["tauri-macros/config-toml"] +image-ico = ["image/ico"] +image-png = ["image/png"] +macos-proxy = ["tauri-runtime-wry?/macos-proxy"] +dynamic-acl = [] +specta = ["dep:specta"] + +[[example]] +name = "commands" +path = "../../examples/commands/main.rs" + +[[example]] +name = "helloworld" +path = "../../examples/helloworld/main.rs" + +[[example]] +name = "drag" +path = "../../examples/drag/main.rs" + +[[example]] +name = "multiwebview" +path = "../../examples/multiwebview/main.rs" +required-features = ["unstable"] + +[[example]] +name = "multiwindow" +path = "../../examples/multiwindow/main.rs" + +[[example]] +name = "run-return" +path = "../../examples/run-return/main.rs" + +[[example]] +name = "splashscreen" +path = "../../examples/splashscreen/main.rs" + +[[example]] +name = "state" +path = "../../examples/state/main.rs" + +[[example]] +name = "streaming" +path = "../../examples/streaming/main.rs" + +[[example]] +name = "isolation" +path = "../../examples/isolation/main.rs" +required-features = ["isolation"] diff --git a/crates/tauri/src/vibrancy/mod.rs b/crates/tauri/src/vibrancy/mod.rs index 38d8caf651a8..9304cc01d848 100644 --- a/crates/tauri/src/vibrancy/mod.rs +++ b/crates/tauri/src/vibrancy/mod.rs @@ -10,6 +10,8 @@ use crate::{Runtime, Window}; #[cfg(target_os = "macos")] mod macos; +#[cfg(target_env = "ohos")] +mod ohos; #[cfg(windows)] mod windows; @@ -22,9 +24,13 @@ pub fn set_window_effects( windows::apply_effects(window, _effects); #[cfg(target_os = "macos")] macos::apply_effects(window, _effects); + #[cfg(target_env = "ohos")] + ohos::apply_effects(window, _effects); } else { #[cfg(windows)] windows::clear_effects(window); + #[cfg(target_env = "ohos")] + ohos::clear_effects(window); } Ok(()) } diff --git a/crates/tauri/src/vibrancy/ohos.rs b/crates/tauri/src/vibrancy/ohos.rs new file mode 100644 index 000000000000..adf16df6aa32 --- /dev/null +++ b/crates/tauri/src/vibrancy/ohos.rs @@ -0,0 +1,63 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::utils::config::{Color, WindowEffectsConfig}; +use crate::window::Effect; +use crate::{Runtime, Window}; +use tauri_runtime::WindowDispatch; + +pub fn apply_effects(window: &Window, effects: WindowEffectsConfig) { + let WindowEffectsConfig { + effects, + radius, + color, + .. + } = effects; + + let window_id = match window.window.dispatcher.ohos_window_id() { + Ok(Some(id)) => id, + Ok(None) => { + log::warn!("[vibrancy] ohos_window_id returned None — tao window_id not set; skipping effects"); + return; + } + Err(e) => { + log::error!("[vibrancy] ohos_window_id failed: {:?}", e); + return; + } + }; + + let blur_radius = radius.unwrap_or(20.0); + + // Pick the first effect; OHOS approximates every vibrancy effect via blur + tint, + // so there is no need to filter by supported variant. + let Some(effect) = effects.into_iter().next() else { + return; + }; + + let result = match effect { + Effect::Blur => window_vibrancy::apply_ohos_blur(window_id, blur_radius), + Effect::Acrylic => { + let c = color.map(|Color(r, g, b, a)| (r, g, b, a)); + window_vibrancy::apply_ohos_acrylic(window_id, blur_radius, c) + } + Effect::Mica => window_vibrancy::apply_ohos_mica(window_id, blur_radius, None), + Effect::MicaDark => window_vibrancy::apply_ohos_mica(window_id, blur_radius, Some(true)), + Effect::MicaLight => window_vibrancy::apply_ohos_mica(window_id, blur_radius, Some(false)), + Effect::Tabbed => window_vibrancy::apply_ohos_mica(window_id, blur_radius, None), + Effect::TabbedDark => window_vibrancy::apply_ohos_mica(window_id, blur_radius, Some(true)), + Effect::TabbedLight => window_vibrancy::apply_ohos_mica(window_id, blur_radius, Some(false)), + // macOS-specific effects: best-effort approximation with basic blur + _ => window_vibrancy::apply_ohos_blur(window_id, blur_radius), + }; + match result { + Ok(_) => log::info!("[vibrancy] applied effect {:?} to window_id {}", effect, window_id), + Err(e) => log::error!("[vibrancy] apply effect {:?} to window_id {} failed: {}", effect, window_id, e), + } +} + +pub fn clear_effects(window: &Window) { + if let Ok(Some(window_id)) = window.window.dispatcher.ohos_window_id() { + let _ = window_vibrancy::clear_ohos_blur(window_id); + } +} diff --git a/crates/tauri/src/window/mod.rs b/crates/tauri/src/window/mod.rs index bd7e3741498b..c1842bc3bf61 100644 --- a/crates/tauri/src/window/mod.rs +++ b/crates/tauri/src/window/mod.rs @@ -451,9 +451,17 @@ tauri::Builder::default() let app_manager = self.manager.manager_owned(); let window_label = window.label().to_string(); let window_ = window.clone(); + let effects_to_apply = self.window_effects.clone(); + // OHOS: apply directly. set_window_blur uses TSFN (threadsafe, no main-thread Env needed), + // so no run_on_main_thread required (avoids run_on_main_thread + rx.recv() deadlock risk). + #[cfg(target_env = "ohos")] + if let Some(effects) = effects_to_apply.clone() { + let _ = crate::vibrancy::set_window_effects(&window_, Some(effects)); + } // run on the main thread to fix a deadlock on webview.eval if the tracing feature is enabled let _ = window.run_on_main_thread(move || { - if let Some(effects) = self.window_effects { + #[cfg(not(target_env = "ohos"))] + if let Some(effects) = effects_to_apply { _ = crate::vibrancy::set_window_effects(&window_, Some(effects)); } let event = crate::EventName::from_str("tauri://window-created"); @@ -2167,6 +2175,16 @@ tauri::Builder::default() pub fn set_effects>>(&self, effects: E) -> crate::Result<()> { let effects = effects.into(); let window = self.clone(); + // OHOS: apply directly. set_window_blur uses TSFN (threadsafe, no main-thread Env needed), + // so no run_on_main_thread required (avoids the run_on_main_thread + rx.recv() deadlock risk + // per ohos-constraints.md). ohos_window_id() uses send_user_message + recv, safe from any + // thread (main thread event loop handles OhosWindowId; not a run_on_main_thread closure). + #[cfg(target_env = "ohos")] + { + let _ = crate::vibrancy::set_window_effects(&window, effects); + return Ok(()); + } + #[cfg(not(target_env = "ohos"))] self.run_on_main_thread(move || { let _ = crate::vibrancy::set_window_effects(&window, effects); }) diff --git a/doc/manual_tests.md b/doc/manual_tests.md index 6472a8208771..3738c110dc4d 100644 --- a/doc/manual_tests.md +++ b/doc/manual_tests.md @@ -361,7 +361,7 @@ | 一级场景 | 二级场景 | 三级场景 | 用例名称 | 用例级别 | 预置条件 | 测试步骤 | 预期结果 | 备注 | |---------|---------|---------|---------|---------|---------|---------|---------|------| -| plugin | global-shortcut | 注册与触发 | Register Shortcut — 注册快捷键并物理键盘触发 | **T0** | 应用已启动;设备连接物理键盘;进入 Tests 页面底部 Global Shortcut Manual Tests 区域 | 1. 点击 "Register Ctrl+Shift+T" 按钮 2. 确认状态显示 "Registered: CommandOrControl+Shift+T" 3. 用物理键盘按下 Ctrl+Shift+T | ① 状态变为 "Triggered! id=xxx, state=Pressed" ② 控制台输出 `[global-shortcut] Shortcut triggered: id=xxx, state=Pressed` | OHOS 使用 inputConsumer API(API 14+);最多支持 2 个修饰键 | +| plugin | global-shortcut | 注册与触发 | Register Shortcut — 注册快捷键并物理键盘触发 | **T0** | 应用已启动;设备连接物理键盘;进入 Tests 页面底部 Global Shortcut Manual Tests 区域 | 1. 点击 "Register Ctrl+Shift+T" 按钮 2. 确认状态显示 "Registered: CommandOrControl+Shift+T" 3. 用物理键盘按下 Ctrl+Shift+T | ① 状态变为 "Triggered! id=xxx, state=Released" ② 控制台输出 `[global-shortcut] Shortcut triggered: id=xxx, state=Released` | OHOS 使用 inputConsumer API(API 14+),仅在 key-down 时触发 Pressed 回调;代码合成 Released 事件以匹配 global-hotkey 合约,UI 最终显示 Released;最多支持 2 个修饰键 | | plugin | global-shortcut | 注销验证 | Unregister All — 注销后快捷键不再触发 | **T0** | 已注册 Ctrl+Shift+T 且已验证触发成功 | 1. 点击 "Unregister All" 按钮 2. 确认状态显示 "All shortcuts unregistered" 3. 用物理键盘再次按下 Ctrl+Shift+T | ① 状态不再变为 "Triggered" ② 快捷键已被注销,系统不再拦截该组合键 | 验证 inputConsumer.off() 精确注销,不影响其他应用的快捷键 | --- @@ -377,13 +377,27 @@ | core | 窗口聚焦 | 多窗口层级 | Window Focus 多窗口层级验证 | **T0** | 应用已启动,进入 Tests 页面 | 1. 点击 "Window Focus" 创建子窗口 2. 手动将其他子窗口拖到该窗口上方 3. 再次点击 "Window Focus" | ① 首次点击创建 Float 子窗口 ② 再次点击调用 `setFocus()` → `raiseToAppTop()` ③ 窗口回到所有 Float 窗口最上方 | `Message::Task` 派发到主线程 → `focus_window(id)` → NAPI → `WindowManager.focusWindow` → `win.raiseToAppTop()` | | core | 热键缩放 | Ctrl+/- | Ctrl+/- 缩放验证 | **T1** | 应用已启动,进入 Tests 页面 | 1. 点击 "Hotkey Zoom" 查看说明 2. 聚焦 webview 区域 3. 按 Ctrl + = 放大 4. 按 Ctrl + - 缩小 | ① 页面内容随快捷键放大/缩小 ② 缩放级别在 0.2~10 之间 | `zoom-hotkey.js` 通过 `cfg(desktop)` 注入。Ctrl+0 被 ArkWeb 引擎拦截,不生效 | -| 模块 | T0 | T1 | 合计 | -|------|-----|-----|------| -| 窗口聚焦与热键缩放 | 1 | 1 | **2** | +--- + +## 十九、Vibrancy(窗口模糊)手动用例 + +> 自动用例 2 个(side-effect): +> 1. `window.setEffects(Blur/Acrylic/Mica/TabbedDark/TabbedLight) + clearEffects` 不抛错(运行时 setEffects,AttributeUpdater 刷新 backdropBlur/backgroundColor) +> 2. `create_transparent_window(effect=Blur)` build 时 effects 不抛错(WindowBuilder::effects,registerController inject) +> +> 以下为手动用例,通过 Tests 视图的手动按钮触发。vibrancy 窗口用 create_transparent_window(Float 子窗口,避开 UIAbility singleton 冲突)。 + +| 一级场景 | 二级场景 | 三级场景 | 用例名称 | 用例级别 | 预置条件 | 测试步骤 | 预期结果 | 备注 | +|---------|---------|---------|---------|---------|---------|---------|---------|------| +| core | vibrancy | Blur | Blur effect visible | **T0** | 应用已启动,进入 Tests 视图 | 1. 点击 "vibrancy: Blur effect visible" 手动测试按钮 2. 观察弹出的透明窗口 | 窗口背景呈磨砂模糊(backdropBlur(25)),能透出背后内容且带模糊 | 窗口加载 vibrancy.html 透明页,Effect::Blur radius=25 | +| core | vibrancy | Acrylic | Acrylic effect visible | T1 | 应用已启动,进入 Tests 视图 | 1. 点击 "vibrancy: Acrylic effect visible" 手动测试按钮 2. 观察弹出的透明窗口 | 窗口背景呈模糊 + 半透明深色 tint(blur + color) | Effect::Acrylic radius=25, color=[0,0,0,128] | +| core | vibrancy | TabbedDark | TabbedDark effect visible | T1 | 应用已启动,进入 Tests 视图 | 1. 点击 "vibrancy: TabbedDark effect visible" 手动测试按钮 2. 观察弹出的透明窗口 | 窗口背景呈模糊 + 深色 tint | Effect::TabbedDark radius=20(OHOS 下等价于 MicaDark 的深色 tint 实现) | +| core | vibrancy | clearEffects | clearEffects removes blur | **T0** | 应用已启动,进入 Tests 视图 | 1. 点击 "vibrancy: clearEffects removes blur" 手动测试按钮 2. 观察:先模糊 1s,然后 clearEffects 后模糊消失 | ① 初始窗口背景呈磨砂模糊 ② clearEffects 后窗口背景变清晰,且无半透明颜色遮罩(完全透出背后内容,不发暗/无色调) | 验证 clearEffects 同时移除 backdropBlur 和 backgroundColor tint | +| core | vibrancy | build-time effects | build-time Blur effect visible | **T0** | 应用已启动,进入 Tests 视图 | 1. 点击 "vibrancy: build-time Blur (WindowBuilder::effects)" 手动测试按钮 2. 观察弹出的透明窗口 | 窗口出现时即呈磨砂模糊(build 时 effects,非运行时 setEffects) | create_transparent_window(effect=Blur, radius=25),WindowBuilder::effects 在窗口创建时 apply | --- -## 十九、用例统计 +## 二十、用例统计 | 模块 | T0 | T1 | 合计 | |------|-----|-----|------| @@ -410,5 +424,6 @@ | Unstable Feature(窗口与 Webview 解耦) | 2 | 1 | **3** | | Global Shortcut(全局快捷键) | 2 | 0 | **2** | | 窗口聚焦与热键缩放 | 1 | 1 | **2** | -| **合计** | **56** | **55** | **111** | +| Vibrancy(窗口模糊) | 3 | 2 | **5** | +| **合计** | **59** | **57** | **116** | diff --git a/examples/api/public/vibrancy.html b/examples/api/public/vibrancy.html new file mode 100644 index 000000000000..fc6c4d97f7d0 --- /dev/null +++ b/examples/api/public/vibrancy.html @@ -0,0 +1,42 @@ + + + + + +Vibrancy + + + +
+

VIBRANCY

+

transparent page

+
+ + + diff --git a/examples/api/src-tauri/Cargo.toml b/examples/api/src-tauri/Cargo.toml index f4c4d2e05c81..670733afab9e 100644 --- a/examples/api/src-tauri/Cargo.toml +++ b/examples/api/src-tauri/Cargo.toml @@ -21,6 +21,7 @@ serde_json = "1" serde = { version = "1", features = ["derive"] } tiny_http = "0.11" log = "0.4.21" +anyhow = "1" tauri-plugin-sample = { path = "./tauri-plugin-sample/" } tauri-plugin-http = { path = "../../../../plugins-workspace/plugins/http" } tauri-plugin-os = { path = "../../../../plugins-workspace/plugins/os" } diff --git a/examples/api/src-tauri/src/cmd.rs b/examples/api/src-tauri/src/cmd.rs index e035eb51f4f9..88f1a89a9e18 100644 --- a/examples/api/src-tauri/src/cmd.rs +++ b/examples/api/src-tauri/src/cmd.rs @@ -687,8 +687,11 @@ const STATUS_SCRIPT: &str = r##" pub fn create_transparent_window( app: tauri::AppHandle, window_id: String, + effect: Option, + radius: Option, + color: Option<[u8; 4]>, ) -> tauri::Result<()> { - log::info!("Creating transparent window: {}", window_id); + log::info!("Creating transparent window: {} (effect={:?}, radius={:?})", window_id, effect, radius); let close_link = CLOSE_LINK_HTML; let status_script = STATUS_SCRIPT; @@ -713,13 +716,36 @@ pub fn create_transparent_window( "# ); - let _window = - tauri::WebviewWindowBuilder::new(&app, &window_id, WebviewUrl::App("hello.html".into())) - .title("Transparent Window") - .transparent(true) - .inner_size(600.0, 400.0) - .initialization_script(&init_script) - .build()?; + let mut builder = tauri::WebviewWindowBuilder::new(&app, &window_id, WebviewUrl::App("hello.html".into())) + .title("Transparent Window") + .transparent(true) + .inner_size(600.0, 400.0) + .initialization_script(&init_script); + + // Optional build-time effects (WindowBuilder::effects path — applied at window creation via + // registerController inject, distinct from runtime setEffects which uses AttributeUpdater). + if let Some(effect_name) = &effect { + let effect = match effect_name.as_str() { + "Blur" => tauri::window::Effect::Blur, + "Acrylic" => tauri::window::Effect::Acrylic, + "Mica" => tauri::window::Effect::Mica, + "MicaDark" => tauri::window::Effect::MicaDark, + "MicaLight" => tauri::window::Effect::MicaLight, + "Tabbed" => tauri::window::Effect::Tabbed, + "TabbedDark" => tauri::window::Effect::TabbedDark, + "TabbedLight" => tauri::window::Effect::TabbedLight, + other => return Err(tauri::Error::Anyhow(anyhow::anyhow!("unknown effect: {}", other))), + }; + let effects = tauri::utils::config::WindowEffectsConfig { + effects: vec![effect], + radius, + state: None, + color: color.map(|c| tauri::utils::config::Color(c[0], c[1], c[2], c[3])), + }; + builder = builder.effects(effects); + } + + let _window = builder.build()?; Ok(()) } diff --git a/examples/api/src/lib/tests/core.ts b/examples/api/src/lib/tests/core.ts index 5db86c6f58d5..44255f540f8f 100644 --- a/examples/api/src/lib/tests/core.ts +++ b/examples/api/src/lib/tests/core.ts @@ -2,7 +2,7 @@ import type { TestCase } from '../test-runner'; import { invoke, Channel, Resource } from '@tauri-apps/api/core'; import { emit, listen, once } from '@tauri-apps/api/event'; import { getVersion } from '@tauri-apps/api/app'; -import { getCurrentWindow, currentMonitor, cursorPosition } from '@tauri-apps/api/window'; +import { getCurrentWindow, currentMonitor, cursorPosition, Effect } from '@tauri-apps/api/window'; import { WebviewWindow } from '@tauri-apps/api/webviewWindow'; import { getCurrentWebview, Webview } from '@tauri-apps/api/webview'; import { appCacheDir } from '@tauri-apps/api/path'; @@ -1307,4 +1307,85 @@ export const coreTests: TestCase[] = [ ); }, }, + + // ── Vibrancy (window effects) ── + // NOTE: WebviewWindow.new defaults to OHOS UIAbility (singleton) which conflicts + // with the main window. Use create_transparent_window (Float sub-window) instead. + { + name: 'window.setEffects (Blur/Acrylic/Mica) — no throw', + category: 'side-effect', + async fn() { + await invoke('create_transparent_window', { windowId: 'test-vibrancy-auto' }); + const win = await WebviewWindow.getByLabel('test-vibrancy-auto'); + if (!win) throw new Error('vibrancy window not created'); + await win.setEffects({ effects: [Effect.Blur], radius: 25 }); + await win.setEffects({ effects: [Effect.Acrylic], radius: 25, color: [0, 0, 0, 128] }); + await win.setEffects({ effects: [Effect.Mica], radius: 20 }); + await win.setEffects({ effects: [Effect.TabbedDark], radius: 20 }); + await win.setEffects({ effects: [Effect.TabbedLight], radius: 20 }); + await win.clearEffects(); + assert(true, 'setEffects + clearEffects did not throw for all effect types'); + await win.close(); + }, + }, + { + name: 'vibrancy: Blur effect visible (manual)', + category: 'manual', + async fn() { + await invoke('create_transparent_window', { windowId: 'test-vibrancy-blur' }); + const win = await WebviewWindow.getByLabel('test-vibrancy-blur'); + if (!win) throw new Error('vibrancy window not created'); + await win.setEffects({ effects: [Effect.Blur], radius: 25 }); + // Manual: window should show frosted/blurry background + }, + }, + { + name: 'vibrancy: Acrylic effect visible (manual)', + category: 'manual', + async fn() { + await invoke('create_transparent_window', { windowId: 'test-vibrancy-acrylic' }); + const win = await WebviewWindow.getByLabel('test-vibrancy-acrylic'); + if (!win) throw new Error('vibrancy window not created'); + await win.setEffects({ effects: [Effect.Acrylic], radius: 25, color: [0, 0, 0, 128] }); + // Manual: window should show blur + semi-transparent tint + }, + }, + { + name: 'vibrancy: TabbedDark effect visible (manual)', + category: 'manual', + async fn() { + await invoke('create_transparent_window', { windowId: 'test-vibrancy-tabbed-dark' }); + const win = await WebviewWindow.getByLabel('test-vibrancy-tabbed-dark'); + if (!win) throw new Error('vibrancy window not created'); + await win.setEffects({ effects: [Effect.TabbedDark], radius: 20 }); + // Manual: window should show blur + dark tint + }, + }, + { + name: 'vibrancy: clearEffects removes blur (manual)', + category: 'manual', + async fn() { + await invoke('create_transparent_window', { windowId: 'test-vibrancy-clear' }); + const win = await WebviewWindow.getByLabel('test-vibrancy-clear'); + if (!win) throw new Error('vibrancy window not created'); + await win.setEffects({ effects: [Effect.Blur], radius: 25 }); + await new Promise((r) => setTimeout(r, 1000)); + await win.clearEffects(); + // Manual: blur should be gone after clearEffects + }, + }, + // ── Vibrancy build-time effects (WindowBuilder::effects, distinct from runtime setEffects) ── + { + name: 'vibrancy build-time effects (WindowBuilder::effects) — no throw', + category: 'side-effect', + async fn() { + // create_transparent_window with effect param applies effects at build time + // (registerController inject), distinct from runtime setEffects (AttributeUpdater). + await invoke('create_transparent_window', { windowId: 'test-vibrancy-build', effect: 'Blur', radius: 25 }); + const win = await WebviewWindow.getByLabel('test-vibrancy-build'); + if (!win) throw new Error('build-time effects window not created'); + await win.close(); + assert(true, 'build-time effects window created + closed without throw'); + }, + }, ]; diff --git a/examples/api/src/lib/tests/tray.ts b/examples/api/src/lib/tests/tray.ts index f71c0fb5aa28..d522adcfd5c6 100644 --- a/examples/api/src/lib/tests/tray.ts +++ b/examples/api/src/lib/tests/tray.ts @@ -279,7 +279,7 @@ export const trayTests: TestCase[] = [ title: 'Test Panel', height: 250, abilityName: 'TestTrayAbility', - moduleName: 'entry', + moduleName: 'entry_desktop', }); }, }, diff --git a/examples/api/src/views/TestRunner.svelte b/examples/api/src/views/TestRunner.svelte index e6d1c904f085..accce1594aa6 100644 --- a/examples/api/src/views/TestRunner.svelte +++ b/examples/api/src/views/TestRunner.svelte @@ -10,8 +10,9 @@ import { trayTests } from '../lib/tests/tray'; import { invoke } from '@tauri-apps/api/core'; import { listen } from '@tauri-apps/api/event'; - import { getCurrentWindow, currentMonitor, cursorPosition } from '@tauri-apps/api/window'; + import { getCurrentWindow, currentMonitor, cursorPosition, Effect } from '@tauri-apps/api/window'; import { getCurrentWebview, Webview } from '@tauri-apps/api/webview'; + import { WebviewWindow } from '@tauri-apps/api/webviewWindow'; import { appCacheDir, join } from '@tauri-apps/api/path'; import { flushConsoleLog, clearConsoleLog } from '../lib/console-capture'; @@ -838,6 +839,93 @@ Expected behavior: }); } + async function manualSetBackgroundColor(color, label) { + await wrapManual(`setBackgroundColor(${label})`, async () => { + const win = getCurrentWindow(); + if (color === null) { + // Use webview-level API which supports null to truly reset to default + await webview.setBackgroundColor(null); + manualResult = `Background color reset to default (null via Webview API).\n\nExpected: Window background returns to its original default color.`; + } else { + await win.setBackgroundColor(color); + const [r, g, b, a] = color; + manualResult = `Background color set to [${r},${g},${b},${a}] (${label}).\n\n` + + `Expected: Window background should change to ${label}.\n` + + `Alpha=${a} (${a === 255 ? 'fully opaque' : a === 0 ? 'fully transparent' : 'semi-transparent'}).\n\n` + + `If visual matches → PASS.`; + } + onMessage(manualResult); + }); + } + + // ─── Vibrancy (Window Effects) Manual Tests (OHOS only) ─── + // NOTE: WebviewWindow.new defaults to OHOS UIAbility (singleton) which conflicts with the + // main window. Use create_transparent_window (Float sub-window) instead so the window + // creates successfully and setEffects can apply backdropBlur. + async function manualVibrancyEffect(effectName, effect, opts, expect) { + await wrapManual(`vibrancy:${effectName}`, async () => { + const windowId = `manual-vibrancy-${effectName}`; + // Reuse label so repeated clicks refresh the same window (avoid leftover windows) + try { await WebviewWindow.getByLabel(windowId)?.then(w => w?.close()); } catch {} + await invoke('create_transparent_window', { windowId }); + const win = await WebviewWindow.getByLabel(windowId); + if (!win) throw new Error('vibrancy window not created'); + await win.setEffects({ effects: [effect], ...opts }); + manualResult = `Vibrancy ${effectName} window created (id: "${windowId}").\n\n` + + `Expected: ${expect}\n\n` + + `If matches → PASS.\nIf no blur/effect visible → FAIL.\n\n` + + `Close with Ctrl+W or Cmd+W.`; + onMessage(manualResult); + }); + } + + async function manualVibrancyBlur() { + await manualVibrancyEffect('Blur', Effect.Blur, { radius: 25 }, + 'Window background FROSTED/BLURRY (backdropBlur 25) — content behind is visible but blurred.'); + } + async function manualVibrancyAcrylic() { + await manualVibrancyEffect('Acrylic', Effect.Acrylic, { radius: 25, color: [0, 0, 0, 128] }, + 'Window background BLURRY + semi-transparent DARK tint (blur + color overlay).'); + } + async function manualVibrancyTabbedDark() { + await manualVibrancyEffect('TabbedDark', Effect.TabbedDark, { radius: 20 }, + 'Window background BLURRY + DARK tint (OHOS approximates MicaDark via blur + dark tint).'); + } + + async function manualVibrancyClearEffects() { + await wrapManual('vibrancy:clearEffects', async () => { + const windowId = 'manual-vibrancy-clear'; + try { await WebviewWindow.getByLabel(windowId)?.then(w => w?.close()); } catch {} + await invoke('create_transparent_window', { windowId }); + const win = await WebviewWindow.getByLabel(windowId); + if (!win) throw new Error('vibrancy window not created'); + await win.setEffects({ effects: [Effect.Blur], radius: 25 }); + await new Promise((r) => setTimeout(r, 1000)); + await win.clearEffects(); + manualResult = `Vibrancy clearEffects window created (id: "${windowId}").\n\n` + + `Expected: Window background was BLURRY for 1 second, then became CLEAR/TRANSPARENT after clearEffects.\n\n` + + `If blur disappeared after ~1s → PASS.\nIf blur remained → FAIL (clearEffects not working).\n\n` + + `Close with Ctrl+W or Cmd+W.`; + onMessage(manualResult); + }); + } + + async function manualVibrancyBuildTimeBlur() { + await wrapManual('vibrancy:build-time Blur', async () => { + const windowId = 'manual-vibrancy-build-blur'; + try { await WebviewWindow.getByLabel(windowId)?.then(w => w?.close()); } catch {} + // create_transparent_window with effect param applies effects at BUILD time + // (WindowBuilder::effects → registerController inject), distinct from runtime setEffects. + await invoke('create_transparent_window', { windowId, effect: 'Blur', radius: 25 }); + manualResult = `Build-time Blur window created (id: "${windowId}").\n\n` + + `Expected: Window appears with FROSTED/BLURRY background IMMEDIATELY on creation\n` + + `(build-time effect via WindowBuilder::effects, not runtime setEffects).\n\n` + + `If frosted on appear → PASS.\nIf clear on appear (needs runtime setEffects) → FAIL.\n\n` + + `Close with Ctrl+W or Cmd+W.`; + onMessage(manualResult); + }); + } + // ─── Process & Updater Manual Tests ─── async function manualRelaunch() { await wrapManual('relaunch', async () => { @@ -1730,6 +1818,16 @@ Mutex released, no cascade deadlock: ${ok ? 'PASS ✅' : 'FAIL ❌'}`; +
+
Vibrancy (Window Effects) — OHOS
+
+ + + + + +
+
Process & Updater Manual Tests
diff --git a/examples/api/src/views/Tray.svelte b/examples/api/src/views/Tray.svelte index 44295566a568..38c7451ba22a 100644 --- a/examples/api/src/views/Tray.svelte +++ b/examples/api/src/views/Tray.svelte @@ -14,7 +14,7 @@ let qoTitle = $state('Tauri API') let qoHeight = $state(300) let qoAbilityName = $state('TestTrayAbility') - let qoModuleName = $state('entry') + let qoModuleName = $state('entry_desktop') let testTray = $state(null) // Tauri 32x32 default icon diff --git a/openspec/changes/archive/2026-07-04-p1-window-vibrancy/.openspec.yaml b/openspec/changes/archive/2026-07-04-p1-window-vibrancy/.openspec.yaml new file mode 100644 index 000000000000..a903f7fe1897 --- /dev/null +++ b/openspec/changes/archive/2026-07-04-p1-window-vibrancy/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-06-16 diff --git a/openspec/changes/archive/2026-07-04-p1-window-vibrancy/design.md b/openspec/changes/archive/2026-07-04-p1-window-vibrancy/design.md new file mode 100644 index 000000000000..e1436fe1db22 --- /dev/null +++ b/openspec/changes/archive/2026-07-04-p1-window-vibrancy/design.md @@ -0,0 +1,174 @@ +## Context + +Tauri 的 `vibrancy` 模块为窗口提供视觉效果(模糊、毛玻璃、Mica 等),目前在 Windows 和 macOS 上有完整实现,OHOS 上为空操作。 + +**当前架构模式(Windows/macOS)**: +``` +tauri/vibrancy/windows.rs → window_vibrancy::apply_blur(window, color) + → windows-sys (DWM/SetWindowCompositionAttribute) ← 平台原生 SDK + +tauri/vibrancy/macos.rs → window_vibrancy::apply_vibrancy(window, material, ...) + → objc2-app-kit (NSVisualEffectView) ← 平台原生 SDK +``` + +**OHOS 适配目标**:保持相同架构模式: +``` +tauri/vibrancy/ohos.rs → window_vibrancy::apply_ohos_blur(window_id, radius) + → openharmony-ability (NAPI → ArkTS backdropBlur) ← 平台原生 SDK +``` + +**本地 SDK 验证**(HarmonyOS 6.1.0, API 23): +- ❌ `Window.setWindowBlur()` 在 `@ohos.window` 中不存在(0 次出现) +- ✅ `backdropBlur(radius: number)` — 组件属性,API 7+ +- ✅ `backgroundBlurStyle(BlurStyle)` — 组件属性,API 9+ +- ✅ `NODE_BACKGROUND_BLUR_STYLE` — 原生节点 API(C/C++) + +**约束**: +- 铁律 1:所有 OHOS 系统能力必须通过 `openharmony-ability` 桥接 +- OHOS 模糊是组件级 API,不是窗口级 API +- `HasWindowHandle` 在 OHOS 返回 `OHNativeWindow`(渲染表面),无法用于操作 ArkUI 组件节点 + +## Goals / Non-Goals + +**Goals:** +- 在 OHOS 上实现窗口模糊效果,让 `WindowEffectsConfig` 配置生效 +- 保持 window-vibrancy 作为 tauri 和平台 SDK 之间的抽象层(与 Windows/macOS 一致) +- 支持 Tauri 的 Blur/Acrylic/Mica/Tabbed 等 Effect 类型 +- 不支持的设备静默跳过 + +**Non-Goals:** +- 不实现 macOS 独有的材质类型的精确映射 +- 不修改 `raw-window-handle` crate + +## Decisions + +### 决策 1:window-vibrancy 作为 OHOS 的抽象层 + +**选择**:`window-vibrancy` crate 新增 OHOS 平台支持,内部依赖 `openharmony-ability`(与 Windows 依赖 `windows-sys`、macOS 依赖 `objc2-app-kit` 模式一致)。 + +**新增 OHOS 专用 API**(不走 `HasWindowHandle`): +```rust +// window-vibrancy/src/lib.rs 新增 +#[cfg(target_env = "ohos")] +pub fn apply_ohos_blur(window_id: i64, radius: f64) -> Result<(), Error>; +#[cfg(target_env = "ohos")] +pub fn clear_ohos_blur(window_id: i64) -> Result<(), Error>; +#[cfg(target_env = "ohos")] +pub fn apply_ohos_acrylic(window_id: i64, radius: f64, color: Option) -> Result<(), Error>; +#[cfg(target_env = "ohos")] +pub fn clear_ohos_acrylic(window_id: i64) -> Result<(), Error>; +#[cfg(target_env = "ohos")] +pub fn apply_ohos_mica(window_id: i64, radius: f64, dark: Option) -> Result<(), Error>; +#[cfg(target_env = "ohos")] +pub fn clear_ohos_mica(window_id: i64) -> Result<(), Error>; +``` + +**理由**: +- 保持 window-vibrancy 作为平台抽象层的角色 +- `HasWindowHandle` 在 OHOS 上返回 `OHNativeWindow`(渲染表面),无法操作 ArkUI 组件节点 +- OHOS 模糊是组件级 API,需要不同的入口标识 + +**替代方案**: +- 修改 `HasWindowHandle` 签名 → 影响上游 `raw-window-handle` crate,不可行 +- tauri 直接调用 openharmony-ability → 破坏三层架构一致性 + +### 决策 2:组件级模糊实现方式 + +**选择**:通过 `openharmony-ability` 的 ArkTS `WindowManager` 将 `backdropBlur(radius)` 应用到 WebView 容器组件。 + +**调用链**: +``` +tauri/vibrancy/ohos.rs + → window_vibrancy::apply_ohos_blur(window_id, radius) + → openharmony_ability::set_window_blur(window_id, radius) + → NAPI → ArkTS WindowManager.setWindowBlur(windowId, radius) + → WebView 容器组件 .backdropBlur(radius) 动态更新 +``` + +**动态更新机制**:见下方"实现演进(2026-07-07)"——`@State` 在 `@Builder`/BuilderNode 内不可用,实际用 `AttributeUpdater`(BlurModifier)运行时刷新 `backdropBlur`/`backgroundColor`。 + +**理由**: +- 组件级 API 是本地 SDK 中唯一可用的模糊方案 +- `backdropBlur(radius: number)` API 7+,与项目最低版本兼容 +- 运行时刷新用 `AttributeUpdater`(`modifier.attribute?.backdropBlur(radius)` 立即触发组件更新,不需 @State) + +### 决策 3:Effect 到 OHOS 的映射策略 + +**选择**:所有 Effect 类型统一映射到 `apply_ohos_blur` + 可选的背景色设置 + +| Tauri Effect | window-vibrancy 调用 | 说明 | +|---|---|---| +| `Blur` | `apply_ohos_blur(id, radius)` | radius 取 config.radius 或默认 20 | +| `Acrylic` | `apply_ohos_acrylic(id, 25, color)` | 模糊 + 半透明背景色 | +| `Mica` | `apply_ohos_mica(id, 20, None)` | 中等模糊 | +| `MicaDark` | `apply_ohos_mica(id, 20, Some(true))` | 模糊 + 深色背景 | +| `MicaLight` | `apply_ohos_mica(id, 20, Some(false))` | 模糊 + 浅色背景 | +| `Tabbed` 系列 | 同 Mica 系列 | OHOS 无对应概念 | +| macOS 材质类 | `apply_ohos_blur(id, 20)` | 统一模糊近似 | + +### 决策 4:通过 dispatcher 消息链传递窗口效果 + +**选择**:与 `set_background_color` 一致,通过 `WindowDispatch` trait → `WindowMessage` → event loop → tao Window 的消息链。 + +**理由**:tauri `Window` 无法直接获取 OHOS window_id,需要经过 dispatcher 层在 event loop handler 中访问 tao Window 内部的 `window_id`。 + +**调用链**(设计初版,已简化): +``` +Window::set_effects(effects) + → dispatcher.set_window_effects(effects) + → WindowMessage::SetEffects(effects) + → event loop handler + → tao_window.set_window_effects(effects) + → window_vibrancy::apply_ohos_blur(self.window_id, radius) +``` + +> **实现偏差(2026-07-02)**:上述 `SetEffects` → `tao_window.set_window_effects` 链路未采用。实际实现中,dispatcher 消息链仅用于取 window id(`WindowMessage::OhosWindowId` → `tao::WindowExtOpenHarmony::window_id()`),effect 应用由 `tauri/vibrancy/ohos.rs` 直接调用 `window_vibrancy::apply_ohos_blur(window_id, radius)`,不经 tao。这与 Windows/macOS 在 `tauri/vibrancy/mod.rs` 直接调用 `window_vibrancy` 的方式一致。详见 `openspec/changes/window-vibrancy-plan.md` 的架构决策。 + +## 实现演进(2026-07-07):运行时刷新机制 + +初始实现(2026-07-04)只支持 build-time effects(`registerController` inject blurRadius 到 build data,`backdropBlur` 在 Stack build 时设置)。运行时 `setEffects` 因 `BuilderNode.update` 不刷新 `backdropBlur` 而失效。 + +### 运行时刷新:AttributeUpdater(BlurModifier) + +`@Builder` 函数内不能用 `@State`,`BuilderNode.update` 不刷新组件属性。解决:`AttributeUpdater`(不需 @State,`attribute?.backdropBlur(radius)` 立即触发组件更新)。 + +``` +// openharmony-ability DefaultWebview.ets +export class BlurModifier extends AttributeUpdater { + initializeModifier(_instance: CommonAttribute): void { + // 空:让 build 时 Stack.backdropBlur(data.style.blurRadius) 生效,不覆盖 + } +} +// WebBuilder Stack: .backdropBlur(data.style.blurRadius).attributeModifier(data.blurModifier) +// 运行时: modifier.attribute?.backdropBlur(radius) // 立即刷新 +``` + +### build-time vs runtime 路径 + +| 路径 | 触发 | 机制 | +|---|---|---| +| build-time | `WindowBuilder::effects` → `build` 时 apply | `set_window_blur` → `applyWindowBlur` queue `pendingBlurs` → `registerController` inject blurRadius 到 build data → Stack `.backdropBlur(data.style.blurRadius)` build 时设置 | +| runtime | `Window::set_effects` → `run_on_main_thread` | `set_window_blur` → `applyWindowBlur` → `controller.setAllWebviewsBlurRadius` → `BlurModifier.attribute?.backdropBlur(radius)` 立即刷新 | + +### TSFN(线程安全 NAPI,符合约束) + +`set_window_blur` / `set_window_background_color` 用 TSFN(ThreadsafeFunction,fire-and-forget NonBlocking)。TSFN 线程安全,不需 `thread_local MAIN_THREAD_ENV`,任何线程可调。`Window::set_effects` + build-time apply 直接调(工作线程),不用 `run_on_main_thread`(符合 ohos-constraints.md 1.2 约束"禁止 run_on_main_thread + rx.recv()")。 + +TSFN init 在 ArkHelper 初始化时(main thread,xcomponent.rs `init_vibrancy_tsfn`)。之后 set_window_blur 任何线程可调。 + +ohos_window_id 用 send_user_message + recv(不在 run_on_main_thread 闭包),工作线程调时 main thread event loop 处理 OhosWindowId,不死锁。 + +### set_window_background_color FnArgs + +`set_window_background_color`(Acrylic/Mica tint)初始用裸 tuple `(i64, u32)` 调 NAPI `func.call`,导致只传 1 个参数(tuple 对象)而非 2 个,ArkTS 收到错误参数。修复:用 `FnArgs<(i64, u32)>`(与 `set_window_blur` 一致,7bd67be 修了 set_window_blur 但漏了 set_window_background_color)。 + +### 窗口创建:Float 子窗口 + +`WebviewWindow.new` 默认 `OHOSWindowKind::UIAbility`(singleton),与主窗口冲突("UIAbility window already exists")。vibrancy 测试窗口用 `create_transparent_window`(`WebviewWindowBuilder::new`,默认 `ohos_window_kind: None` → Float 子窗口),避开冲突。 + +## Risks / Trade-offs + +- **[API 不存在]** `Window.setWindowBlur()` 在本地 SDK 中不存在 → 改用组件级 `backdropBlur` +- **[效果近似]** OHOS 无法精确复现 Windows Mica/Tabbed 的分层材质效果 → 文档标注为 "best-effort 近似" +- **[OHOS 专用 API 签名]** `apply_ohos_blur(window_id, radius)` 与 `apply_blur(window, color)` 签名不同 → OHOS 平台特殊性,无法避免 +- **[文件数增加]** 需要修改 window-vibrancy + openharmony-ability + tauri 三个 crate(tao 仅经既有 trait 提供 window id,不新增代码)→ 每个改动都是模式化的平台适配 diff --git a/openspec/changes/archive/2026-07-04-p1-window-vibrancy/proposal.md b/openspec/changes/archive/2026-07-04-p1-window-vibrancy/proposal.md new file mode 100644 index 000000000000..5a368eadc2e3 --- /dev/null +++ b/openspec/changes/archive/2026-07-04-p1-window-vibrancy/proposal.md @@ -0,0 +1,29 @@ +## Why + +Tauri 的窗口视觉效果(模糊、毛玻璃、Mica 等)目前在 OHOS 平台上是空操作。用户在 OHOS 设备上配置 `window_effects` 后看不到任何效果,与 Windows/macOS 体验严重不一致。OHOS 提供了组件级模糊 API(`backdropBlur` / `NODE_BACKGROUND_BLUR_STYLE`),可以实现背景模糊效果,现在需要补全这一能力。 + +## What Changes + +- 在 `window-vibrancy` crate 中新增 OHOS 平台支持,提供 `apply_ohos_blur` / `clear_ohos_blur` 等 OHOS 专用 API,内部依赖 `openharmony-ability` 作为平台 SDK(与 Windows 依赖 `windows-sys`、macOS 依赖 `objc2-app-kit` 模式一致) +- 在 `openharmony-ability` 的 Rust NAPI 层新增 `set_window_blur(window_id, radius)` 函数,桥接 OHOS 组件级模糊 API +- 在 `openharmony-ability` 的 ArkTS `WindowManager` 中新增 `setWindowBlur()` 方法,将模糊效果应用到 WebView 容器组件 +- 在 `tauri` 的 `vibrancy` 模块新增 OHOS 平台实现(`ohos.rs`),调用 `window_vibrancy::apply_ohos_blur()`,保持与 Windows/macOS 相同的调用模式 + +## Capabilities + +### New Capabilities +- `ohos-window-blur`: OHOS 平台窗口模糊效果适配,通过 window-vibrancy → openharmony-ability → 组件级 backdropBlur 的调用链,将 Tauri WindowEffect 枚举映射到 OHOS 模糊 API + +### Modified Capabilities + + +## Impact + +- **受影响代码**: + - `window-vibrancy` — 新增 OHOS 平台支持(`ohos.rs`、Cargo.toml、lib.rs) + - `openharmony-ability` — Rust NAPI 层 (`window/mod.rs`) + ArkTS 层 (`WindowManager.ets`) + - `tauri` — vibrancy 模块 (`mod.rs`, 新增 `ohos.rs`) + Cargo.toml +- **API 影响**:`window-vibrancy` 新增 OHOS 专用公开 API(`apply_ohos_blur` 等) +- **依赖**:`window-vibrancy` 在 OHOS cfg 下依赖 `openharmony-ability`;`tauri` 在 OHOS 下依赖 `window-vibrancy` +- **OHOS API**:使用组件级 `backdropBlur(radius)`(API 7+)或原生节点 API `NODE_BACKGROUND_BLUR_STYLE` +- **注意**:本地 SDK(HarmonyOS 6.1.0, API 23)中 `Window.setWindowBlur()` 不存在,模糊效果只能通过组件级 API 实现 diff --git a/openspec/changes/archive/2026-07-04-p1-window-vibrancy/specs/ohos-window-blur/spec.md b/openspec/changes/archive/2026-07-04-p1-window-vibrancy/specs/ohos-window-blur/spec.md new file mode 100644 index 000000000000..8fbdca08c739 --- /dev/null +++ b/openspec/changes/archive/2026-07-04-p1-window-vibrancy/specs/ohos-window-blur/spec.md @@ -0,0 +1,132 @@ +## ADDED Requirements + +### Requirement: window-vibrancy OHOS 平台支持 + +`window-vibrancy` crate SHALL 新增 OHOS 平台支持。在 `cfg(target_env = "ohos")` 下依赖 `openharmony-ability`(与 Windows 依赖 `windows-sys`、macOS 依赖 `objc2-app-kit` 模式一致),提供 OHOS 专用 API。 + +#### Scenario: apply_ohos_blur 设置模糊 +- **WHEN** 调用 `window_vibrancy::apply_ohos_blur(window_id, radius)` +- **THEN** SHALL 调用 `openharmony_ability::set_window_blur(window_id, radius)` 将模糊效果应用到指定窗口的 WebView 容器组件 +- **测试分类**: `manual`(需人工确认模糊效果可见) + +#### Scenario: clear_ohos_blur 清除模糊 +- **WHEN** 调用 `window_vibrancy::clear_ohos_blur(window_id)` +- **THEN** SHALL 调用 `openharmony_ability::set_window_blur(window_id, 0.0)` 关闭模糊 +- **测试分类**: `side-effect`(验证模糊效果被移除) + +#### Scenario: apply_ohos_acrylic 设置亚克力效果 +- **WHEN** 调用 `window_vibrancy::apply_ohos_acrylic(window_id, radius, color)` +- **THEN** SHALL 调用 `openharmony_ability::set_window_blur(window_id, radius)` 并设置半透明背景色 +- **测试分类**: `manual` + +#### Scenario: apply_ohos_mica 设置 Mica 效果 +- **WHEN** 调用 `window_vibrancy::apply_ohos_mica(window_id, radius, dark)` +- **THEN** SHALL 调用 `openharmony_ability::set_window_blur(window_id, radius)` 并根据 dark 参数设置深浅背景色 +- **测试分类**: `manual` + +#### Scenario: 设备不支持时静默跳过 +- **WHEN** 调用 OHOS 模糊 API 但设备不支持 +- **THEN** SHALL 返回 `Ok(())`,不中断执行 +- **测试分类**: `auto` + +### Requirement: openharmony-ability 窗口模糊 NAPI 桥接 + +`openharmony-ability` SHALL 提供 `set_window_blur(window_id: i64, radius: f64) -> napi_ohos::Result<()>` NAPI 函数。该函数 SHALL 遵循 `set_window_background_color` 相同的桥接模式(`get_helper()` + `get_main_thread_env()` + ArkTS 函数调用)。 + +#### Scenario: 设置主窗口模糊 +- **WHEN** 调用 `set_window_blur(0, 20.0)`,windowId=0 表示主窗口 +- **THEN** ArkTS WindowManager SHALL 将 `backdropBlur(20)` 应用到主窗口的 WebView 容器组件 +- **测试分类**: `manual` + +#### Scenario: 设置子窗口模糊 +- **WHEN** 调用 `set_window_blur(id, 30.0)`,id 为已创建的子窗口 ID +- **THEN** ArkTS WindowManager SHALL 将 `backdropBlur(30)` 应用到对应子窗口的 WebView 容器组件 +- **测试分类**: `manual` + +#### Scenario: 模糊半径为 0 关闭模糊 +- **WHEN** 调用 `set_window_blur(window_id, 0.0)` +- **THEN** ArkTS WindowManager SHALL 将 `backdropBlur(0)` 应用到组件,关闭模糊效果 +- **测试分类**: `side-effect` + +### Requirement: ArkTS WindowManager setWindowBlur 方法 + +ArkTS `WindowManager` SHALL 新增 `setWindowBlur(windowId: number, radius: number): void` 方法。该方法 SHALL 将 `backdropBlur(radius)` 组件属性应用到指定窗口的 WebView 容器组件(`DefaultXComponent` 的外层 Stack)。 + +#### Scenario: 主窗口模糊(windowId=0) +- **WHEN** `windowId` 为 0 且 `windowStage` 已初始化 +- **THEN** SHALL 更新主窗口 WebView 容器的 blur 状态,使 `backdropBlur(radius)` 生效 +- **测试分类**: `manual` + +#### Scenario: 子窗口模糊 +- **WHEN** `windowId` 不为 0 且存在于 `windows` Map 中 +- **THEN** SHALL 更新对应子窗口 WebView 容器的 blur 状态 +- **测试分类**: `manual` + +#### Scenario: 窗口不存在时静默忽略 +- **WHEN** `windowId` 不在管理范围内 +- **THEN** SHALL 记录 warn 日志并返回,不抛异常 +- **测试分类**: `auto` + +### Requirement: Tauri vibrancy OHOS 平台实现 + +`tauri` crate SHALL 在 `vibrancy` 模块中新增 OHOS 平台实现,当 `cfg(target_env = "ohos")` 时调用 `window_vibrancy` 的 OHOS API。映射 SHALL 通过 dispatcher 消息链传递(`WindowDispatch::set_window_effects` → `WindowMessage::SetEffects` → event loop → tao `Window::set_window_effects`),与 `set_background_color` 的架构模式一致。 + +#### Scenario: Blur 效果映射 +- **WHEN** `WindowEffectsConfig.effects` 包含 `Effect::Blur` +- **THEN** SHALL 调用 `window_vibrancy::apply_ohos_blur(window_id, radius)` +- **THEN** radius SHALL 取 `WindowEffectsConfig.radius` 的值,若未指定则默认 20.0 +- **测试分类**: `manual` + +#### Scenario: Acrylic 效果映射 +- **WHEN** `WindowEffectsConfig.effects` 包含 `Effect::Acrylic` +- **THEN** SHALL 调用 `window_vibrancy::apply_ohos_acrylic(window_id, 25.0, color)` +- **测试分类**: `manual` + +#### Scenario: Mica 系列效果映射 +- **WHEN** `WindowEffectsConfig.effects` 包含 `Effect::Mica` / `MicaDark` / `MicaLight` +- **THEN** SHALL 调用 `window_vibrancy::apply_ohos_mica(window_id, 20.0, dark)` +- **测试分类**: `manual` + +#### Scenario: Tabbed 系列效果映射 +- **WHEN** `WindowEffectsConfig.effects` 包含 `Effect::Tabbed` / `TabbedDark` / `TabbedLight` +- **THEN** SHALL 采用与 Mica 系列相同的映射策略 +- **测试分类**: `manual` + +#### Scenario: 清除效果 +- **WHEN** `set_window_effects` 传入 `effects: None` +- **THEN** SHALL 调用 `window_vibrancy::clear_ohos_blur(window_id)` 关闭模糊 +- **测试分类**: `side-effect` + +#### Scenario: 窗口创建时自动应用效果 +- **WHEN** `WindowAttributes.window_effects` 已配置 +- **THEN** 窗口创建完成后 SHALL 自动调用 `set_window_effects` 应用效果(已有代码路径) +- **测试分类**: `manual` + +### Requirement: Effect 类型优先级 + +当 `WindowEffectsConfig.effects` 包含多个 Effect 时,SHALL 取第一个可映射的 Effect 并忽略其余,与 Windows/macOS 行为一致。 + +#### Scenario: 多 Effect 取首个 +- **WHEN** `effects` 为 `[Effect::Acrylic, Effect::Blur]` +- **THEN** SHALL 仅应用 Acrylic 效果,忽略 Blur +- **测试分类**: `auto` + +#### Scenario: 无可映射 Effect 时静默跳过 +- **WHEN** `effects` 列表为空或全部为 macOS 专属材质 +- **THEN** SHALL 不调用任何 OHOS API,不返回错误 +- **测试分类**: `auto` + +### Requirement: Dispatcher 消息链支持窗口效果 + +`WindowDispatch` trait SHALL 新增 `set_window_effects(effects: Option)` 方法,通过 `WindowMessage::SetEffects` 转发到 event loop handler,handler 调用 tao `Window::set_window_effects`。此模式与 `set_background_color` 一致。 + +#### Scenario: set_window_effects 消息传递 +- **WHEN** tauri `Window::set_effects()` 被调用 +- **THEN** SHALL 通过 dispatcher 发送 `WindowMessage::SetEffects(effects)` 消息 +- **THEN** event loop handler SHALL 调用 `tao_window.set_window_effects(effects)` +- **测试分类**: `auto` + +#### Scenario: tao OHOS Window 处理窗口效果 +- **WHEN** tao `Window::set_window_effects` 被调用且 `self.window_id` 不为 None +- **THEN** SHALL 根据 Effect 类型调用 `window_vibrancy::apply_ohos_blur` / `apply_ohos_acrylic` / `apply_ohos_mica` +- **测试分类**: `manual` diff --git a/openspec/changes/archive/2026-07-04-p1-window-vibrancy/tasks.md b/openspec/changes/archive/2026-07-04-p1-window-vibrancy/tasks.md new file mode 100644 index 000000000000..057c8287bff5 --- /dev/null +++ b/openspec/changes/archive/2026-07-04-p1-window-vibrancy/tasks.md @@ -0,0 +1,30 @@ +## 1. openharmony-ability ArkTS 层 + +- [x] 1.1 在 `WindowManager.ets` 中新增 `setWindowBlur(windowId: number, radius: number): void` 方法,将 `backdropBlur(radius)` 应用到指定窗口的 WebView 容器组件,通过 @State 或 LocalStorage 动态更新 + +## 2. openharmony-ability Rust NAPI 层 + +- [x] 2.1 在 `crates/ability/src/window/mod.rs` 中新增 `set_window_blur(window_id: i64, radius: f64) -> napi_ohos::Result<()>` 函数,复用 `get_helper()` + `get_main_thread_env()` 模式调用 ArkTS `setWindowBlur` + +## 3. window-vibrancy OHOS 平台支持 + +- [x] 3.1 在 `window-vibrancy/Cargo.toml` 中添加 OHOS 依赖:`[target.'cfg(target_env = "ohos")'.dependencies] openharmony-ability = { path = "..." }` +- [x] 3.2 新建 `window-vibrancy/src/ohos.rs`,实现 `apply_ohos_blur` / `clear_ohos_blur` / `apply_ohos_acrylic` / `clear_ohos_acrylic` / `apply_ohos_mica` / `clear_ohos_mica`,内部调用 `openharmony_ability::set_window_blur` + `set_window_background_color` +- [x] 3.3 修改 `window-vibrancy/src/lib.rs`:添加 `#[cfg(target_env = "ohos")] mod ohos;` 和 `pub use ohos::*;`,在 Error 枚举中添加 OHOS 相关错误变体 + +## 4. tao OHOS 窗口效果支持 + +- [x] 4.1 ~~在 `tao/Cargo.toml` 的 OHOS 依赖中添加 `window-vibrancy`~~ → 撤销:effect 应用不经 tao(与 Windows/macOS 一致,由 tauri vibrancy 层直接调 window-vibrancy),tao 仅经既有 `WindowExtOpenHarmony::window_id()` 提供 window id +- [x] 4.2 在 `tauri-runtime/src/lib.rs` 的 `WindowDispatch` trait 中添加 `ohos_window_id()` 方法,在 `tauri-runtime-wry/src/lib.rs` 中添加 `WindowMessage::OhosWindowId` variant + handler + dispatcher 实现 +- [x] 4.3 ~~在 `tao/src/platform_impl/ohos/mod.rs` 中实现 `set_window_effects`~~ → 简化:tao 已提供 `WindowExtOpenHarmony::window_id()`,由 tauri vibrancy 层直接调用 window-vibrancy + +## 5. tauri-runtime dispatcher 扩展 + +- [x] 5.1 ~~在 `tauri-runtime/src/window.rs` 的 `WindowDispatch` trait 中添加 `set_window_effects(effects: WindowEffectsConfig)` 方法~~ → 简化为 `ohos_window_id()` (已在 4.2 实现) +- [x] 5.2 ~~在 `tauri-runtime-wry/src/lib.rs` 的 `WindowMessage` 枚举中添加 `SetEffects(WindowEffectsConfig)` variant~~ → 简化为 `OhosWindowId` (已在 4.2 实现) +- [x] 5.3 ~~在 `tauri-runtime-wry/src/lib.rs` 的 event loop handler 中添加 `WindowMessage::SetEffects` 处理分支~~ → 简化为 `OhosWindowId` handler (已在 4.2 实现) + +## 6. tauri vibrancy OHOS 集成 + +- [x] 6.1 修改 `tauri/crates/tauri/src/vibrancy/mod.rs`:添加 `#[cfg(target_env = "ohos")] mod ohos;` 和对应的 `ohos::apply_effects` / `ohos::clear_effects` 调用分支 +- [x] 6.2 新建 `tauri/crates/tauri/src/vibrancy/ohos.rs`,实现 `apply_effects` 函数:通过 dispatcher 获取 OHOS window_id,调用 `window_vibrancy::apply_ohos_blur` 等 API;实现 `clear_effects` 函数 diff --git a/openspec/changes/window-vibrancy-plan.md b/openspec/changes/window-vibrancy-plan.md new file mode 100644 index 000000000000..d3c34e792d5f --- /dev/null +++ b/openspec/changes/window-vibrancy-plan.md @@ -0,0 +1,76 @@ +# window-vibrancy 适配计划 + +**创建时间**:2026-06-16 +**最后更新**:2026-07-07 +**功能描述**:在 OHOS 上实现 Tauri 窗口模糊效果(Blur/Acrylic/Mica/Tabbed),通过 tauri/vibrancy → window-vibrancy → openharmony-ability → 组件级 backdropBlur 的调用链 +**判断依据**:涉及 3 个代码层(window-vibrancy + openharmony-ability + tauri),预估 13 个文件,不拆分 +**状态**:✓ 完整适配 + 设备端验证通过(2026-07-07)— 运行时 setEffects/clearEffects(AttributeUpdater 刷新 backdropBlur/backgroundColor)+ build 时 effects(WindowBuilder::effects)均生效 + +> **架构决策(2026-07-02)**:effect 应用**不经过 tao**,由 `tauri/vibrancy/ohos.rs` 直接调用 `window_vibrancy`,与 Windows/macOS 在 `tauri/vibrancy/mod.rs` 直接调用 `window_vibrancy` 的方式保持一致。tao 在本特性中仅通过既有的 `WindowExtOpenHarmony::window_id()` 提供 window ID,不新增 vibrancy 相关 API。原计划中 `tao/src/window.rs` 的 `set_window_effects` 方法与 `tao/src/platform_impl/ohos/mod.rs` 的 OHOS 实现均不再需要,`tao/Cargo.toml` 也不依赖 window-vibrancy。 + +## Phase 列表 + +| Phase | 名称 | openspec change | 状态 | 涉及层 | 预估文件 | 验证方式 | +|-------|------|----------------|------|--------|---------|---------| +| 1 | 窗口模糊效果适配 | p1-window-vibrancy | ✓ 已归档 + 设备验证通过 | window-vibrancy + openharmony-ability + tauri | 12 | 设备端验证模糊效果 | + +## Phase 详细说明 + +### Phase 1: 窗口模糊效果适配 +- **目标**:在 OHOS 上实现窗口模糊效果,保持 window-vibrancy 作为平台抽象层(与 Windows/macOS 架构一致) +- **架构**(实际实现): + ``` + tauri/vibrancy/ohos.rs + ├─ window.dispatcher.ohos_window_id() ← tauri-runtime → wry → tao::WindowExtOpenHarmony::window_id() + └─ window_vibrancy::apply_ohos_blur(window_id, radius) + → openharmony_ability::set_window_blur(window_id, radius) + → NAPI (FnArgs) → ArkTS ArkHelper.setWindowBlur(windowId, radius) + → WindowManager.applyWindowBlur → pendingBlurs.set(Number(windowId), radius) + → registerController 时注入 webview build 数据 style.blurRadius + → BuilderNode.build → .backdropBlur(blurRadius) 构建时生效 + ``` +- **文件列表**(实际实现): + 1. `openharmony-ability/native_ability/.../window/WindowManager.ets` — applyWindowBlur 排队 + registerController build 时注入 blurRadius + 2. `openharmony-ability/crates/ability/src/window/mod.rs` — set_window_blur NAPI 函数(用 FnArgs 传参) + 3. `window-vibrancy/Cargo.toml` — 添加 OHOS 依赖 openharmony-ability + 4. `window-vibrancy/src/ohos.rs` — 新建 OHOS 平台实现 + 5. `window-vibrancy/src/lib.rs` — 添加 OHOS 模块、pub use 与 OhosError(String) 变体 + 6. `tauri/crates/tauri-runtime/src/lib.rs` — WindowDispatch trait 添加 ohos_window_id() + 7. `tauri/crates/tauri-runtime-wry/src/lib.rs` — WindowMessage 添加 OhosWindowId + handler(经 tao 取 window id) + 8. `tauri/crates/tauri/src/vibrancy/ohos.rs` — 新建 OHOS 平台实现,直接调 window_vibrancy + 9. `tauri/crates/tauri/src/vibrancy/mod.rs` — 添加 OHOS 分支 + 10. `tauri/crates/tauri/src/window/mod.rs` — build_internal 中 OHOS 直接 apply effects(见 P3) + - ~~`tao/Cargo.toml` / `tao/src/window.rs` / `tao/src/platform_impl/ohos/mod.rs`~~ — 不再需要(见架构决策) +- **依赖**:无 +- **OHOS API**:组件级 `backdropBlur(radius)`(API 7+),非 Window.setWindowBlur(本地 SDK 中不存在) +- **验证方式**:设备端运行,确认窗口背景模糊效果可见(2026-07-04 验证通过) + +## 根因与修复(2026-07-04 设备端调试发现) + +vibrancy 在 OHOS 上从未真正生效过(p1 归档时的"设备端验证"是虚假的)。经 ~50 轮构建调试,发现两层根因: + +### 根因 1:napi-ohos Function::call 裸 tuple 传参 bug(根本原因) +- **现象**:`set_window_blur` 的 `func.call((window_id, radius))` 用裸 tuple,napi-ohos 1.2.0 的通用 `JsValuesTupleIntoVec` impl(`function.rs:19`)把整个 tuple 当成 **1 个** napi 值传。ArkHelper.setWindowBlur 收到 `(tuple对象, undefined)` → windowId=NaN, radius=undefined。blur 的值从未到达 ArkTS。 +- **诊断**:在 registerController 打印 pendingBlurs 的 key/value,发现 `keys=number:NaN=undefined`(key 是 NaN,value 是 undefined)。 +- **修复**:用 `FnArgs { data: (window_id, radius) }` 包裹 tuple,触发 `FnArgs` 专用的拆包 impl(`function.rs:55`),正确传 2 个参数。`Function<'_, (i64, f64), ()>` 改为 `Function<'_, FnArgs<(i64, f64)>, ()>`。 + +### 根因 2:hilog 在 NAPI 回调里抛 Argc mismatch(掩盖了根因 1) +- **现象**:原始 p1 setWindowBlur 方法体里有 `hilog.info(...)`,在 NAPI 回调上下文(ArkHelper.setWindowBlur 被 Rust NAPI 调)里调 hilog 会抛 "assertion (false) failed: Argc mismatch"。被 catch 吞成 `failed: {}`,看不到真正的参数问题。 +- **修复**:applyWindowBlur 内不用 hilog(NAPI 回调上下文禁用 hilog)。 + +### 根因 3:BuilderNode.update 不刷新 backdropBlur +- **现象**:`setAllWebviewsBlurRadius` 改 `entry.style.blurRadius` 后调 `BuilderNode.update(entry)`,但 backdropBlur 不刷新(SDK 文档:update 要求 @Prop 反应式)。 +- **修复**:在 `registerController` 的 `addWebview` 前把 blurRadius 注入 webview build 数据(`pendingInit.style.blurRadius`),让 `backdropBlur` 在构建时就生效,不依赖 update。 + +### 附带发现 +- **oh-package.json5 全角冒号**:gen/ohos/entry/oh-package.json5 曾有 `file:`(全角冒号)导致 ohpm 装了 registry 旧版 @ohos-rs/ability(不含 setWindowBlur)。`tauri ohos init` 重新生成后修复(模板是正确的 ASCII 冒号)。 +- **build 缓存多版本堆积**:每次改 openharmony-ability 源码重建,build cache 累积旧编译版本(最多 16 个),导致运行时可能加载旧版。需定期删 `entry/build` 清理。 +- **set_window_background_color / set_window_decorations 也有同样的 FnArgs bug**:它们也用裸 tuple `func.call((window_id, color))`,参数没传对。本次只修了 set_window_blur,其他两个待修。 + +## 已知遗留项 + +- **P3(已查清,非遗留)**:`tauri/crates/tauri/src/window/mod.rs` 中 OHOS 在 `run_on_main_thread` 之外直接 apply effects。根因:vibrancy 路径调用 `ohos_window_id()`,是阻塞 `rx.recv()` getter;而 OHOS 的 `run_on_main_thread` 把闭包调度到 Chrome_IOThread(非 ArkTS 主线程),阻塞 recv 会触发 Chrome_IOThread ↔ ArkTS 主线程互等死锁(见 `ohos-constraints.md`)。直接 apply 使 `send_user_message` 在主线程同步内联执行,recv 立即返回,符合 OHOS 约束。这是正确做法,非临时 workaround。非 OHOS 平台仍走 `run_on_main_thread`。 +- **待修复**:`set_window_background_color` 和 `set_window_decorations` 也有 napi-ohos 裸 tuple 传参 bug,需同样用 FnArgs 修复(影响 acrylic/mica 的背景色和 decorations 功能)。 +- **待观察(本特性外)**:tauri 中其他在 `run_on_main_thread` 闭包内使用阻塞 getter(`window_getter!`/`webview_getter!`)的调用路径,在 OHOS 上存在同样的死锁风险,需另行排查。 +- **测试代码**:`examples/api/src-tauri/src/lib.rs` 中有两个 vibrancy 对比测试窗口(vibrancy-blur / vibrancy-noblur)和 `examples/api/public/vibrancy.html` 透明测试页,用于验证。提交前可考虑简化为单个窗口或保留作为回归测试。 + diff --git a/openspec/specs/ohos-window-blur/spec.md b/openspec/specs/ohos-window-blur/spec.md new file mode 100644 index 000000000000..a8983e784752 --- /dev/null +++ b/openspec/specs/ohos-window-blur/spec.md @@ -0,0 +1,164 @@ +# ohos-window-blur Specification + +## Purpose +TBD - created by archiving change p1-window-vibrancy. Update Purpose after archive. +## Requirements +### Requirement: window-vibrancy OHOS 平台支持 + +`window-vibrancy` crate SHALL 新增 OHOS 平台支持。在 `cfg(target_env = "ohos")` 下依赖 `openharmony-ability`(与 Windows 依赖 `windows-sys`、macOS 依赖 `objc2-app-kit` 模式一致),提供 OHOS 专用 API。 + +#### Scenario: apply_ohos_blur 设置模糊 +- **WHEN** 调用 `window_vibrancy::apply_ohos_blur(window_id, radius)` +- **THEN** SHALL 调用 `openharmony_ability::set_window_blur(window_id, radius)` 将模糊效果应用到指定窗口的 WebView 容器组件 +- **测试分类**: `manual`(需人工确认模糊效果可见) + +#### Scenario: clear_ohos_blur 清除模糊 +- **WHEN** 调用 `window_vibrancy::clear_ohos_blur(window_id)` +- **THEN** SHALL 调用 `openharmony_ability::set_window_blur(window_id, 0.0)` 关闭模糊 +- **测试分类**: `side-effect`(验证模糊效果被移除) + +#### Scenario: apply_ohos_acrylic 设置亚克力效果 +- **WHEN** 调用 `window_vibrancy::apply_ohos_acrylic(window_id, radius, color)` +- **THEN** SHALL 调用 `openharmony_ability::set_window_blur(window_id, radius)` 并设置半透明背景色 +- **测试分类**: `manual` + +#### Scenario: apply_ohos_mica 设置 Mica 效果 +- **WHEN** 调用 `window_vibrancy::apply_ohos_mica(window_id, radius, dark)` +- **THEN** SHALL 调用 `openharmony_ability::set_window_blur(window_id, radius)` 并根据 dark 参数设置深浅背景色 +- **测试分类**: `manual` + +#### Scenario: 设备不支持时静默跳过 +- **WHEN** 调用 OHOS 模糊 API 但设备不支持 +- **THEN** SHALL 返回 `Ok(())`,不中断执行 +- **测试分类**: `auto` + +### Requirement: openharmony-ability 窗口模糊 NAPI 桥接 + +`openharmony-ability` SHALL 提供 `set_window_blur(window_id: i64, radius: f64) -> napi_ohos::Result<()>` NAPI 函数。该函数 SHALL 遵循 `set_window_background_color` 相同的桥接模式(`get_helper()` + `get_main_thread_env()` + ArkTS 函数调用)。 + +#### Scenario: 设置主窗口模糊 +- **WHEN** 调用 `set_window_blur(0, 20.0)`,windowId=0 表示主窗口 +- **THEN** ArkTS WindowManager SHALL 将 `backdropBlur(20)` 应用到主窗口的 WebView 容器组件 +- **测试分类**: `manual` + +#### Scenario: 设置子窗口模糊 +- **WHEN** 调用 `set_window_blur(id, 30.0)`,id 为已创建的子窗口 ID +- **THEN** ArkTS WindowManager SHALL 将 `backdropBlur(30)` 应用到对应子窗口的 WebView 容器组件 +- **测试分类**: `manual` + +#### Scenario: 模糊半径为 0 关闭模糊 +- **WHEN** 调用 `set_window_blur(window_id, 0.0)` +- **THEN** ArkTS WindowManager SHALL 将 `backdropBlur(0)` 应用到组件,关闭模糊效果 +- **测试分类**: `side-effect` + +### Requirement: ArkTS WindowManager setWindowBlur 方法 + +ArkTS `WindowManager` SHALL 新增 `setWindowBlur(windowId: number, radius: number): void` 方法。该方法 SHALL 将 `backdropBlur(radius)` 组件属性应用到指定窗口的 WebView 容器组件(`DefaultXComponent` 的外层 Stack)。 + +#### Scenario: 主窗口模糊(windowId=0) +- **WHEN** `windowId` 为 0 且 `windowStage` 已初始化 +- **THEN** SHALL 更新主窗口 WebView 容器的 blur 状态,使 `backdropBlur(radius)` 生效 +- **测试分类**: `manual` + +#### Scenario: 子窗口模糊 +- **WHEN** `windowId` 不为 0 且存在于 `windows` Map 中 +- **THEN** SHALL 更新对应子窗口 WebView 容器的 blur 状态 +- **测试分类**: `manual` + +#### Scenario: 窗口不存在时静默忽略 +- **WHEN** `windowId` 不在管理范围内 +- **THEN** SHALL 记录 warn 日志并返回,不抛异常 +- **测试分类**: `auto` + +### Requirement: Tauri vibrancy OHOS 平台实现 + +`tauri` crate SHALL 在 `vibrancy` 模块中新增 OHOS 平台实现,当 `cfg(target_env = "ohos")` 时调用 `window_vibrancy` 的 OHOS API。映射 SHALL 通过 dispatcher 消息链传递(`WindowDispatch::set_window_effects` → `WindowMessage::SetEffects` → event loop → tao `Window::set_window_effects`),与 `set_background_color` 的架构模式一致。 + +#### Scenario: Blur 效果映射 +- **WHEN** `WindowEffectsConfig.effects` 包含 `Effect::Blur` +- **THEN** SHALL 调用 `window_vibrancy::apply_ohos_blur(window_id, radius)` +- **THEN** radius SHALL 取 `WindowEffectsConfig.radius` 的值,若未指定则默认 20.0 +- **测试分类**: `manual` + +#### Scenario: Acrylic 效果映射 +- **WHEN** `WindowEffectsConfig.effects` 包含 `Effect::Acrylic` +- **THEN** SHALL 调用 `window_vibrancy::apply_ohos_acrylic(window_id, 25.0, color)` +- **测试分类**: `manual` + +#### Scenario: Mica 系列效果映射 +- **WHEN** `WindowEffectsConfig.effects` 包含 `Effect::Mica` / `MicaDark` / `MicaLight` +- **THEN** SHALL 调用 `window_vibrancy::apply_ohos_mica(window_id, 20.0, dark)` +- **测试分类**: `manual` + +#### Scenario: Tabbed 系列效果映射 +- **WHEN** `WindowEffectsConfig.effects` 包含 `Effect::Tabbed` / `TabbedDark` / `TabbedLight` +- **THEN** SHALL 采用与 Mica 系列相同的映射策略 +- **测试分类**: `manual` + +#### Scenario: 清除效果 +- **WHEN** `set_window_effects` 传入 `effects: None` +- **THEN** SHALL 调用 `window_vibrancy::clear_ohos_blur(window_id)` 关闭模糊 +- **测试分类**: `side-effect` + +#### Scenario: 窗口创建时自动应用效果 +- **WHEN** `WindowAttributes.window_effects` 已配置 +- **THEN** 窗口创建完成后 SHALL 自动调用 `set_window_effects` 应用效果(已有代码路径) +- **测试分类**: `manual` + +### Requirement: Effect 类型优先级 + +当 `WindowEffectsConfig.effects` 包含多个 Effect 时,SHALL 取第一个可映射的 Effect 并忽略其余,与 Windows/macOS 行为一致。 + +#### Scenario: 多 Effect 取首个 +- **WHEN** `effects` 为 `[Effect::Acrylic, Effect::Blur]` +- **THEN** SHALL 仅应用 Acrylic 效果,忽略 Blur +- **测试分类**: `auto` + +#### Scenario: 无可映射 Effect 时静默跳过 +- **WHEN** `effects` 列表为空或全部为 macOS 专属材质 +- **THEN** SHALL 不调用任何 OHOS API,不返回错误 +- **测试分类**: `auto` + +### Requirement: Dispatcher 消息链支持窗口效果 + +`WindowDispatch` trait SHALL 新增 `set_window_effects(effects: Option)` 方法,通过 `WindowMessage::SetEffects` 转发到 event loop handler,handler 调用 tao `Window::set_window_effects`。此模式与 `set_background_color` 一致。 + +#### Scenario: set_window_effects 消息传递 +- **WHEN** tauri `Window::set_effects()` 被调用 +- **THEN** SHALL 通过 dispatcher 发送 `WindowMessage::SetEffects(effects)` 消息 +- **THEN** event loop handler SHALL 调用 `tao_window.set_window_effects(effects)` +- **测试分类**: `auto` + +#### Scenario: tao OHOS Window 处理窗口效果 +- **WHEN** tao `Window::set_window_effects` 被调用且 `self.window_id` 不为 None +- **THEN** SHALL 根据 Effect 类型调用 `window_vibrancy::apply_ohos_blur` / `apply_ohos_acrylic` / `apply_ohos_mica` +- **测试分类**: `manual` + +### Requirement: 运行时 backdropBlur/backgroundColor 刷新(AttributeUpdater) + +`openharmony-ability` SHALL 用 `AttributeUpdater`(BlurModifier)运行时刷新 `backdropBlur`/`backgroundColor`,因为 `BuilderNode.update` 不刷新组件属性。`@Builder` 函数内不能用 `@State`,`AttributeUpdater` 的 `attribute?.backdropBlur(radius)` 不需 @State 即可立即触发组件更新。 + +#### Scenario: 运行时 setEffects 刷新 backdropBlur +- **WHEN** `Window::set_effects` 调用(runtime) +- **THEN** SHALL 通过 `run_on_main_thread` 在 main thread 执行 `set_window_blur`(thread_local `MAIN_THREAD_ENV` 可用) +- **AND** SHALL 通过 `BlurModifier.attribute?.backdropBlur(radius)` 立即刷新组件 backdropBlur +- **测试分类**: `manual`(需人工确认模糊效果可见) + +#### Scenario: 运行时 setEffects 刷新 backgroundColor(Acrylic/Mica tint) +- **WHEN** `set_window_background_color` 调用(Acrylic/Mica tint) +- **THEN** SHALL 通过 `BlurModifier.attribute?.backgroundColor(color)` 立即刷新组件 backgroundColor +- **AND** SHALL 用 `FnArgs<(i64, u32)>` 调用 NAPI(裸 tuple 只传 1 个参数,7bd67be 修了 set_window_blur 但漏了 set_window_background_color) +- **测试分类**: `manual`(需人工确认 tint 可见) + +#### Scenario: build-time effects 在窗口创建时应用 +- **WHEN** `WindowBuilder::effects` 配置 effects +- **THEN** SHALL 在 `WindowBuilder::build` 时通过 `run_on_main_thread` apply effects +- **AND** SHALL 通过 `registerController` inject blurRadius 到 build data(pendingBlurs),`backdropBlur` 在 Stack build 时设置 +- **测试分类**: `manual`(窗口出现时即模糊) + +#### Scenario: BlurModifier initializeModifier 不覆盖 build-time backdropBlur +- **WHEN** BlurModifier 绑定到 Stack +- **THEN** `initializeModifier` SHALL 不设置 backdropBlur(让 build 时 `Stack.backdropBlur(data.style.blurRadius)` 生效) +- **AND** 运行时 `attribute?.backdropBlur(radius)` SHALL 覆盖 build 时值 +- **测试分类**: `side-effect`(build-time effects 用例验证不抛错) +