cranpose_services/
share_sheet.rs1use cranpose_core::compositionLocalOfWithPolicy;
12use cranpose_core::CompositionLocal;
13use cranpose_core::CompositionLocalProvider;
14use cranpose_macros::composable;
15use std::cell::RefCell;
16use std::rc::Rc;
17
18#[derive(thiserror::Error, Debug)]
19pub enum ShareError {
20 #[error("sharing is not supported on this platform")]
21 Unsupported,
22 #[error("failed to present the share sheet: {0}")]
23 Failed(String),
24}
25
26#[derive(Clone, Debug, PartialEq, Eq)]
28pub struct ShareContent {
29 pub file_name: String,
31 pub mime_type: String,
33 pub bytes: Vec<u8>,
35 pub text: Option<String>,
37}
38
39impl ShareContent {
40 pub fn file(
42 file_name: impl Into<String>,
43 mime_type: impl Into<String>,
44 bytes: Vec<u8>,
45 ) -> Self {
46 Self {
47 file_name: file_name.into(),
48 mime_type: mime_type.into(),
49 bytes,
50 text: None,
51 }
52 }
53
54 pub fn with_text(mut self, text: impl Into<String>) -> Self {
56 self.text = Some(text.into());
57 self
58 }
59}
60
61pub trait ShareSheet {
64 fn share(&self, content: ShareContent) -> Result<(), ShareError>;
67
68 fn is_supported(&self) -> bool;
71}
72
73pub type ShareSheetRef = Rc<dyn ShareSheet>;
74
75thread_local! {
76 static PLATFORM_SHARE_SHEET: RefCell<Option<ShareSheetRef>> = const { RefCell::new(None) };
77}
78
79pub fn set_platform_share_sheet(share_sheet: ShareSheetRef) {
82 PLATFORM_SHARE_SHEET.with(|cell| *cell.borrow_mut() = Some(share_sheet));
83}
84
85pub fn clear_platform_share_sheet() {
87 PLATFORM_SHARE_SHEET.with(|cell| *cell.borrow_mut() = None);
88}
89
90fn registered_platform_share_sheet() -> Option<ShareSheetRef> {
91 PLATFORM_SHARE_SHEET.with(|cell| cell.borrow().clone())
92}
93
94struct PlatformShareSheet;
95
96impl ShareSheet for PlatformShareSheet {
97 fn share(&self, content: ShareContent) -> Result<(), ShareError> {
98 match registered_platform_share_sheet() {
99 Some(handler) => handler.share(content),
100 None => Err(ShareError::Unsupported),
101 }
102 }
103
104 fn is_supported(&self) -> bool {
105 registered_platform_share_sheet().is_some_and(|handler| handler.is_supported())
106 }
107}
108
109pub fn default_share_sheet() -> ShareSheetRef {
110 Rc::new(PlatformShareSheet)
111}
112
113pub fn local_share_sheet() -> CompositionLocal<ShareSheetRef> {
114 thread_local! {
115 static LOCAL_SHARE_SHEET: RefCell<Option<CompositionLocal<ShareSheetRef>>> = const { RefCell::new(None) };
116 }
117
118 LOCAL_SHARE_SHEET.with(|cell| {
119 let mut local = cell.borrow_mut();
120 local
121 .get_or_insert_with(|| compositionLocalOfWithPolicy(default_share_sheet, Rc::ptr_eq))
122 .clone()
123 })
124}
125
126#[allow(non_snake_case)]
127#[composable]
128pub fn ProvideShareSheet(content: impl FnOnce()) {
129 let share_sheet = cranpose_core::remember(default_share_sheet).with(|state| state.clone());
130 let local = local_share_sheet();
131
132 CompositionLocalProvider(vec![local.provides(share_sheet)], move || {
133 content();
134 });
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140 use std::cell::RefCell;
141
142 struct RecordingShareSheet {
143 shared: RefCell<Option<ShareContent>>,
144 supported: bool,
145 }
146
147 impl ShareSheet for RecordingShareSheet {
148 fn share(&self, content: ShareContent) -> Result<(), ShareError> {
149 *self.shared.borrow_mut() = Some(content);
150 Ok(())
151 }
152 fn is_supported(&self) -> bool {
153 self.supported
154 }
155 }
156
157 #[test]
158 fn default_share_sheet_is_unsupported_without_a_backend() {
159 clear_platform_share_sheet();
160 let sheet = default_share_sheet();
161 assert!(!sheet.is_supported());
162 let error = sheet
163 .share(ShareContent::file(
164 "a.pdf",
165 "application/pdf",
166 vec![1, 2, 3],
167 ))
168 .expect_err("no backend installed");
169 assert!(matches!(error, ShareError::Unsupported));
170 }
171
172 #[test]
173 fn registered_share_sheet_takes_precedence() {
174 let recorder = Rc::new(RecordingShareSheet {
175 shared: RefCell::new(None),
176 supported: true,
177 });
178 set_platform_share_sheet(recorder.clone());
179
180 let sheet = default_share_sheet();
181 assert!(sheet.is_supported());
182 sheet
183 .share(ShareContent::file("r.pdf", "application/pdf", vec![9]).with_text("hi"))
184 .expect("registered backend shares");
185
186 let shared = recorder.shared.borrow();
187 let shared = shared.as_ref().expect("content recorded");
188 assert_eq!(shared.file_name, "r.pdf");
189 assert_eq!(shared.text.as_deref(), Some("hi"));
190
191 clear_platform_share_sheet();
192 assert!(!default_share_sheet().is_supported());
193 }
194}