cranpose_services/
uri_handler.rs1use std::rc::Rc;
2
3use cranpose_core::{CompositionLocal, CompositionLocalProvider, compositionLocalOfWithPolicy};
4use cranpose_macros::composable;
5
6#[derive(thiserror::Error, Debug)]
8pub enum UriHandlerError {
9 #[error("Failed to open URL: {0}")]
11 OpenFailed(String),
12 #[error("No window object available")]
14 NoWindow,
15 #[error("Popup blocked for URL: {0}")]
17 PopupBlocked(String),
18 #[error("Opening external links is not supported on this platform: {0}")]
20 UnsupportedPlatform(String),
21 #[error("{operation} requires cranpose-services feature `{feature}`")]
23 UnsupportedFeature {
24 operation: &'static str,
26 feature: &'static str,
28 },
29}
30
31pub trait UriHandler {
33 fn open_uri(&self, uri: &str) -> Result<(), UriHandlerError>;
36}
37
38pub type UriHandlerRef = Rc<dyn UriHandler>;
40
41pub fn set_platform_uri_handler(handler: UriHandlerRef) {
49 crate::thread_state::with(|state| *state.uri_handler.borrow_mut() = Some(handler));
50}
51
52pub fn clear_platform_uri_handler() {
54 crate::thread_state::with(|state| *state.uri_handler.borrow_mut() = None);
55}
56
57fn registered_platform_uri_handler() -> Option<UriHandlerRef> {
58 crate::thread_state::with(|state| state.uri_handler.borrow().clone())
59}
60
61struct PlatformUriHandler;
62
63impl UriHandler for PlatformUriHandler {
64 fn open_uri(&self, uri: &str) -> Result<(), UriHandlerError> {
65 if let Some(handler) = registered_platform_uri_handler() {
66 return handler.open_uri(uri);
67 }
68
69 #[cfg(all(
70 not(target_arch = "wasm32"),
71 not(target_os = "android"),
72 feature = "uri-native"
73 ))]
74 {
75 open::that(uri).map_err(|err| UriHandlerError::OpenFailed(format!("{err:?}")))?;
76 Ok(())
77 }
78
79 #[cfg(all(target_arch = "wasm32", feature = "uri-web"))]
80 {
81 let window = web_sys::window().ok_or(UriHandlerError::NoWindow)?;
82 let opened = window
83 .open_with_url_and_target(uri, "_blank")
84 .map_err(|err| UriHandlerError::OpenFailed(format!("{err:?}")))?;
85 if opened.is_none() {
86 Err(UriHandlerError::PopupBlocked(uri.to_string()))
87 } else {
88 Ok(())
89 }
90 }
91
92 #[cfg(all(target_os = "android", feature = "uri-android"))]
93 {
94 webbrowser::open(uri).map_err(|err| UriHandlerError::OpenFailed(err.to_string()))?;
95 Ok(())
96 }
97
98 #[cfg(all(
99 not(target_arch = "wasm32"),
100 not(target_os = "android"),
101 not(feature = "uri-native")
102 ))]
103 {
104 let _ = uri;
105 Err(UriHandlerError::UnsupportedFeature {
106 operation: "native URI opening",
107 feature: "uri-native",
108 })
109 }
110
111 #[cfg(all(target_arch = "wasm32", not(feature = "uri-web")))]
112 {
113 let _ = uri;
114 Err(UriHandlerError::UnsupportedFeature {
115 operation: "web URI opening",
116 feature: "uri-web",
117 })
118 }
119
120 #[cfg(all(target_os = "android", not(feature = "uri-android")))]
121 {
122 let _ = uri;
123 Err(UriHandlerError::UnsupportedFeature {
124 operation: "Android URI opening",
125 feature: "uri-android",
126 })
127 }
128 }
129}
130
131pub fn default_uri_handler() -> UriHandlerRef {
134 Rc::new(PlatformUriHandler)
135}
136
137pub fn local_uri_handler() -> CompositionLocal<UriHandlerRef> {
140 crate::composition_locals::cached_local(
141 |locals| &locals.uri_handler,
142 || compositionLocalOfWithPolicy(default_uri_handler, Rc::ptr_eq),
143 )
144}
145
146#[composable]
148pub fn ProvideUriHandler(content: impl FnOnce()) {
149 let uri_handler = cranpose_core::remember(default_uri_handler).with(|state| state.clone());
150 let uri_local = local_uri_handler();
151
152 CompositionLocalProvider([uri_local.provides(uri_handler)], move || {
153 content();
154 });
155}
156
157#[cfg(test)]
158#[path = "tests/uri_handler_tests.rs"]
159mod tests;