///|
/// An embedded platform WebView associated with a caller-owned native container.
pub struct WebView {
priv handle : UInt64
priv owner_thread : UInt64
}
///|
/// Returns whether the native backend is available on the current host.
pub fn available() -> Bool {
native_available()
}
///|
/// Registers an application-owned custom URL scheme for the process.
///
/// Call this before the first successful `WebView::create`. Native handlers
/// have a 30-second response deadline; unanswered requests are cancelled by
/// the backend and reported as `ProtocolCancelled`.
pub fn register_custom_scheme(name : String) -> Result[Unit, WebViewError] {
if custom_schemes_locked.contains(0) {
Err(WebViewError::ConfigurationLocked)
} else if name.length() == 0 {
Err(WebViewError::NativeFailure(0, "Custom scheme name must not be empty"))
} else if native_register_custom_scheme(encode_utf8(name)) != 0 {
Ok(())
} else {
Err(
WebViewError::Unsupported(
"Custom schemes are unavailable on this backend",
),
)
}
}
///|
/// Starts asynchronous creation of a WebView child view in `parent_handle`.
///
/// The caller must keep the native parent container alive and run its UI event
/// loop until the listener receives `WebViewEvent::Ready` or `CreationFailed`.
/// A later `ProcessFailed` is terminal: destroy the view and create a new one
/// explicitly after the host has handled the failure.
/// On Windows, this is an `HWND`; on macOS, an `NSView*`; and on Linux, a
/// `GtkFixed*`. Every later command, including `destroy`, must use this same
/// UI thread.
pub fn WebView::create(
parent_handle : UInt64,
options : WebViewOptions,
) -> Result[WebView, WebViewError] {
WebView::create_in_context(options.context, parent_handle, options)
}
///|
/// Starts asynchronous creation in a browser-data context shared by one or
/// more WebViews.
pub fn WebView::create_in_context(
context : WebContext,
parent_handle : UInt64,
options : WebViewOptions,
) -> Result[WebView, WebViewError] {
if native_is_ohos() {
Err(
WebViewError::Unsupported(
"OpenHarmony Web components must be created by ArkUI and attached with WebView::attach_ohos",
),
)
} else if !available() {
Err(WebViewError::Unavailable)
} else {
match resource_limits_error(options.resource_limits) {
Some(error) => Err(error)
None =>
match context_error(context) {
Some(error) => Err(error)
None => {
let owner_thread = native_thread_token()
if owner_thread == 0UL {
return Err(WebViewError::CreateRejected)
}
ensure_native_callbacks()
let handle = native_create(
context.id,
context.storage == WebContextStorage::Ephemeral,
encode_utf8(context.data_directory),
parent_handle,
options.bounds.x,
options.bounds.y,
options.bounds.width,
options.bounds.height,
encode_utf8(options.initial_url),
encode_utf8(options.initial_html),
encode_utf8(options.initialization_script),
encode_utf8(options.user_agent),
options.resource_limits.max_pending_commands,
options.resource_limits.max_pending_command_bytes,
options.resource_limits.max_protocol_request_body_bytes,
)
if handle == 0 {
Err(WebViewError::CreateRejected)
} else {
if should_lock_custom_schemes(handle) {
native_lock_custom_schemes()
custom_schemes_locked.set(0, ())
}
register_view(handle, options)
let view = WebView::{ handle, owner_thread }
if !options.visible {
ignore(native_set_visible(handle, false))
}
native_start(handle)
Ok(view)
}
}
}
}
}
}
///|
fn should_lock_custom_schemes(handle : UInt64) -> Bool {
handle != 0UL && !custom_schemes_locked.contains(0)
}
///|
test "custom scheme configuration waits for a successful create" {
assert_eq(should_lock_custom_schemes(0UL), false)
assert_eq(should_lock_custom_schemes(1UL), true)
}
///|
fn context_error(context : WebContext) -> WebViewError? {
if active_backend() == NativeBackend::Windows &&
context.storage == WebContextStorage::Ephemeral {
Some(
WebViewError::Unsupported(
"Ephemeral WebContext is unavailable on WebView2",
),
)
} else if active_backend() == NativeBackend::MacOS &&
context.data_directory.length() > 0 {
Some(
WebViewError::Unsupported(
"Custom WebContext data directories are unavailable on WKWebView",
),
)
} else {
None
}
}
///|
fn resource_limits_error(limits : WebViewResourceLimits) -> WebViewError? {
if limits.max_pending_commands < 0 ||
limits.max_pending_command_bytes < 0 ||
limits.max_protocol_request_body_bytes < 0 {
Some(
WebViewError::NativeFailure(
0, "WebView resource limits must be non-negative",
),
)
} else {
None
}
}
///|
/// Attaches Moonview to an ArkUI-owned OpenHarmony `Web` component.
///
/// Call this on the ArkUI UI thread after the host has created the component
/// with the same stable `web_tag`. The host remains responsible for its source,
/// layout, visibility, and permission policy. The API is experimental and
/// currently supports only `reload`, fire-and-forget `eval`, and `destroy`.
pub fn WebView::attach_ohos(
web_tag : String,
options : OhosAttachOptions,
) -> Result[WebView, WebViewError] {
if web_tag.length() == 0 {
Err(WebViewError::NativeFailure(0, "OpenHarmony Web tag must not be empty"))
} else if !native_is_ohos() {
Err(
WebViewError::Unsupported(
"OpenHarmony ArkWeb is unavailable on this backend",
),
)
} else if !available() {
Err(WebViewError::Unavailable)
} else {
ensure_native_callbacks()
let handle = native_attach_ohos(encode_utf8(web_tag))
if handle == 0 {
Err(WebViewError::CreateRejected)
} else {
register_ohos_view(handle, options)
native_start_ohos(handle)
Ok(WebView::{ handle, owner_thread: 0UL })
}
}
}
///|
pub fn WebView::lifecycle(self : WebView) -> WebViewLifecycle {
lifecycle_of(self.handle)
}
///|
/// Returns the last document URL and history state reported by the native
/// backend. The value is updated asynchronously through navigation events.
pub fn WebView::navigation_state(self : WebView) -> NavigationState {
navigation_state_of(self.handle)
}
///|
pub fn WebView::set_bounds(
self : WebView,
bounds : Rect,
) -> Result[Unit, WebViewError] {
self.with_handle("set bounds", handle => {
native_set_bounds(handle, bounds.x, bounds.y, bounds.width, bounds.height)
})
}
///|
pub fn WebView::set_visible(
self : WebView,
visible : Bool,
) -> Result[Unit, WebViewError] {
self.with_handle("set visibility", handle => {
native_set_visible(handle, visible)
})
}
///|
pub fn WebView::focus(self : WebView) -> Result[Unit, WebViewError] {
self.with_handle("focus", handle => native_focus(handle))
}
///|
pub fn WebView::navigate(
self : WebView,
url : String,
) -> Result[Unit, WebViewError] {
self.with_handle("navigate", handle => {
native_navigate(handle, encode_utf8(url))
})
}
///|
pub fn WebView::load_html(
self : WebView,
html : String,
) -> Result[Unit, WebViewError] {
self.with_handle("load HTML", handle => {
native_load_html(handle, encode_utf8(html))
})
}
///|
pub fn WebView::reload(self : WebView) -> Result[Unit, WebViewError] {
self.with_ohos_supported_handle("reload", handle => native_reload(handle))
}
///|
pub fn WebView::stop(self : WebView) -> Result[Unit, WebViewError] {
self.with_handle("stop", handle => native_stop(handle))
}
///|
pub fn WebView::go_back(self : WebView) -> Result[Unit, WebViewError] {
self.with_handle("go back", handle => native_go_back(handle))
}
///|
pub fn WebView::go_forward(self : WebView) -> Result[Unit, WebViewError] {
self.with_handle("go forward", handle => native_go_forward(handle))
}
///|
pub fn WebView::add_init_script(
self : WebView,
script : String,
) -> Result[Unit, WebViewError] {
self.with_handle("add initialization script", handle => {
native_add_init_script(handle, encode_utf8(script))
})
}
///|
/// Sets the page zoom factor. Values must be greater than zero.
pub fn WebView::set_zoom_factor(
self : WebView,
factor : Double,
) -> Result[Unit, WebViewError] {
if factor <= 0.0 {
Err(
WebViewError::NativeFailure(
0, "WebView zoom factor must be greater than zero",
),
)
} else {
self.with_handle("set zoom", handle => native_set_zoom(handle, factor))
}
}
///|
/// Opens the platform developer tools for this WebView.
///
/// This is supported by the Windows and Linux backends. macOS does not expose
/// a supported public API for programmatically opening WKWebView inspector.
pub fn WebView::open_devtools(self : WebView) -> Result[Unit, WebViewError] {
if !self.on_owner_thread() {
Err(WebViewError::WrongThread)
} else {
match self.lifecycle() {
WebViewLifecycle::Destroyed => Err(WebViewError::Destroyed)
WebViewLifecycle::Failed(error) => Err(error)
_ =>
if native_open_devtools(self.handle) != 0 {
Ok(())
} else {
Err(
WebViewError::Unsupported(
"WebView developer tools are unavailable on this backend",
),
)
}
}
}
}
///|
/// Opens the platform print dialog for the current document.
///
/// The dialog is available after `Ready`. Older WebView2 runtimes and macOS
/// releases before 11 return `Unsupported`.
pub fn WebView::open_print_dialog(self : WebView) -> Result[Unit, WebViewError] {
if !self.on_owner_thread() {
Err(WebViewError::WrongThread)
} else {
match self.lifecycle() {
WebViewLifecycle::Destroyed => Err(WebViewError::Destroyed)
WebViewLifecycle::Failed(error) => Err(error)
_ =>
if native_open_print_dialog(self.handle) != 0 {
Ok(())
} else {
Err(
WebViewError::Unsupported(
"WebView print dialog is unavailable on this backend",
),
)
}
}
}
}
///|
pub fn WebView::eval(
self : WebView,
script : String,
request_id : String,
) -> Result[Unit, WebViewError] {
self.with_ohos_supported_handle("evaluate script", handle => {
native_eval(handle, encode_utf8(script), encode_utf8(request_id))
})
}
///|
/// Posts an application-defined UTF-8 message to `window.moonview` in the page.
pub fn WebView::post_message(
self : WebView,
message : String,
) -> Result[Unit, WebViewError] {
self.with_handle("post message", handle => {
native_post_message(handle, encode_utf8(message))
})
}
///|
/// Completes a pending custom-scheme request. The request ID is valid only
/// until the native backend's 30-second response deadline expires.
pub fn WebView::respond_protocol(
self : WebView,
request_id : String,
response : ProtocolResponse,
) -> Result[Unit, WebViewError] {
if native_is_ohos() {
Err(
WebViewError::Unsupported(
"OpenHarmony custom-scheme responses require a future IO-thread adapter",
),
)
} else if !self.on_owner_thread() {
Err(WebViewError::WrongThread)
} else {
match self.lifecycle() {
WebViewLifecycle::Destroyed => Err(WebViewError::Destroyed)
WebViewLifecycle::Failed(error) => Err(error)
_ =>
if native_respond_protocol(
self.handle,
encode_utf8(request_id),
response.status,
encode_http_headers(response.headers),
response.body,
) !=
0 {
Ok(())
} else {
Err(
WebViewError::NativeFailure(
0, "Protocol request is no longer pending",
),
)
}
}
}
}
///|
/// Destroys the native child view. Repeated calls are harmless.
pub fn WebView::destroy(self : WebView) -> Result[Unit, WebViewError] {
if !self.on_owner_thread() {
Err(WebViewError::WrongThread)
} else if self.lifecycle() == WebViewLifecycle::Destroyed {
Ok(())
} else if native_destroy(self.handle) != 0 {
unregister_view(self.handle)
Ok(())
} else {
Err(
WebViewError::NativeFailure(
0, "WebView destroy must run on its creation UI thread",
),
)
}
}
///|
fn WebView::on_owner_thread(self : WebView) -> Bool {
if native_is_ohos() {
true
} else {
match owner_thread_result(native_is_owner_thread(self.owner_thread)) {
Ok(_) => true
Err(_) => false
}
}
}
///|
fn owner_thread_result(on_owner_thread : Bool) -> Result[Unit, WebViewError] {
if on_owner_thread {
Ok(())
} else {
Err(WebViewError::WrongThread)
}
}
///|
fn WebView::with_handle(
self : WebView,
operation_name : String,
operation : (UInt64) -> Int,
) -> Result[Unit, WebViewError] {
if !self.on_owner_thread() {
Err(WebViewError::WrongThread)
} else {
match self.lifecycle() {
WebViewLifecycle::Destroyed => Err(WebViewError::Destroyed)
WebViewLifecycle::Failed(error) => Err(error)
_ if native_is_ohos() =>
Err(
WebViewError::Unsupported(
"This operation is controlled by the ArkUI host or is not exposed by ArkWeb API level 12",
),
)
_ => command_result(operation_name, operation(self.handle))
}
}
}
///|
fn WebView::with_ohos_supported_handle(
self : WebView,
operation_name : String,
operation : (UInt64) -> Int,
) -> Result[Unit, WebViewError] {
if !self.on_owner_thread() {
Err(WebViewError::WrongThread)
} else {
match self.lifecycle() {
WebViewLifecycle::Destroyed => Err(WebViewError::Destroyed)
WebViewLifecycle::Failed(error) => Err(error)
_ => command_result(operation_name, operation(self.handle))
}
}
}
///|
fn command_result(
operation_name : String,
accepted : Int,
) -> Result[Unit, WebViewError] {
if accepted != 0 {
Ok(())
} else {
Err(
WebViewError::NativeFailure(
0,
"WebView backend rejected " + operation_name,
),
)
}
}