Skip to main content

cranpose_services/
uri_handler.rs

1use 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(&current, &custom_handler));
145        assert!(!Rc::ptr_eq(&current, &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}