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
264 changes: 231 additions & 33 deletions src/apps/desktop/src/api/browser_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@
//!
//! Browser webviews are created as native child webviews by this desktop
//! adapter so stream-specific initialization can run before page scripts.
//!
//! On OHOS the desktop child-webview APIs (`app.get_webview(label)`,
//! `window.add_child(...)`) are unavailable, so the `browser_webview_*`
//! commands route through ArkTS callbacks registered by
//! `EntryAbility.onWindowStageCreate` (see `BrowserWebviewService.ets`).
//! The Rust side serializes the request to JSON, calls the registered
//! `ThreadsafeFunction` via `JS_THREADSAFE_FUNCTION`, awaits the returned
//! `Promise<String>` envelope (`{ok:true}` / `{ok:true,result:"..."}` /
//! `{error:"..."}`), and decodes it uniformly. Page-load lifecycle is
//! forwarded back to the web-ui by the ArkTS service calling the
//! `emit_browser_page_load` `#[napi]` function (mirrors the desktop
//! `.on_page_load` handler in `lib.rs`).

use serde::Deserialize;
use tauri::Manager;
Expand Down Expand Up @@ -66,6 +78,7 @@ fn find_browser_webview(app: &tauri::AppHandle, label: &str) -> Result<tauri::We
}
#[cfg(target_env = "ohos")]
{
let _ = app;
Err("Unable to find browser webview".to_owned())
}
}
Expand Down Expand Up @@ -129,6 +142,61 @@ fn validate_webview_bounds(x: f64, y: f64, width: f64, height: f64) -> Result<()
}
}

// #region OHOS ArkTS bridge helpers
// Only compiled on the OHOS target. On desktop the child-webview APIs are used
// directly (no ArkTS bridge involved).

#[cfg(target_env = "ohos")]
async fn ohos_browser_call(name: &str, json_arg: &str) -> Result<String, String> {
use bitfun_core::util::JS_THREADSAFE_FUNCTION;
let function = {
let lock = JS_THREADSAFE_FUNCTION.read();
lock.get(name).cloned()
};
let Some(function) = function else {
return Err(format!("{name} has not been registered by ArkTS"));
};
let res = function.call_async(Ok(json_arg.to_string())).await;
match res {
Ok(promise) => match promise.await {
Ok(json) => Ok(json),
Err(err) => Err(err.to_string()),
},
Err(err) => Err(err.to_string()),
}
}

/// Decode the JSON envelope returned by `BrowserWebviewService` methods.
/// - `{ok:true}` / `{ok:true,result:"..."}` → `Ok(())`
/// - `{error:"..."}` → `Err(error)`
/// Anything else surfaces as an unexpected-response error so the frontend's
/// `setError` shows a meaningful message instead of a silent failure.
#[cfg(target_env = "ohos")]
fn decode_ok_envelope(response: &str) -> Result<(), String> {
let value: serde_json::Value = serde_json::from_str(response)
.map_err(|e| format!("invalid json response from ArkTS: {e}: {response}"))?;
if value.get("ok").and_then(|v| v.as_bool()).unwrap_or(false) {
Ok(())
} else if let Some(error) = value.get("error").and_then(|v| v.as_str()) {
Err(error.to_owned())
} else {
Err(format!("unexpected response from ArkTS: {response}"))
}
}

/// Shared URL-scheme validation for create / navigate. Returns the parsed
/// `tauri::Url` so the caller can re-use it on desktop or discard it on OHOS.
fn parse_browser_url(raw: &str) -> Result<tauri::Url, String> {
let url = raw
.parse::<tauri::Url>()
.map_err(|e| format!("invalid url: {e}"))?;
match url.scheme() {
"http" | "https" => Ok(url),
scheme => Err(format!("unsupported protocol: {scheme}")),
}
}
// #endregion

#[tauri::command]
pub async fn browser_webview_create(
app: tauri::AppHandle,
Expand All @@ -139,14 +207,7 @@ pub async fn browser_webview_create(
validate_browser_label(&request.label)?;
validate_webview_bounds(request.x, request.y, request.width, request.height)?;

let url = request
.url
.parse::<tauri::Url>()
.map_err(|e| format!("invalid url: {e}"))?;
match url.scheme() {
"http" | "https" => {}
scheme => return Err(format!("unsupported protocol: {scheme}")),
}
let url = parse_browser_url(&request.url)?;

let window = app
.get_window("main")
Expand All @@ -163,20 +224,28 @@ pub async fn browser_webview_create(
}

let webview = window
.add_child(
builder,
tauri::LogicalPosition::new(request.x, request.y),
tauri::LogicalSize::new(request.width, request.height),
)
.map_err(|e| format!("failed to create browser webview: {e}"))?;
.add_child(
builder,
tauri::LogicalPosition::new(request.x, request.y),
tauri::LogicalSize::new(request.width, request.height),
)
.map_err(|e| format!("failed to create browser webview: {e}"))?;

webview
.hide()
.map_err(|e| format!("failed to hide browser webview before positioning: {e}"))
}

#[cfg(target_env = "ohos")]
{
Err("Unable to find browser webview".to_string())
let _ = app;
validate_browser_label(&request.label)?;
validate_webview_bounds(request.x, request.y, request.width, request.height)?;
let _ = parse_browser_url(&request.url)?;
let json = serde_json::to_string(&request)
.map_err(|e| format!("failed to encode request: {e}"))?;
let response = ohos_browser_call("browser_webview_create_ohos", &json).await?;
decode_ok_envelope(&response)
}
}

Expand All @@ -194,7 +263,12 @@ pub async fn browser_webview_eval(

#[cfg(target_env = "ohos")]
{
Err("Unable to find browser webview".to_string())
let _ = app;
validate_browser_label(&request.label)?;
let json = serde_json::to_string(&request)
.map_err(|e| format!("failed to encode request: {e}"))?;
let response = ohos_browser_call("browser_webview_eval_ohos", &json).await?;
decode_ok_envelope(&response)
}

}
Expand All @@ -204,19 +278,26 @@ pub async fn browser_webview_navigate(
app: tauri::AppHandle,
request: WebviewNavigateRequest,
) -> Result<(), String> {
let url = request
.url
.parse::<tauri::Url>()
.map_err(|e| format!("invalid url: {e}"))?;
let _ = parse_browser_url(&request.url)?;

match url.scheme() {
"http" | "https" => {}
scheme => return Err(format!("unsupported protocol: {scheme}")),
#[cfg(not(target_env = "ohos"))]
{
let url = request.url.parse::<tauri::Url>()
.map_err(|e| format!("invalid url: {e}"))?;
find_browser_webview(&app, &request.label)?
.navigate(url)
.map_err(|e| format!("navigate failed: {e}"))
}

find_browser_webview(&app, &request.label)?
.navigate(url)
.map_err(|e| format!("navigate failed: {e}"))
#[cfg(target_env = "ohos")]
{
let _ = app;
validate_browser_label(&request.label)?;
let json = serde_json::to_string(&request)
.map_err(|e| format!("failed to encode request: {e}"))?;
let response = ohos_browser_call("browser_webview_navigate_ohos", &json).await?;
decode_ok_envelope(&response)
}
}

#[derive(Debug, Deserialize)]
Expand All @@ -230,9 +311,22 @@ pub async fn browser_webview_reload(
app: tauri::AppHandle,
request: WebviewLabelRequest,
) -> Result<(), String> {
find_browser_webview(&app, &request.label)?
.reload()
.map_err(|e| format!("reload failed: {e}"))
#[cfg(not(target_env = "ohos"))]
{
find_browser_webview(&app, &request.label)?
.reload()
.map_err(|e| format!("reload failed: {e}"))
}

#[cfg(target_env = "ohos")]
{
let _ = app;
validate_browser_label(&request.label)?;
let json = serde_json::to_string(&request)
.map_err(|e| format!("failed to encode request: {e}"))?;
let response = ohos_browser_call("browser_webview_reload_ohos", &json).await?;
decode_ok_envelope(&response)
}
}

#[tauri::command]
Expand All @@ -244,9 +338,9 @@ pub async fn browser_webview_set_bounds(
{
validate_webview_bounds(request.x, request.y, request.width, request.height)?;

let webview = app
.get_webview(&request.label)
.ok_or_else(|| format!("Webview not found: {}", request.label))?;
let webview = app
.get_webview(&request.label)
.ok_or_else(|| format!("Webview not found: {}", request.label))?;

webview
.set_bounds(tauri::Rect {
Expand All @@ -258,11 +352,115 @@ pub async fn browser_webview_set_bounds(

#[cfg(target_env = "ohos")]
{
Err("invalid webview bounds".to_string())
let _ = app;
validate_browser_label(&request.label)?;
validate_webview_bounds(request.x, request.y, request.width, request.height)?;
let json = serde_json::to_string(&request)
.map_err(|e| format!("failed to encode request: {e}"))?;
let response = ohos_browser_call("browser_webview_set_bounds_ohos", &json).await?;
decode_ok_envelope(&response)
}

}

// Handle operations that the frontend drives via the command-based
// `BrowserWebviewHandle` (createCommandBasedBrowserWebviewHandle). On desktop
// these resolve the Tauri child webview by label and call its native method;
// on OHOS they route through the ArkTS `BrowserWebviewService` via the
// `ohos_browser_call` bridge. Using commands instead of `Webview.getByLabel`
// from `@tauri-apps/api/webview` avoids the Tauri webview registry entirely
// — ArkUI Web components created by `RustWebviewNodeController.addWebview`
// are invisible to that registry, so `getByLabel` returns null / throws on
// OHOS. The command path works uniformly on both platforms.

#[tauri::command]
pub async fn browser_webview_show(
app: tauri::AppHandle,
request: WebviewLabelRequest,
) -> Result<(), String> {
#[cfg(not(target_env = "ohos"))]
{
find_browser_webview(&app, &request.label)?
.show()
.map_err(|e| format!("show failed: {e}"))
}
#[cfg(target_env = "ohos")]
{
let _ = app;
validate_browser_label(&request.label)?;
let json = serde_json::to_string(&request)
.map_err(|e| format!("failed to encode request: {e}"))?;
let response = ohos_browser_call("browser_webview_show_ohos", &json).await?;
decode_ok_envelope(&response)
}
}

#[tauri::command]
pub async fn browser_webview_hide(
app: tauri::AppHandle,
request: WebviewLabelRequest,
) -> Result<(), String> {
#[cfg(not(target_env = "ohos"))]
{
find_browser_webview(&app, &request.label)?
.hide()
.map_err(|e| format!("hide failed: {e}"))
}
#[cfg(target_env = "ohos")]
{
let _ = app;
validate_browser_label(&request.label)?;
let json = serde_json::to_string(&request)
.map_err(|e| format!("failed to encode request: {e}"))?;
let response = ohos_browser_call("browser_webview_hide_ohos", &json).await?;
decode_ok_envelope(&response)
}
}

#[tauri::command]
pub async fn browser_webview_close(
app: tauri::AppHandle,
request: WebviewLabelRequest,
) -> Result<(), String> {
#[cfg(not(target_env = "ohos"))]
{
find_browser_webview(&app, &request.label)?
.close()
.map_err(|e| format!("close failed: {e}"))
}
#[cfg(target_env = "ohos")]
{
let _ = app;
validate_browser_label(&request.label)?;
let json = serde_json::to_string(&request)
.map_err(|e| format!("failed to encode request: {e}"))?;
let response = ohos_browser_call("browser_webview_close_ohos", &json).await?;
decode_ok_envelope(&response)
}
}

#[tauri::command]
pub async fn browser_webview_set_focus(
app: tauri::AppHandle,
request: WebviewLabelRequest,
) -> Result<(), String> {
#[cfg(not(target_env = "ohos"))]
{
find_browser_webview(&app, &request.label)?
.set_focus()
.map_err(|e| format!("set_focus failed: {e}"))
}
#[cfg(target_env = "ohos")]
{
let _ = app;
validate_browser_label(&request.label)?;
let json = serde_json::to_string(&request)
.map_err(|e| format!("failed to encode request: {e}"))?;
let response = ohos_browser_call("browser_webview_set_focus_ohos", &json).await?;
decode_ok_envelope(&response)
}
}

/// Return the current URL of a browser webview.
///
/// Uses `catch_unwind` to guard against a known wry bug where
Expand Down
4 changes: 4 additions & 0 deletions src/apps/desktop/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1571,6 +1571,10 @@ pub async fn _run() {
api::browser_api::browser_webview_navigate,
api::browser_api::browser_webview_reload,
api::browser_api::browser_webview_set_bounds,
api::browser_api::browser_webview_show,
api::browser_api::browser_webview_hide,
api::browser_api::browser_webview_close,
api::browser_api::browser_webview_set_focus,
api::browser_api::browser_get_url,
// Browser Control API (CDP-based user browser control)
api::browser_control_api::browser_control_list_browsers,
Expand Down
9 changes: 4 additions & 5 deletions src/apps/ohos/entry/oh-package-lock.json5

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/apps/ohos/entry/oh-package.json5
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"dependencies": {
"libbitfun_desktop_lib.so": "file:./src/main/cpp/types/libbitfun_desktop_lib",
"libentry.so": "file:./src/main/cpp/types/libentry",
"@ohos-rs/ability": "0.4.0-beta.0"
"@ohos-rs/ability": "file:../oh-rs-ability"
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export declare function registerArktsFunction(funcName: string, callback: ((err:
export declare function setBuildResult(msg: string): void;
export declare function ohosMarkCleanShutdown(): void;
export declare function getAppConfigBool(path: string): boolean;
export declare function notifySystemColorMode(mode: string): void;
export declare function notifySystemColorMode(mode: string): void;
export declare function emitBrowserPageLoad(label: string, event: string, url: string): void;
Loading
Loading