cranpose_services/
uri_handler.rs1use cranpose_core::compositionLocalOfWithPolicy;
2use cranpose_core::CompositionLocal;
3use cranpose_core::CompositionLocalProvider;
4use cranpose_macros::composable;
5use std::cell::RefCell;
6use std::rc::Rc;
7
8#[derive(thiserror::Error, Debug)]
9pub enum UriHandlerError {
10 #[error("Failed to open URL: {0}")]
11 OpenFailed(String),
12 #[error("No window object available")]
13 NoWindow,
14 #[error("Popup blocked for URL: {0}")]
15 PopupBlocked(String),
16 #[error("Opening external links is not supported on this platform: {0}")]
17 UnsupportedPlatform(String),
18}
19
20pub trait UriHandler {
21 fn open_uri(&self, uri: &str) -> Result<(), UriHandlerError>;
22}
23
24pub type UriHandlerRef = Rc<dyn UriHandler>;
25
26struct PlatformUriHandler;
27
28impl UriHandler for PlatformUriHandler {
29 fn open_uri(&self, uri: &str) -> Result<(), UriHandlerError> {
30 #[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))]
31 {
32 open::that(uri).map_err(|err| UriHandlerError::OpenFailed(format!("{:?}", err)))?;
33 Ok(())
34 }
35
36 #[cfg(target_arch = "wasm32")]
37 {
38 let window = web_sys::window().ok_or(UriHandlerError::NoWindow)?;
39 let opened = window
40 .open_with_url_and_target(uri, "_blank")
41 .map_err(|err| UriHandlerError::OpenFailed(format!("{:?}", err)))?;
42 if opened.is_none() {
43 Err(UriHandlerError::PopupBlocked(uri.to_string()))
44 } else {
45 Ok(())
46 }
47 }
48
49 #[cfg(target_os = "android")]
50 {
51 webbrowser::open(uri).map_err(|err| UriHandlerError::OpenFailed(err.to_string()))?;
52 Ok(())
53 }
54 }
55}
56
57pub fn default_uri_handler() -> UriHandlerRef {
58 Rc::new(PlatformUriHandler)
59}
60
61pub fn local_uri_handler() -> CompositionLocal<UriHandlerRef> {
62 thread_local! {
63 static LOCAL_URI_HANDLER: RefCell<Option<CompositionLocal<UriHandlerRef>>> = const { RefCell::new(None) };
64 }
65
66 LOCAL_URI_HANDLER.with(|cell| {
67 let mut local = cell.borrow_mut();
68 if local.is_none() {
69 *local = Some(compositionLocalOfWithPolicy(
70 default_uri_handler,
71 Rc::ptr_eq,
72 ));
73 }
74 local
75 .as_ref()
76 .expect("Uri handler composition local must be initialized")
77 .clone()
78 })
79}
80
81#[allow(non_snake_case)]
82#[composable]
83pub fn ProvideUriHandler(content: impl FnOnce()) {
84 let uri_handler = cranpose_core::remember(default_uri_handler).with(|state| state.clone());
85 let uri_local = local_uri_handler();
86
87 CompositionLocalProvider(vec![uri_local.provides(uri_handler)], move || {
88 content();
89 });
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95 use crate::run_test_composition;
96 use cranpose_core::CompositionLocalProvider;
97 use std::cell::RefCell;
98
99 struct TestUriHandler;
100
101 impl UriHandler for TestUriHandler {
102 fn open_uri(&self, _uri: &str) -> Result<(), UriHandlerError> {
103 Ok(())
104 }
105 }
106
107 #[test]
108 fn default_uri_handler_can_be_created() {
109 let handler = default_uri_handler();
110 assert_eq!(Rc::strong_count(&handler), 1);
111 }
112
113 #[test]
114 fn local_uri_handler_can_be_overridden() {
115 let local = local_uri_handler();
116 let default_handler = default_uri_handler();
117 let custom_handler: UriHandlerRef = Rc::new(TestUriHandler);
118 let captured = Rc::new(RefCell::new(None));
119
120 {
121 let captured = Rc::clone(&captured);
122 let custom_handler = custom_handler.clone();
123 let local_for_provider = local.clone();
124 let local_for_read = local.clone();
125 run_test_composition(move || {
126 let captured = Rc::clone(&captured);
127 let custom_handler = custom_handler.clone();
128 let local_for_read = local_for_read.clone();
129 CompositionLocalProvider(
130 vec![local_for_provider.provides(custom_handler)],
131 move || {
132 let current = local_for_read.current();
133 *captured.borrow_mut() = Some(current);
134 },
135 );
136 });
137 }
138
139 let current = captured
140 .borrow()
141 .as_ref()
142 .expect("handler captured")
143 .clone();
144 assert!(Rc::ptr_eq(¤t, &custom_handler));
145 assert!(!Rc::ptr_eq(¤t, &default_handler));
146 }
147
148 #[test]
149 fn provide_uri_handler_sets_current_handler() {
150 let local = local_uri_handler();
151 let captured = Rc::new(RefCell::new(None));
152
153 {
154 let captured = Rc::clone(&captured);
155 let local = local.clone();
156 run_test_composition(move || {
157 let captured = Rc::clone(&captured);
158 let local = local.clone();
159 ProvideUriHandler(move || {
160 let current = local.current();
161 *captured.borrow_mut() = Some(current);
162 });
163 });
164 }
165
166 assert!(captured.borrow().is_some());
167 }
168}