cranpose_services/
uri_handler.rs1use std::{cell::RefCell, 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
29thread_local! {
30 static PLATFORM_URI_HANDLER: RefCell<Option<UriHandlerRef>> = const { RefCell::new(None) };
31}
32
33pub fn set_platform_uri_handler(handler: UriHandlerRef) {
41 PLATFORM_URI_HANDLER.with(|cell| *cell.borrow_mut() = Some(handler));
42}
43
44pub fn clear_platform_uri_handler() {
46 PLATFORM_URI_HANDLER.with(|cell| *cell.borrow_mut() = None);
47}
48
49fn registered_platform_uri_handler() -> Option<UriHandlerRef> {
50 PLATFORM_URI_HANDLER.with(|cell| cell.borrow().clone())
51}
52
53struct PlatformUriHandler;
54
55impl UriHandler for PlatformUriHandler {
56 fn open_uri(&self, uri: &str) -> Result<(), UriHandlerError> {
57 if let Some(handler) = registered_platform_uri_handler() {
58 return handler.open_uri(uri);
59 }
60
61 #[cfg(all(
62 not(target_arch = "wasm32"),
63 not(target_os = "android"),
64 feature = "uri-native"
65 ))]
66 {
67 open::that(uri).map_err(|err| UriHandlerError::OpenFailed(format!("{err:?}")))?;
68 Ok(())
69 }
70
71 #[cfg(all(target_arch = "wasm32", feature = "uri-web"))]
72 {
73 let window = web_sys::window().ok_or(UriHandlerError::NoWindow)?;
74 let opened = window
75 .open_with_url_and_target(uri, "_blank")
76 .map_err(|err| UriHandlerError::OpenFailed(format!("{err:?}")))?;
77 if opened.is_none() {
78 Err(UriHandlerError::PopupBlocked(uri.to_string()))
79 } else {
80 Ok(())
81 }
82 }
83
84 #[cfg(all(target_os = "android", feature = "uri-android"))]
85 {
86 webbrowser::open(uri).map_err(|err| UriHandlerError::OpenFailed(err.to_string()))?;
87 Ok(())
88 }
89
90 #[cfg(all(
91 not(target_arch = "wasm32"),
92 not(target_os = "android"),
93 not(feature = "uri-native")
94 ))]
95 {
96 let _ = uri;
97 Err(UriHandlerError::UnsupportedFeature {
98 operation: "native URI opening",
99 feature: "uri-native",
100 })
101 }
102
103 #[cfg(all(target_arch = "wasm32", not(feature = "uri-web")))]
104 {
105 let _ = uri;
106 Err(UriHandlerError::UnsupportedFeature {
107 operation: "web URI opening",
108 feature: "uri-web",
109 })
110 }
111
112 #[cfg(all(target_os = "android", not(feature = "uri-android")))]
113 {
114 let _ = uri;
115 Err(UriHandlerError::UnsupportedFeature {
116 operation: "Android URI opening",
117 feature: "uri-android",
118 })
119 }
120 }
121}
122
123pub fn default_uri_handler() -> UriHandlerRef {
124 Rc::new(PlatformUriHandler)
125}
126
127pub fn local_uri_handler() -> CompositionLocal<UriHandlerRef> {
128 thread_local! {
129 static LOCAL_URI_HANDLER: RefCell<Option<CompositionLocal<UriHandlerRef>>> = const { RefCell::new(None) };
130 }
131
132 LOCAL_URI_HANDLER.with(|cell| {
133 let mut local = cell.borrow_mut();
134 local
135 .get_or_insert_with(|| compositionLocalOfWithPolicy(default_uri_handler, Rc::ptr_eq))
136 .clone()
137 })
138}
139
140#[composable]
141pub fn ProvideUriHandler(content: impl FnOnce()) {
142 let uri_handler = cranpose_core::remember(default_uri_handler).with(|state| state.clone());
143 let uri_local = local_uri_handler();
144
145 CompositionLocalProvider(vec![uri_local.provides(uri_handler)], move || {
146 content();
147 });
148}
149
150#[cfg(test)]
151#[path = "tests/uri_handler_tests.rs"]
152mod tests;