cranpose_services/
uri_handler.rs1use cranpose_core::compositionLocalOf;
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(compositionLocalOf(default_uri_handler));
70 }
71 local
72 .as_ref()
73 .expect("Uri handler composition local must be initialized")
74 .clone()
75 })
76}
77
78#[allow(non_snake_case)]
79#[composable]
80pub fn ProvideUriHandler(content: impl FnOnce()) {
81 let uri_handler = cranpose_core::remember(default_uri_handler).with(|state| state.clone());
82 let uri_local = local_uri_handler();
83
84 CompositionLocalProvider(vec![uri_local.provides(uri_handler)], move || {
85 content();
86 });
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92 use crate::run_test_composition;
93 use cranpose_core::CompositionLocalProvider;
94 use std::cell::RefCell;
95
96 struct TestUriHandler;
97
98 impl UriHandler for TestUriHandler {
99 fn open_uri(&self, _uri: &str) -> Result<(), UriHandlerError> {
100 Ok(())
101 }
102 }
103
104 #[test]
105 fn default_uri_handler_can_be_created() {
106 let handler = default_uri_handler();
107 assert_eq!(Rc::strong_count(&handler), 1);
108 }
109
110 #[test]
111 fn local_uri_handler_can_be_overridden() {
112 let local = local_uri_handler();
113 let default_handler = default_uri_handler();
114 let custom_handler: UriHandlerRef = Rc::new(TestUriHandler);
115 let captured = Rc::new(RefCell::new(None));
116
117 {
118 let captured = Rc::clone(&captured);
119 let custom_handler = custom_handler.clone();
120 let local_for_provider = local.clone();
121 let local_for_read = local.clone();
122 run_test_composition(move || {
123 let captured = Rc::clone(&captured);
124 let custom_handler = custom_handler.clone();
125 let local_for_read = local_for_read.clone();
126 CompositionLocalProvider(
127 vec![local_for_provider.provides(custom_handler)],
128 move || {
129 let current = local_for_read.current();
130 *captured.borrow_mut() = Some(current);
131 },
132 );
133 });
134 }
135
136 let current = captured
137 .borrow()
138 .as_ref()
139 .expect("handler captured")
140 .clone();
141 assert!(Rc::ptr_eq(¤t, &custom_handler));
142 assert!(!Rc::ptr_eq(¤t, &default_handler));
143 }
144
145 #[test]
146 fn provide_uri_handler_sets_current_handler() {
147 let local = local_uri_handler();
148 let captured = Rc::new(RefCell::new(None));
149
150 {
151 let captured = Rc::clone(&captured);
152 let local = local.clone();
153 run_test_composition(move || {
154 let captured = Rc::clone(&captured);
155 let local = local.clone();
156 ProvideUriHandler(move || {
157 let current = local.current();
158 *captured.borrow_mut() = Some(current);
159 });
160 });
161 }
162
163 assert!(captured.borrow().is_some());
164 }
165}