use std::rc::Rc;
use cranpose_core::{CompositionLocal, CompositionLocalProvider, compositionLocalOfWithPolicy};
use cranpose_macros::composable;
#[derive(thiserror::Error, Debug)]
pub enum UriHandlerError {
#[error("Failed to open URL: {0}")]
OpenFailed(String),
#[error("No window object available")]
NoWindow,
#[error("Popup blocked for URL: {0}")]
PopupBlocked(String),
#[error("Opening external links is not supported on this platform: {0}")]
UnsupportedPlatform(String),
#[error("{operation} requires cranpose-services feature `{feature}`")]
UnsupportedFeature {
operation: &'static str,
feature: &'static str,
},
}
pub trait UriHandler {
fn open_uri(&self, uri: &str) -> Result<(), UriHandlerError>;
}
pub type UriHandlerRef = Rc<dyn UriHandler>;
pub fn set_platform_uri_handler(handler: UriHandlerRef) {
crate::thread_state::with(|state| *state.uri_handler.borrow_mut() = Some(handler));
}
pub fn clear_platform_uri_handler() {
crate::thread_state::with(|state| *state.uri_handler.borrow_mut() = None);
}
fn registered_platform_uri_handler() -> Option<UriHandlerRef> {
crate::thread_state::with(|state| state.uri_handler.borrow().clone())
}
struct PlatformUriHandler;
impl UriHandler for PlatformUriHandler {
fn open_uri(&self, uri: &str) -> Result<(), UriHandlerError> {
if let Some(handler) = registered_platform_uri_handler() {
return handler.open_uri(uri);
}
#[cfg(all(
not(target_arch = "wasm32"),
not(target_os = "android"),
feature = "uri-native"
))]
{
open::that(uri).map_err(|err| UriHandlerError::OpenFailed(format!("{err:?}")))?;
Ok(())
}
#[cfg(all(target_arch = "wasm32", feature = "uri-web"))]
{
let window = web_sys::window().ok_or(UriHandlerError::NoWindow)?;
let opened = window
.open_with_url_and_target(uri, "_blank")
.map_err(|err| UriHandlerError::OpenFailed(format!("{err:?}")))?;
if opened.is_none() {
Err(UriHandlerError::PopupBlocked(uri.to_string()))
} else {
Ok(())
}
}
#[cfg(all(target_os = "android", feature = "uri-android"))]
{
webbrowser::open(uri).map_err(|err| UriHandlerError::OpenFailed(err.to_string()))?;
Ok(())
}
#[cfg(all(
not(target_arch = "wasm32"),
not(target_os = "android"),
not(feature = "uri-native")
))]
{
let _ = uri;
Err(UriHandlerError::UnsupportedFeature {
operation: "native URI opening",
feature: "uri-native",
})
}
#[cfg(all(target_arch = "wasm32", not(feature = "uri-web")))]
{
let _ = uri;
Err(UriHandlerError::UnsupportedFeature {
operation: "web URI opening",
feature: "uri-web",
})
}
#[cfg(all(target_os = "android", not(feature = "uri-android")))]
{
let _ = uri;
Err(UriHandlerError::UnsupportedFeature {
operation: "Android URI opening",
feature: "uri-android",
})
}
}
}
pub fn default_uri_handler() -> UriHandlerRef {
Rc::new(PlatformUriHandler)
}
pub fn local_uri_handler() -> CompositionLocal<UriHandlerRef> {
crate::composition_locals::cached_local(
|locals| &locals.uri_handler,
|| compositionLocalOfWithPolicy(default_uri_handler, Rc::ptr_eq),
)
}
#[composable]
pub fn ProvideUriHandler(content: impl FnOnce()) {
let uri_handler = cranpose_core::remember(default_uri_handler).with(|state| state.clone());
let uri_local = local_uri_handler();
CompositionLocalProvider([uri_local.provides(uri_handler)], move || {
content();
});
}
#[cfg(test)]
#[path = "tests/uri_handler_tests.rs"]
mod tests;