use alloc::boxed::Box;
use alloc::string::ToString;
use alloc::vec::Vec;
use core::cell::Cell;
use crate::closure::WuiFn;
use crate::reactive::WuiComputed;
use crate::{IntoFFI, IntoRust, WuiEnv, WuiStr};
use base64::Engine;
use cookie::Cookie;
use nami::{Signal, SignalExt};
use waterui_str::Str;
use waterui_webview::{
BackendEvent, CustomWebViewController, JsReply, ScriptInjectionTime, Url, WatcherGuard,
WatcherSet, WebView, WebViewController, WebViewError, WebViewEvent, WebViewHandle,
};
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WuiScriptInjectionTime {
DocumentStart = 0,
DocumentEnd = 1,
}
impl IntoFFI for ScriptInjectionTime {
type FFI = WuiScriptInjectionTime;
fn into_ffi(self) -> Self::FFI {
match self {
Self::DocumentStart => WuiScriptInjectionTime::DocumentStart,
Self::DocumentEnd => WuiScriptInjectionTime::DocumentEnd,
}
}
}
impl IntoRust for WuiScriptInjectionTime {
type Rust = ScriptInjectionTime;
unsafe fn into_rust(self) -> Self::Rust {
match self {
Self::DocumentStart => ScriptInjectionTime::DocumentStart,
Self::DocumentEnd => ScriptInjectionTime::DocumentEnd,
}
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WuiWebViewEventType {
WillNavigate = 1,
Loading = 2,
Loaded = 3,
Redirect = 4,
SslError = 5,
Error = 6,
StateChanged = 7,
}
#[repr(C)]
#[derive(Debug)]
pub struct WuiWebViewEvent {
pub event_type: WuiWebViewEventType,
pub url: *mut WuiStr,
pub url2: *mut WuiStr,
pub message: *mut WuiStr,
pub progress: f32,
pub can_go_back: bool,
pub can_go_forward: bool,
}
fn parse_url(s: &Str) -> Url {
s.as_str().parse().unwrap_or_else(|error| {
panic!(
"WebView backend emitted an unparseable URL {:?}: {error}",
s.as_str()
)
})
}
unsafe fn take_event_string(value: *mut WuiStr) -> Str {
let value = unsafe { Box::from_raw(value) };
unsafe { (*value).into_rust() }
}
impl IntoRust for WuiWebViewEvent {
type Rust = BackendEvent;
unsafe fn into_rust(self) -> Self::Rust {
match self.event_type {
WuiWebViewEventType::WillNavigate => BackendEvent::Event(WebViewEvent::WillNavigate {
url: parse_url(&unsafe { take_event_string(self.url) }),
}),
WuiWebViewEventType::Loading => BackendEvent::Event(WebViewEvent::Loading {
progress: self.progress,
}),
WuiWebViewEventType::Loaded => BackendEvent::Event(WebViewEvent::Loaded),
WuiWebViewEventType::Redirect => BackendEvent::Event(WebViewEvent::Redirect {
from: parse_url(&unsafe { take_event_string(self.url) }),
to: parse_url(&unsafe { take_event_string(self.url2) }),
}),
WuiWebViewEventType::SslError => {
BackendEvent::Event(WebViewEvent::Error(WebViewError::Ssl {
url: parse_url(&unsafe { take_event_string(self.url) }),
message: unsafe { take_event_string(self.message) },
}))
}
WuiWebViewEventType::Error => {
BackendEvent::Event(WebViewEvent::Error(WebViewError::LoadFailed(unsafe {
take_event_string(self.message)
})))
}
WuiWebViewEventType::StateChanged => BackendEvent::NavigationState {
can_go_back: self.can_go_back,
can_go_forward: self.can_go_forward,
},
}
}
}
#[repr(C)]
#[derive(Debug)]
pub struct WuiJsCallback {
pub data: *mut (),
pub call: unsafe extern "C" fn(data: *mut (), success: bool, result: WuiStr),
}
#[repr(C)]
#[derive(Debug)]
pub struct WuiStringCallback {
pub data: *mut (),
pub call: unsafe extern "C" fn(data: *mut (), result: WuiStr),
}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WuiJsReplyKind {
Json = 0,
Bytes = 1,
}
#[repr(C)]
#[derive(Debug)]
pub struct WuiWebViewReply {
pub data: *mut (),
pub call:
unsafe extern "C" fn(data: *mut (), success: bool, kind: WuiJsReplyKind, result: WuiStr),
}
#[repr(C)]
#[derive(Debug)]
pub struct WuiWebViewMessage {
pub payload_base64: WuiStr,
pub reply: WuiWebViewReply,
}
#[repr(C)]
#[derive(Debug)]
pub struct WuiWebViewHandle {
pub data: *mut (),
pub go_back: unsafe extern "C" fn(*mut ()),
pub go_forward: unsafe extern "C" fn(*mut ()),
pub go_to: unsafe extern "C" fn(*mut (), WuiStr),
pub stop: unsafe extern "C" fn(*mut ()),
pub refresh: unsafe extern "C" fn(*mut ()),
pub can_go_back: unsafe extern "C" fn(*const ()) -> bool,
pub can_go_forward: unsafe extern "C" fn(*const ()) -> bool,
pub set_user_agent: unsafe extern "C" fn(*mut (), WuiStr),
pub set_redirects_enabled: unsafe extern "C" fn(*mut (), *mut WuiComputed<bool>),
pub inject_script: unsafe extern "C" fn(*mut (), WuiStr, WuiStr, WuiScriptInjectionTime),
pub watch: unsafe extern "C" fn(*mut (), WuiFn<WuiWebViewEvent>),
pub add_handler: Option<unsafe extern "C" fn(*mut (), WuiStr, WuiFn<WuiWebViewMessage>)>,
pub remove_handler: Option<unsafe extern "C" fn(*mut (), WuiStr)>,
pub set_bridge_origins: Option<unsafe extern "C" fn(*mut (), WuiStr)>,
pub set_cookie: Option<unsafe extern "C" fn(*mut (), WuiStr)>,
pub get_cookies: Option<unsafe extern "C" fn(*const (), WuiStringCallback)>,
pub run_javascript: unsafe extern "C" fn(*mut (), WuiStr, WuiJsCallback),
pub call_async_javascript: unsafe extern "C" fn(*mut (), WuiStr, WuiJsCallback),
pub drop: unsafe extern "C" fn(*mut ()),
}
pub struct FfiWebViewHandle {
ffi: WuiWebViewHandle,
watchers: WatcherSet<BackendEvent>,
watcher_installed: Cell<bool>,
}
impl core::fmt::Debug for FfiWebViewHandle {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("FfiWebViewHandle")
.field("ffi", &self.ffi)
.field("watcher_installed", &self.watcher_installed)
.finish_non_exhaustive()
}
}
impl FfiWebViewHandle {
pub(crate) fn new(ffi: WuiWebViewHandle) -> Self {
Self {
ffi,
watchers: WatcherSet::new(),
watcher_installed: Cell::new(false),
}
}
pub const fn native_ptr(&self) -> *mut () {
self.ffi.data
}
}
impl Drop for FfiWebViewHandle {
fn drop(&mut self) {
unsafe {
(self.ffi.drop)(self.ffi.data);
}
}
}
impl WebViewHandle for FfiWebViewHandle {
fn go_back(&self) {
unsafe { (self.ffi.go_back)(self.ffi.data) }
}
fn go_forward(&self) {
unsafe { (self.ffi.go_forward)(self.ffi.data) }
}
fn go_to(&self, url: &Url) {
let owned_url = Str::from(url.as_str().to_string());
unsafe { (self.ffi.go_to)(self.ffi.data, owned_url.into_ffi()) }
}
fn stop(&self) {
unsafe { (self.ffi.stop)(self.ffi.data) }
}
fn refresh(&self) {
unsafe { (self.ffi.refresh)(self.ffi.data) }
}
fn can_go_back(&self) -> bool {
unsafe { (self.ffi.can_go_back)(self.ffi.data) }
}
fn can_go_forward(&self) -> bool {
unsafe { (self.ffi.can_go_forward)(self.ffi.data) }
}
fn set_user_agent(&self, user_agent: &str) {
let owned_ua = Str::from(user_agent.to_string());
unsafe { (self.ffi.set_user_agent)(self.ffi.data, owned_ua.into_ffi()) }
}
fn set_redirects_enabled(&self, enabled: impl Signal<Output = bool>) {
unsafe {
(self.ffi.set_redirects_enabled)(self.ffi.data, enabled.computed().into_ffi());
}
}
fn inject_script(&self, key: &str, script: &str, time: ScriptInjectionTime) {
let owned_key = Str::from(key.to_string());
let owned_script = Str::from(script.to_string());
unsafe {
(self.ffi.inject_script)(
self.ffi.data,
owned_key.into_ffi(),
owned_script.into_ffi(),
time.into_ffi(),
);
}
}
fn watch(&self, f: impl Fn(BackendEvent) + 'static) -> WatcherGuard {
let guard = self.watchers.insert(f);
if self.watcher_installed.replace(true) {
return guard;
}
let watchers = self.watchers.clone();
let callback = WuiFn::from(move |ffi_event: WuiWebViewEvent| {
let event = unsafe { ffi_event.into_rust() };
watchers.emit(&event);
});
unsafe { (self.ffi.watch)(self.ffi.data, callback) };
guard
}
fn add_handler(&self, name: &str, handler: Box<waterui_webview::ScriptMessageHandler>) {
let add_handler = self
.ffi
.add_handler
.expect("WebView backend must implement `add_handler`");
let engine = base64::engine::general_purpose::STANDARD;
let name = Str::from(name.to_string());
let callback = WuiFn::from(move |msg: WuiWebViewMessage| {
let payload_b64: Str = unsafe { msg.payload_base64.into_rust() };
let payload = match engine.decode(payload_b64.as_str()) {
Ok(bytes) => bytes,
Err(err) => {
let message = Str::from(err.to_string());
unsafe {
(msg.reply.call)(
msg.reply.data,
false,
WuiJsReplyKind::Json,
message.into_ffi(),
);
};
return;
}
};
let future = handler(&payload);
let reply_call = msg.reply.call;
let reply_data = msg.reply.data;
executor_core::spawn_local(async move {
let engine = base64::engine::general_purpose::STANDARD;
let (success, kind, payload) = match future.await {
Ok(JsReply::Json(json)) => (true, WuiJsReplyKind::Json, engine.encode(json)),
Ok(JsReply::Bytes(bytes)) => {
(true, WuiJsReplyKind::Bytes, engine.encode(bytes))
}
Err(message) => (false, WuiJsReplyKind::Json, message),
};
unsafe { (reply_call)(reply_data, success, kind, Str::from(payload).into_ffi()) };
})
.detach();
});
unsafe { add_handler(self.ffi.data, name.into_ffi(), callback) }
}
fn set_bridge_origins(&self, policy: waterui_webview::OriginPolicy) {
let Some(set) = self.ffi.set_bridge_origins else {
return;
};
let patterns = policy.wire();
unsafe { set(self.ffi.data, patterns.into_ffi()) }
}
fn remove_handler(&self, name: &str) {
let remove_handler = self
.ffi
.remove_handler
.expect("WebView backend must implement `remove_handler`");
let name = Str::from(name.to_string());
unsafe { remove_handler(self.ffi.data, name.into_ffi()) }
}
fn set_cookie(&self, cookie: Cookie<'static>) {
let set_cookie = self
.ffi
.set_cookie
.expect("WebView backend must implement `set_cookie`");
let cookie = Str::from(cookie.to_string());
unsafe { set_cookie(self.ffi.data, cookie.into_ffi()) }
}
#[expect(
clippy::future_not_send,
reason = "WaterUI webview bridge futures resolve on the main-thread local executor and carry non-`Send` `Str` payloads by design"
)]
fn get_cookies(&self) -> impl core::future::Future<Output = Vec<Cookie<'static>>> {
unsafe extern "C" fn cookies_callback(data: *mut (), result: WuiStr) {
let sender = unsafe { Box::from_raw(data.cast::<async_channel::Sender<Str>>()) };
let result = unsafe { result.into_rust() };
let _ = sender.try_send(result);
}
let get_cookies = self
.ffi
.get_cookies
.expect("WebView backend must implement `get_cookies`");
let (sender, receiver) = async_channel::bounded::<Str>(1);
let callback_data = Box::into_raw(Box::new(sender)).cast::<()>();
unsafe {
get_cookies(
self.ffi.data.cast_const(),
WuiStringCallback {
data: callback_data,
call: cookies_callback,
},
);
}
async move {
let Ok(text) = receiver.recv().await else {
return Vec::new();
};
text.as_str()
.lines()
.filter_map(|line| match Cookie::parse(line.to_string()) {
Ok(cookie) => Some(cookie.into_owned()),
Err(error) => {
tracing::warn!(%error, "skipping a cookie the web view could not parse");
None
}
})
.collect()
}
}
#[expect(
clippy::future_not_send,
reason = "WaterUI webview bridge futures resolve on the main-thread local executor and carry non-`Send` `Str` payloads by design"
)]
fn run_javascript(&self, script: &str) -> impl core::future::Future<Output = Result<Str, Str>> {
unsafe { self.evaluate_through(self.ffi.run_javascript, script) }
}
#[expect(
clippy::future_not_send,
reason = "WaterUI webview bridge futures resolve on the main-thread local executor and carry non-`Send` `Str` payloads by design"
)]
fn call_async_javascript(
&self,
body: &str,
) -> impl core::future::Future<Output = Result<Str, Str>> {
unsafe { self.evaluate_through(self.ffi.call_async_javascript, body) }
}
}
impl FfiWebViewHandle {
#[expect(
clippy::future_not_send,
reason = "WaterUI webview bridge futures resolve on the main-thread local executor and carry non-`Send` `Str` payloads by design"
)]
unsafe fn evaluate_through(
&self,
evaluate: unsafe extern "C" fn(*mut (), WuiStr, WuiJsCallback),
source: &str,
) -> impl core::future::Future<Output = Result<Str, Str>> {
unsafe extern "C" fn js_callback_trampoline(data: *mut (), success: bool, result: WuiStr) {
let sender =
unsafe { Box::from_raw(data.cast::<async_channel::Sender<Result<Str, Str>>>()) };
let result = unsafe { result.into_rust() };
let _ = sender.try_send(if success { Ok(result) } else { Err(result) });
}
let (sender, receiver) = async_channel::bounded::<Result<Str, Str>>(1);
let callback_data = Box::into_raw(Box::new(sender)).cast::<()>();
let ffi_callback = WuiJsCallback {
data: callback_data,
call: js_callback_trampoline,
};
let owned_source = Str::from(source.to_string());
unsafe {
evaluate(self.ffi.data, owned_source.into_ffi(), ffi_callback);
}
async move {
receiver.recv().await.unwrap_or_else(|_| {
Err(Str::from_static(
"the web view was torn down before the script completed",
))
})
}
}
}
#[repr(C)]
#[derive(Debug)]
pub struct WuiBridgeRequest {
pub ok: bool,
pub id: u64,
pub name: WuiStr,
pub payload_base64: WuiStr,
}
#[unsafe(no_mangle)]
pub extern "C" fn waterui_webview_bridge_script() -> WuiStr {
Str::from_static(waterui_webview::DOCUMENT_START_SCRIPT).into_ffi()
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn waterui_webview_parse_bridge_request(
envelope: WuiStr,
) -> WuiBridgeRequest {
let envelope: Str = unsafe { envelope.into_rust() };
match waterui_webview::bridge::Request::parse(envelope.as_str()) {
Ok(request) => WuiBridgeRequest {
ok: true,
id: request.id,
name: Str::from(request.name).into_ffi(),
payload_base64: Str::from(
base64::engine::general_purpose::STANDARD.encode(&request.payload),
)
.into_ffi(),
},
Err(error) => {
tracing::warn!(%error, "page script sent a malformed WaterUI bridge request");
WuiBridgeRequest {
ok: false,
id: 0,
name: Str::from_static("").into_ffi(),
payload_base64: Str::from_static("").into_ffi(),
}
}
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn waterui_webview_bridge_reply_script(
id: u64,
success: bool,
kind: WuiJsReplyKind,
payload_base64: WuiStr,
) -> WuiStr {
let payload: Str = unsafe { payload_base64.into_rust() };
let reply = if success {
match base64::engine::general_purpose::STANDARD.decode(payload.as_str()) {
Ok(bytes) => match kind {
WuiJsReplyKind::Json => waterui_webview::bridge::Reply::Json(bytes),
WuiJsReplyKind::Bytes => waterui_webview::bridge::Reply::Bytes(bytes),
},
Err(error) => waterui_webview::bridge::Reply::failure(&error),
}
} else {
waterui_webview::bridge::Reply::failure(&payload.as_str())
};
Str::from(reply.resolve_script(id)).into_ffi()
}
opaque!(WuiWebView, WebView);
ffi_view!(WebView, *mut WuiWebView, webview, any());
#[unsafe(no_mangle)]
pub unsafe extern "C" fn waterui_webview_native_handle(webview: *mut WuiWebView) -> *mut () {
unsafe {
let webview = crate::borrow_ffi(webview);
let handle = webview.0.handle();
let ffi_handle = handle.downcast_ref::<FfiWebViewHandle>().unwrap_or_else(|| {
panic!(
"waterui_webview_native_handle requires a WebView created by the backend-installed FFI WebViewController"
)
});
ffi_handle.native_ptr()
}
}
pub type WuiCreateWebViewFn = unsafe extern "C" fn() -> WuiWebViewHandle;
struct FfiWebViewController {
create_fn: WuiCreateWebViewFn,
}
impl CustomWebViewController for FfiWebViewController {
fn open(&self) -> impl WebViewHandle {
let handle = unsafe { (self.create_fn)() };
FfiWebViewHandle::new(handle)
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn waterui_env_install_webview_controller(
env: *mut WuiEnv,
create_fn: WuiCreateWebViewFn,
) {
let env = unsafe { crate::borrow_ffi_mut(env) };
let controller = WebViewController::new(FfiWebViewController { create_fn });
env.insert(controller);
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn waterui_env_has_webview_controller(env: *const WuiEnv) -> bool {
let env = unsafe { crate::borrow_ffi(env) };
env.get::<WebViewController>().is_some()
}