Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions crates/ability/src/render/xcomponent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ pub fn render(
crate::error!("init_clipboard_tsfn failed: {}", e);
}

// Initialize vibrancy ThreadsafeFunctions (set_window_blur / set_window_background_color)
// for cross-thread calls without needing the main-thread thread_local Env.
if let Err(e) = crate::window::init_vibrancy_tsfn(env) {
crate::error!("init_vibrancy_tsfn failed: {}", e);
}

// Initialize permission request threadsafe function
let _ = create_permission_request_tsfn(env);

Expand Down
97 changes: 79 additions & 18 deletions crates/ability/src/window/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use crate::{get_helper, get_main_thread_env};
use napi_ohos::bindgen_prelude::*;
use napi_ohos::threadsafe_function::{ThreadsafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode};
use napi_ohos::Env;
use crate::{get_helper, get_main_thread_env};
use std::sync::atomic::{AtomicI64, Ordering};
use std::sync::OnceLock;

/// Global window ID generator to ensure unique IDs across Rust and ArkTS.
static NEXT_WINDOW_ID: AtomicI64 = AtomicI64::new(1);
Expand Down Expand Up @@ -165,26 +168,84 @@ pub fn set_window_decorations(window_id: i64, decorations: bool) -> napi_ohos::R
/// `color` is in `0xAARRGGBB` format (e.g., `0x00000000` = fully transparent).
///
/// Phase 3 implementation.

// ─── TSFN for cross-thread vibrancy calls (threadsafe, no main-thread Env needed) ───
// Fire-and-forget (NonBlocking, no return value wait): applyWindowBlur queues pendingBlurs
// (build-time inject via registerController) or calls setAllWebviewsBlurRadius (runtime
// modifier refresh), both idempotent, so no synchronous result needed.
type SetWindowBlurTsfn = ThreadsafeFunction<(i64, f64), (), FnArgs<(i64, f64)>, Status, false>;
type SetWindowBgColorTsfn = ThreadsafeFunction<(i64, u32), (), FnArgs<(i64, u32)>, Status, false>;

static TSFN_SET_WINDOW_BLUR: OnceLock<SetWindowBlurTsfn> = OnceLock::new();
static TSFN_SET_WINDOW_BG_COLOR: OnceLock<SetWindowBgColorTsfn> = OnceLock::new();

/// Initialize vibrancy ThreadsafeFunctions. Must be called on ArkTS main thread (during
/// ArkHelper setup, like init_clipboard_tsfn). After init, set_window_blur /
/// set_window_background_color are callable from any thread (TSFN is threadsafe, does not
/// need the thread_local MAIN_THREAD_ENV, so no run_on_main_thread required).
pub fn init_vibrancy_tsfn(env: &Env) -> Result<()> {
if TSFN_SET_WINDOW_BLUR.get().is_some() {
return Ok(());
}
let helper_obj = {
let helper_rc = unsafe { get_helper() };
let helper_guard = helper_rc.borrow();
let helper_ref = helper_guard
.as_ref()
.ok_or_else(|| Error::from_reason("ArkHelper not initialized"))?;
helper_ref.get_value(env)?
};

let blur_fn: Function<'_, FnArgs<(i64, f64)>, ()> = helper_obj
.get_named_property("setWindowBlur")
.map_err(|e| Error::from_reason(format!("setWindowBlur not found: {}", e)))?;
let blur_tsfn = blur_fn
.build_threadsafe_function::<(i64, f64)>()
.callee_handled::<false>()
.build_callback(move |ctx: ThreadsafeCallContext<(i64, f64)>| {
Ok(FnArgs { data: ctx.value })
})?;
let _ = TSFN_SET_WINDOW_BLUR.set(blur_tsfn);

let bg_fn: Function<'_, FnArgs<(i64, u32)>, ()> = helper_obj
.get_named_property("setWindowBackgroundColor")
.map_err(|e| Error::from_reason(format!("setWindowBackgroundColor not found: {}", e)))?;
let bg_tsfn = bg_fn
.build_threadsafe_function::<(i64, u32)>()
.callee_handled::<false>()
.build_callback(move |ctx: ThreadsafeCallContext<(i64, u32)>| {
Ok(FnArgs { data: ctx.value })
})?;
let _ = TSFN_SET_WINDOW_BG_COLOR.set(bg_tsfn);

Ok(())
}

/// Sets window background color via TSFN (threadsafe, callable from any thread).
pub fn set_window_background_color(window_id: i64, color: u32) -> napi_ohos::Result<()> {
let ret = unsafe { get_helper() };
if let Some(h) = ret.borrow().as_ref() {
if let Some(env) = get_main_thread_env().borrow().as_ref() {
let obj = h.get_value(env).map_err(|e| {
crate::error!("Failed to get helper object value: {:?}", e);
e
})?;
let tsfn = TSFN_SET_WINDOW_BG_COLOR.get()
.ok_or_else(|| Error::from_reason("set_window_background_color TSFN not initialized"))?;
let status = tsfn.call((window_id, color), ThreadsafeFunctionCallMode::NonBlocking);
if status != Status::Ok {
return Err(Error::from_reason(format!("TSFN call failed: {:?}", status)));
}
Ok(())
}

let func =
obj.get_named_property::<Function<'_, (i64, u32), ()>>("setWindowBackgroundColor")?;
func.call((window_id, color))?;
return Ok(());
} else {
crate::error!("Main thread env not available");
}
} else {
crate::error!("Helper object not initialized");
/// Sets window blur radius via TSFN (threadsafe, callable from any thread).
///
/// Calls ArkTS `setWindowBlur(windowId, radius)` handler which applies
/// `backdropBlur(radius)` to the WebView container component.
///
/// `radius` is the blur radius in pixels (0 = no blur).
pub fn set_window_blur(window_id: i64, radius: f64) -> napi_ohos::Result<()> {
let tsfn = TSFN_SET_WINDOW_BLUR.get()
.ok_or_else(|| Error::from_reason("set_window_blur TSFN not initialized"))?;
let status = tsfn.call((window_id, radius), ThreadsafeFunctionCallMode::NonBlocking);
if status != Status::Ok {
return Err(Error::from_reason(format!("TSFN call failed: {:?}", status)));
}
Err(Error::from_reason("Helper or Env not initialized"))
Ok(())
}

/// Brings a Float sub-window to the front and focuses it.
Expand Down
21 changes: 21 additions & 0 deletions native_ability/src/main/ets/ability/ArkHelper.ets
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,9 @@ export function createArkHelper(): ArkHelper {
ret.controller.setBounds = (x: number, y: number, width: number, height: number) => {
applyStyle({ x, y, width, height });
};
ret.controller.setBlurRadius = (radius: number) => {
applyStyle({ blurRadius: radius });
};

ret.controller.dispose = () => {
targetController!.removeWebview(ret.webTag);
Expand Down Expand Up @@ -398,6 +401,9 @@ export function createArkHelper(): ArkHelper {
ret.controller.setBounds = (x: number, y: number, width: number, height: number) => {
applyStyle({ x, y, width, height });
};
ret.controller.setBlurRadius = (radius: number) => {
applyStyle({ blurRadius: radius });
};

ret.controller.dispose = () => {
manager.removeWebview(ret.webTag);
Expand Down Expand Up @@ -472,6 +478,21 @@ export function createArkHelper(): ArkHelper {
}
},

setWindowBlur: (windowId: number, radius: number): void => {
try {
WindowManager.getInstance().applyWindowBlur(windowId, radius);
} catch (_err) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 [F3] 此注释声称“error is surfaced via the Rust-side set_window_blur Err return instead”,但实际 catch 块不 rethrow,ArkTS setWindowBlur 正常返回 void → Rust func.call(FnArgs{...}) 返回 Ok(())。错误在两侧都丢失,并未上报到 Rust。

这违反 F3(ArkTS↔Rust 错误传播对称性):ArkTS 调用失败被吞,Rust 仍认为已生效。

建议:catch 中 throw _err 让错误经 func.call 返回 Err 上报到 Rust;或若确需静默,修正注释不要声称“经 Rust Err 上报”。

// Intentionally no hilog here: this is called from Rust NAPI (NAPI-reentrant ArkTS
// context), where hilog throws "Argc mismatch" and would mask the original error.
// Note: the error is NOT propagated to Rust — set_window_blur uses TSFN fire-and-forget
// (NonBlocking, no return value wait), so Rust receives Ok regardless of ArkTS failure.
// applyWindowBlur has internal fallbacks (queue pendingBlurs for build-time inject,
// modifier refresh for runtime), so a throw here is non-fatal. If synchronous error
// propagation is needed later, switch to call_with_return_value + oneshot (like
// clipboard_write_image).
}
},

loadUrl: (id: number, url: string): void => {
try {
hilog.info(DOMAIN, 'ArkHelper', 'loadUrl called for window %{public}d: %{public}s', id, url);
Expand Down
1 change: 1 addition & 0 deletions native_ability/src/main/ets/ability/type.ets
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ export interface ArkHelper {
setWindowBackgroundColor: (windowId: number, color: number) => void;
focusWindow: (windowId: number) => void;
setWindowFocusable: (windowId: number, focusable: boolean) => void;
setWindowBlur: (windowId: number, radius: number) => void;
loadUrl: (id: number, url: string) => void;
createMenu?: (options: MenuCreateOptions) => Promise<string>;
createMenuItem?: (options: MenuItemCreateOptions) => Promise<string>;
Expand Down
96 changes: 83 additions & 13 deletions native_ability/src/main/ets/webview/DefaultWebview.ets
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { hilog } from '@kit.PerformanceAnalysisKit';
import { randomString } from "../helper";
import { OnDownloadStartResult, OnWindowNewResult } from "../ability/type";
import { getCookies, setCookie, setWebDebuggingAccess, isWebDebuggingAccess, JsHelper, convertRRGGBBAAtoAARRGGBB, SnapshotData } from "./Utils";
import { WebHeader } from "@kit.ArkUI";
import { WebHeader, AttributeUpdater } from "@kit.ArkUI";
import { router } from '@kit.ArkUI';
import { WindowManager } from '../window/WindowManager';
import { DOMAIN } from '../helper/constants';
Expand Down Expand Up @@ -79,13 +79,27 @@ function handleWindowNew(
}
}

/**
* AttributeUpdater for runtime backdropBlur refresh.
* BuilderNode.update does not refresh backdropBlur, so we use AttributeUpdater to
* directly modify the Stack's backdropBlur attribute at runtime (setWindowBlur path).
* attribute?.backdropBlur(radius) triggers an immediate component update without @State.
*/
export class BlurModifier extends AttributeUpdater<CommonAttribute> {
initializeModifier(_instance: CommonAttribute): void {
// Intentionally empty: let the build-time Stack.backdropBlur(data.style.blurRadius) take
// effect. Runtime setWindowBlur refreshes via attribute?.backdropBlur(radius).
}
}

export interface WebviewStyle {
x?: number | string;
y?: number | string;
width?: number | string;
height?: number | string;
backgroundColor?: string | Color | number;
visible?: boolean;
blurRadius?: number;
}

export interface WebviewInitData {
Expand Down Expand Up @@ -117,24 +131,29 @@ export interface WebviewInitData {
interface WebviewNodeData extends WebviewInitData {
controller: WebviewController;
didInitialLoad?: boolean;
blurModifier?: BlurModifier;
}

@Builder
function WebBuilder(data: WebviewNodeData) {
// init with empty url and reload with loadUrl or loadData with onControllerAttach
Web({
src: "",
controller: data.controller as web_webview.WebviewController,
renderMode: data?.transparent ? RenderMode.SYNC_RENDER : RenderMode.ASYNC_RENDER
})
.width(data.style?.width ?? "100%")
.height(data.style?.height ?? "100%")
.position({
x: data.style?.x ?? 0,
y: data.style?.y ?? 0,
// NOTE: data.blurModifier is initialized in addWebview (cannot assign inside @Builder).
// Runtime setWindowBlur refreshes backdropBlur via modifier.attribute?.backdropBlur(radius)
// because BuilderNode.update does not refresh backdropBlur.
Stack() {
Web({
src: "",
controller: data.controller as web_webview.WebviewController,
renderMode: data?.transparent ? RenderMode.SYNC_RENDER : RenderMode.ASYNC_RENDER
})
.backgroundColor(convertRRGGBBAAtoAARRGGBB(data?.style?.backgroundColor))
.visibility(data?.style?.visible === false ? Visibility.Hidden : Visibility.Visible)
.width(data.style?.width ?? "100%")
.height(data.style?.height ?? "100%")
.position({
x: data.style?.x ?? 0,
y: data.style?.y ?? 0,
})
.backgroundColor(convertRRGGBBAAtoAARRGGBB(data?.style?.backgroundColor))
.visibility(data?.style?.visible === false ? Visibility.Hidden : Visibility.Visible)
.javaScriptAccess(data?.javascriptEnable)
.domStorageAccess(true)
.mediaPlayGestureAccess(
Expand Down Expand Up @@ -243,6 +262,12 @@ function WebBuilder(data: WebviewNodeData) {
.onWindowNew((event) => {
handleWindowNew(event, data, 'WebBuilder');
});
}
.width("100%")
.height("100%")
.backgroundColor(Color.Transparent)
.backdropBlur(data?.style?.blurRadius ?? 0)
.attributeModifier(data.blurModifier)
}

@Builder
Expand Down Expand Up @@ -654,6 +679,7 @@ function buildJsHelper(controller: WebviewController): JsHelper {
setBackgroundColor: (_color: number) => {},
setVisible: (_visible: boolean) => {},
setBounds: (_x: number, _y: number, _width: number, _height: number) => {},
setBlurRadius: (_radius: number) => {},
dispose: () => {},
createPdf,
} as JsHelper;
Expand All @@ -679,9 +705,39 @@ export class RustWebviewNodeController extends NodeController {

const newEntry: WebviewNodeData = { ...entry, style: style };
this.webviewEntries.set(webTag, newEntry);
// Runtime refresh via AttributeUpdater — node.update does not refresh backdropBlur/backgroundColor,
// so Acrylic/Mica tints (set_window_background_color) and blur radius need modifier.attribute to
// trigger an immediate component update.
if (entry.blurModifier) {
if (style.backgroundColor !== undefined && style.backgroundColor !== null) {
entry.blurModifier.attribute?.backgroundColor(style.backgroundColor);
}
if (style.blurRadius !== undefined) {
entry.blurModifier.attribute?.backdropBlur(style.blurRadius);
}
}
node.update(newEntry);
}

/**
* Update blur radius for all webviews managed by this controller.
* Called by WindowManager.setWindowBlur() to apply window-level blur effect.
*/
setAllWebviewsBlurRadius(radius: number): void {
for (const [webTag, entry] of this.webviewEntries) {
// Runtime refresh via AttributeUpdater — BuilderNode.update does not refresh backdropBlur,
// so call modifier.attribute?.backdropBlur(radius) which triggers an immediate update.
if (entry.blurModifier) {
entry.blurModifier.attribute?.backdropBlur(radius);
}
// Keep style.blurRadius in sync so any subsequent re-build preserves the value.
if (!entry.style) {
entry.style = {};
}
entry.style.blurRadius = radius;
}
}

private buildData(controller: WebviewController) {
return buildJsHelper(controller);
}
Expand All @@ -699,6 +755,9 @@ export class RustWebviewNodeController extends NodeController {
data.uiContext = this.uiContext;
}
const prepared = ensureWebviewNodeData(data);
if (!prepared.blurModifier) {
prepared.blurModifier = new BlurModifier();
}
const webTag = prepared.webTag!;
if (this.rootNode === null) {
this.rootNode = new FrameNode(this.uiContext!);
Expand Down Expand Up @@ -785,6 +844,16 @@ export class EmbeddedWebviewManager {
entry.content.update(newData);
}

/**
* Update blur radius for all embedded webviews managed by this manager.
* NOTE: EmbeddedWebBuilder does not attach a BlurModifier and content.update does not refresh
* backdropBlur, so this is a no-op. Vibrancy is only supported via RustWebviewNodeController
* (WebBuilder path). Kept as no-op to satisfy the controller interface.
*/
setAllWebviewsBlurRadius(_radius: number): void {
// No-op: embedded webviews do not support runtime backdropBlur refresh.
}

removeWebview(webTag: string) {
const entry = this.entries.get(webTag);
if (!entry) {
Expand All @@ -802,6 +871,7 @@ declare class WebviewController extends web_webview.WebviewController {
setBackgroundColor: (color: number) => void;
setVisible: (visible: boolean) => void;
setBounds: (x: number, y: number, width: number, height: number) => void;
setBlurRadius: (radius: number) => void;
dispose: () => void;
clearAllBrowsingData: () => void;
}
9 changes: 9 additions & 0 deletions native_ability/src/main/ets/webview/Utils.ets
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export interface JsHelper {
setBackgroundColor: (color: number) => void;
setVisible: (visible: boolean) => void;
setBounds: (x: number, y: number, width: number, height: number) => void;
setBlurRadius: (radius: number) => void;
dispose: () => void;
clearAllBrowsingData: () => void;
webPageSnapshot: () => Promise<SnapshotData>;
Expand Down Expand Up @@ -210,6 +211,14 @@ export class ProxyJsHelper implements JsHelper {
}
}

setBlurRadius(radius: number): void {
if (this.realController) {
this.realController.setBlurRadius(radius);
} else {
this.pendingOperations.push(() => this.realController!.setBlurRadius(radius));
}
}

dispose(): void {
if (this.realController) {
this.realController.dispose();
Expand Down
Loading