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